@zappdev/runtime 0.1.0 → 0.6.0-alpha.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.
package/events.ts CHANGED
@@ -1,249 +1,127 @@
1
- /** Numeric identifiers for window lifecycle and state-change events. */
1
+ /**
2
+ * Events — global event bus for cross-context communication.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { Events } from "@zappdev/runtime";
7
+ *
8
+ * // Listen for events
9
+ * const off = Events.on("my-event", (data) => console.log(data));
10
+ *
11
+ * // Emit events (fire-and-forget, all listeners receive)
12
+ * Events.emit("my-event", { hello: "world" });
13
+ *
14
+ * // Unsubscribe
15
+ * off();
16
+ * ```
17
+ */
18
+
19
+ import { getBridge } from "./bridge";
20
+
21
+ /** Numeric identifiers for window events. */
2
22
  export enum WindowEvent {
3
- /** The window has finished loading and is ready. */
4
- READY = 0,
5
- /** The window gained input focus. */
6
- FOCUS = 1,
7
- /** The window lost input focus. */
8
- BLUR = 2,
9
- /** The window was resized. */
10
- RESIZE = 3,
11
- /** The window was moved. */
12
- MOVE = 4,
13
- /** The window close was requested. */
14
- CLOSE = 5,
15
- /** The window was minimized. */
16
- MINIMIZE = 6,
17
- /** The window was maximized. */
18
- MAXIMIZE = 7,
19
- /** The window was restored from minimize or maximize. */
20
- RESTORE = 8,
21
- /** The window entered fullscreen mode. */
22
- FULLSCREEN = 9,
23
- /** The window exited fullscreen mode. */
24
- UNFULLSCREEN = 10,
23
+ READY = 0,
24
+ FOCUS = 1,
25
+ BLUR = 2,
26
+ RESIZE = 3,
27
+ MOVE = 4,
28
+ CLOSE = 5,
29
+ MINIMIZE = 6,
30
+ MAXIMIZE = 7,
31
+ RESTORE = 8,
32
+ FULLSCREEN = 9,
33
+ UNFULLSCREEN = 10,
25
34
  }
26
35
 
27
- /** Numeric identifiers for application lifecycle events. */
36
+ /** App lifecycle events.
37
+ * STARTED/SHUTDOWN are backend-only (fire before/after WebViews exist).
38
+ * The rest are available in both frontend and backend. */
28
39
  export enum AppEvent {
29
- /** The application has started. */
30
- STARTED = 100,
31
- /** The application is shutting down. */
32
- SHUTDOWN = 101,
40
+ STARTED = 100, // backend-only
41
+ SHUTDOWN = 101, // backend-only
42
+ REOPEN = 104, // dock icon clicked
43
+ OPEN_URL = 105, // deep link opened
44
+ DID_BECOME_ACTIVE = 106,
45
+ DID_RESIGN_ACTIVE = 107,
33
46
  }
34
47
 
48
+ /** Map WindowEvent enum to string event names. */
35
49
  const WINDOW_EVENT_NAMES: Record<number, string> = {
36
- [WindowEvent.READY]: "ready",
37
- [WindowEvent.FOCUS]: "focus",
38
- [WindowEvent.BLUR]: "blur",
39
- [WindowEvent.RESIZE]: "resize",
40
- [WindowEvent.MOVE]: "move",
41
- [WindowEvent.CLOSE]: "close",
42
- [WindowEvent.MINIMIZE]: "minimize",
43
- [WindowEvent.MAXIMIZE]: "maximize",
44
- [WindowEvent.RESTORE]: "restore",
45
- [WindowEvent.FULLSCREEN]: "fullscreen",
46
- [WindowEvent.UNFULLSCREEN]: "unfullscreen",
50
+ [WindowEvent.READY]: "window:ready",
51
+ [WindowEvent.FOCUS]: "window:focus",
52
+ [WindowEvent.BLUR]: "window:blur",
53
+ [WindowEvent.RESIZE]: "window:resize",
54
+ [WindowEvent.MOVE]: "window:move",
55
+ [WindowEvent.CLOSE]: "window:close",
56
+ [WindowEvent.MINIMIZE]: "window:minimize",
57
+ [WindowEvent.MAXIMIZE]: "window:maximize",
58
+ [WindowEvent.RESTORE]: "window:restore",
59
+ [WindowEvent.FULLSCREEN]: "window:fullscreen",
60
+ [WindowEvent.UNFULLSCREEN]: "window:unfullscreen",
47
61
  };
48
62
 
49
63
  const APP_EVENT_NAMES: Record<number, string> = {
50
- [AppEvent.STARTED]: "app:started",
51
- [AppEvent.SHUTDOWN]: "app:shutdown",
64
+ [AppEvent.STARTED]: "app:started",
65
+ [AppEvent.SHUTDOWN]: "app:shutdown",
66
+ [AppEvent.REOPEN]: "app:reopen",
67
+ [AppEvent.OPEN_URL]: "app:open-url",
68
+ [AppEvent.DID_BECOME_ACTIVE]: "app:active",
69
+ [AppEvent.DID_RESIGN_ACTIVE]: "app:inactive",
52
70
  };
53
71
 
54
- type EventHandler = (payload?: unknown) => void;
55
-
56
- type ZappBridge = {
57
- _listeners?: Record<string, Array<{ id: number; fn: EventHandler; once?: boolean }>>;
58
- _lastId?: number;
59
- _emit?: (name: string, payload: unknown) => boolean;
60
- _onEvent?: (name: string, handler: EventHandler) => number;
61
- _offEvent?: (name: string, id: number) => void;
62
- _onceEvent?: (name: string, handler: EventHandler) => number;
63
- _offAllEvents?: (name?: string) => void;
64
- };
65
-
66
- const BRIDGE_SYMBOL = Symbol.for("zapp.bridge");
67
-
68
- const getBridge = (): ZappBridge => {
69
- const bridge = (globalThis as unknown as Record<symbol, unknown>)[BRIDGE_SYMBOL] as ZappBridge | undefined;
70
- if (!bridge) {
71
- throw new Error("Zapp bridge is unavailable. Is the bootstrap loaded?");
72
- }
73
- return bridge;
74
- };
75
-
76
- const ensureBridge = (): ZappBridge => {
77
- const symbolStore = globalThis as unknown as Record<symbol, unknown>;
78
- let bridge = symbolStore[BRIDGE_SYMBOL] as ZappBridge | undefined;
79
- if (!bridge) {
80
- bridge = { _listeners: {}, _lastId: 0 };
81
- try {
82
- Object.defineProperty(symbolStore, BRIDGE_SYMBOL, {
83
- value: bridge, enumerable: false, configurable: true, writable: false,
84
- });
85
- } catch {
86
- // @ts-ignore -- fallback for non-configurable
87
- symbolStore[BRIDGE_SYMBOL] = bridge;
88
- }
89
- }
90
- if (!bridge._listeners) bridge._listeners = {};
91
- return bridge;
92
- };
93
-
94
- /** Base payload delivered with window events. */
95
- export interface WindowEventPayload {
96
- /** The ID of the window that emitted the event. */
97
- windowId: string;
98
- /** Unix-epoch millisecond timestamp of when the event occurred. */
99
- timestamp: number;
100
- /** Window dimensions, present on size-related events. */
101
- size?: { width: number; height: number };
102
- /** Window screen coordinates, present on position-related events. */
103
- position?: { x: number; y: number };
72
+ /** Payload for window events that include size and position. */
73
+ export interface WindowSizePayload {
74
+ windowId: string;
75
+ timestamp: number;
76
+ size: { width: number; height: number };
77
+ position: { x: number; y: number };
104
78
  }
105
79
 
106
- /** Payload for events that always include size and position (resize, move, maximize, restore) */
107
- export interface WindowSizeEventPayload {
108
- windowId: string;
109
- timestamp: number;
110
- size: { width: number; height: number };
111
- position: { x: number; y: number };
80
+ /** Payload for simple window events (focus, blur, minimize, etc). */
81
+ export interface WindowPayload {
82
+ windowId: string;
83
+ timestamp: number;
112
84
  }
113
85
 
114
- // ---------------------------------------------------------------------------
115
- // Known event name payload type mapping
116
- // ---------------------------------------------------------------------------
117
-
118
- /** Window events that carry size + position data */
119
- type WindowSizeEvents =
120
- | "window:resize"
121
- | "window:move"
122
- | "window:maximize"
123
- | "window:restore";
124
-
125
- /** Window events without size/position data */
126
- type WindowSimpleEvents =
127
- | "window:ready"
128
- | "window:focus"
129
- | "window:blur"
130
- | "window:close"
131
- | "window:minimize"
132
- | "window:fullscreen"
133
- | "window:unfullscreen";
134
-
135
- /** App lifecycle events */
136
- type AppEvents =
137
- | "app:started"
138
- | "app:shutdown";
139
-
140
- /** All known Zapp event names */
141
- export type KnownEventName = WindowSizeEvents | WindowSimpleEvents | AppEvents;
86
+ /** Known window events that carry size+position data. */
87
+ type SizeEvents = "window:resize" | "window:move" | "window:maximize" | "window:restore";
142
88
 
143
- /** Resolve the payload type for a given event name */
144
- export type EventPayloadFor<T extends string> =
145
- T extends WindowSizeEvents ? WindowSizeEventPayload :
146
- T extends WindowSimpleEvents ? WindowEventPayload :
147
- T extends AppEvents ? undefined :
148
- unknown;
89
+ /** Known window events without size data. */
90
+ type SimpleEvents = "window:ready" | "window:focus" | "window:blur" | "window:close"
91
+ | "window:minimize" | "window:fullscreen" | "window:unfullscreen";
149
92
 
150
- /** Event name type — known names get autocomplete, arbitrary strings still work */
151
- export type EventName = KnownEventName | (string & {});
93
+ /** App event string names. */
94
+ type AppEvents = "app:started" | "app:shutdown" | "app:reopen" | "app:open-url" | "app:active" | "app:inactive";
152
95
 
153
- /** Type-safe event emitter API for subscribing to and emitting Zapp events. */
154
- export interface EventsAPI {
155
- /** Emit a named event with an optional payload. */
156
- emit(name: string, payload?: unknown): unknown;
96
+ /** All known event names. Arbitrary strings also work. */
97
+ export type EventName = SizeEvents | SimpleEvents | AppEvents | (string & {});
157
98
 
158
- /** Subscribe to a window size/position event. Returns an unsubscribe function. */
159
- on(name: WindowSizeEvents, handler: (payload: WindowSizeEventPayload) => void): () => void;
160
- /** Subscribe to a simple window event. Returns an unsubscribe function. */
161
- on(name: WindowSimpleEvents, handler: (payload: WindowEventPayload) => void): () => void;
162
- /** Subscribe to an app lifecycle event. Returns an unsubscribe function. */
163
- on(name: AppEvents, handler: () => void): () => void;
164
- /** Subscribe to a custom event. Returns an unsubscribe function. */
165
- on(name: string & {}, handler: (payload?: unknown) => void): () => void;
166
-
167
- /** Subscribe to a window size/position event once. Returns an unsubscribe function. */
168
- once(name: WindowSizeEvents, handler: (payload: WindowSizeEventPayload) => void): () => void;
169
- /** Subscribe to a simple window event once. Returns an unsubscribe function. */
170
- once(name: WindowSimpleEvents, handler: (payload: WindowEventPayload) => void): () => void;
171
- /** Subscribe to an app lifecycle event once. Returns an unsubscribe function. */
172
- once(name: AppEvents, handler: () => void): () => void;
173
- /** Subscribe to a custom event once. Returns an unsubscribe function. */
174
- once(name: string & {}, handler: (payload?: unknown) => void): () => void;
175
-
176
- /** Remove a specific handler, or all handlers, for the given event name. */
177
- off(name: EventName, handler?: EventHandler): void;
178
- /** Remove all handlers for a given event name, or all events if no name is provided. */
179
- offAll(name?: EventName): void;
99
+ /** Resolve event name from enum to string. */
100
+ export function eventName(event: WindowEvent | AppEvent): string {
101
+ return WINDOW_EVENT_NAMES[event] ?? APP_EVENT_NAMES[event] ?? `unknown:${event}`;
180
102
  }
181
103
 
182
- /** The singleton event bus for emitting, subscribing to, and removing event listeners. */
183
- export const Events = {
184
- emit(name: string, payload?: unknown): unknown {
185
- return getBridge()._emit?.(name, payload);
186
- },
187
-
188
- on(name: string, handler: EventHandler): () => void {
189
- const bridge = ensureBridge();
190
- if (bridge._onEvent) {
191
- const id = bridge._onEvent(name, handler);
192
- return () => bridge._offEvent?.(name, id);
193
- }
194
- const listeners = bridge._listeners!;
195
- const id = (bridge._lastId = (bridge._lastId ?? 0) + 1);
196
- (listeners[name] ??= []).push({ id, fn: handler });
197
- return () => {
198
- listeners[name] = (listeners[name] ?? []).filter((e) => e.id !== id);
199
- };
200
- },
201
-
202
- once(name: string, handler: EventHandler): () => void {
203
- const bridge = ensureBridge();
204
- if (bridge._onceEvent) {
205
- const id = bridge._onceEvent(name, handler);
206
- return () => bridge._offEvent?.(name, id);
207
- }
208
- const listeners = bridge._listeners!;
209
- const id = (bridge._lastId = (bridge._lastId ?? 0) + 1);
210
- (listeners[name] ??= []).push({ id, fn: handler, once: true });
211
- return () => {
212
- listeners[name] = (listeners[name] ?? []).filter((e) => e.id !== id);
213
- };
214
- },
215
-
216
- off(name: string, handler?: EventHandler): void {
217
- const bridge = getBridge();
218
- if (!handler) {
219
- bridge._offAllEvents?.(name) ??
220
- ((bridge._listeners ?? {})[name] = []);
221
- return;
222
- }
223
- const listeners = bridge._listeners ?? {};
224
- listeners[name] = (listeners[name] ?? []).filter((e) => e.fn !== handler);
225
- },
104
+ type EventHandler = (payload: any) => void;
226
105
 
227
- offAll(name?: string): void {
228
- const bridge = getBridge();
229
- if (bridge._offAllEvents) {
230
- bridge._offAllEvents(name);
231
- return;
232
- }
233
- if (name) {
234
- (bridge._listeners ?? {})[name] = [];
235
- } else {
236
- bridge._listeners = {};
237
- }
238
- },
239
- } satisfies Record<string, unknown> as EventsAPI;
240
-
241
- /** Resolve a {@link WindowEvent} enum member to its string event name. */
242
- export function getWindowEventName(event: WindowEvent): string {
243
- return WINDOW_EVENT_NAMES[event] ?? `window:${event}`;
244
- }
245
-
246
- /** Resolve an {@link AppEvent} enum member to its string event name. */
247
- export function getAppEventName(event: AppEvent): string {
248
- return APP_EVENT_NAMES[event] ?? `app:${event}`;
249
- }
106
+ export const Events = {
107
+ /**
108
+ * Subscribe to an event. Returns an unsubscribe function.
109
+ */
110
+ on(name: EventName, handler: EventHandler): () => void {
111
+ return getBridge().on(name, handler);
112
+ },
113
+
114
+ /**
115
+ * Emit a fire-and-forget event.
116
+ *
117
+ * - From a **webview**: dispatched locally to listeners in the same window
118
+ * (and to native listeners). Does not cross window boundaries.
119
+ * - From the **backend** worker: broadcast to *every* open webview's
120
+ * listeners. This is the canonical pattern for pushing backend-owned
121
+ * state to all windows without per-window polling.
122
+ * - From a **regular worker**: same as backend — broadcast to every webview.
123
+ */
124
+ emit(name: string, payload?: Record<string, unknown>): void {
125
+ getBridge().emit(name, payload);
126
+ },
127
+ };
package/index.ts CHANGED
@@ -1,39 +1,31 @@
1
1
  /**
2
2
  * @module
3
- * Frontend runtime API for Zapp desktop apps.
4
- *
5
- * Provides window management, events, dialogs, menus, workers, sync primitives,
6
- * and service invocation for building cross-platform desktop applications.
3
+ * Zapp Runtime — frontend API for Zapp desktop apps.
7
4
  *
8
5
  * @example
9
6
  * ```ts
10
- * import { App, Window, WindowEvent, Dialog, Menu, Events } from "@zapp/runtime";
7
+ * import { App, Window, WindowEvent, Events, Services } from "@zappdev/runtime";
11
8
  *
12
9
  * Window.current().on(WindowEvent.READY, () => {
13
10
  * Window.current().show();
14
11
  * });
15
12
  *
16
- * const result = await Dialog.message({
17
- * message: "Hello from Zapp!",
18
- * buttons: ["OK"],
19
- * });
13
+ * const result = await Services.invoke("greet", { name: "World" });
20
14
  * ```
21
15
  */
22
16
 
23
17
  export { App } from "./app";
24
- export type { AppAPI, AppConfig } from "./app";
25
- export { Events, WindowEvent, AppEvent, getWindowEventName, getAppEventName } from "./events";
26
- export type { EventsAPI, WindowEventPayload, WindowSizeEventPayload, EventName, KnownEventName, EventPayloadFor } from "./events";
27
- export { Window } from "./windows";
28
- export type { WindowAPI, WindowOptions, WindowHandle } from "./windows";
29
- export { Worker, SharedWorker } from "./worker";
30
- export { Services } from "./services";
31
- export type { ServicesAPI } from "./services";
32
- export { Sync } from "./sync";
33
- export type { SyncAPI, SyncWaitOptions } from "./sync";
34
- export { Dialog } from "./dialog";
35
- export type { DialogAPI, OpenFileOptions, SaveFileOptions, MessageOptions, FileFilter, OpenFileResult, SaveFileResult, MessageResult } from "./dialog";
36
- export { Menu } from "./menu";
37
- export type { MenuAPI, MenuItemDef, MenuHandle } from "./menu";
38
- export * from "./protocol";
39
- export * from "./bindings-contract";
18
+ export { Window, type WindowHandle, type WindowOptions } from "./window";
19
+ export { Events, WindowEvent, AppEvent, eventName, type WindowPayload, type WindowSizePayload, type EventName } from "./events";
20
+ export { Services, type InvokeOptions, type CancellablePromise } from "./services";
21
+ export { Worker, SharedWorker, SharedWorkerPort, type WorkerMessageEvent } from "./worker";
22
+ export { Dialog, type OpenFileOptions, type SaveFileOptions, type MessageOptions, type OpenFileResult, type SaveFileResult, type MessageResult } from "./dialog";
23
+ export { Menu, type MenuItemDef, type MenuHandle } from "./menu";
24
+ export { ContextMenu, type ContextMenuOptions } from "./context-menu";
25
+ export { Notification, type NotificationOptions, type ScheduleOptions, type PermissionStatus } from "./notification";
26
+ export { Sync, type SyncWaitOptions } from "./sync";
27
+ export { Dock } from "./dock";
28
+
29
+ // Re-export worker globals type declarations.
30
+ // Workers should add: import "@zappdev/runtime/worker-globals";
31
+ // This registers send/receive/postMessage/onmessage on the global scope.
package/menu.ts CHANGED
@@ -1,98 +1,89 @@
1
+ /**
2
+ * Menu — application menu bar.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { Menu, App } from "@zappdev/runtime";
7
+ *
8
+ * Menu.build([
9
+ * { role: "appMenu" },
10
+ * { label: "File", submenu: [
11
+ * { label: "New", accelerator: "CmdOrCtrl+N", action: () => console.log("New!") },
12
+ * { type: "separator" },
13
+ * { label: "Quit", accelerator: "CmdOrCtrl+Q", action: () => App.quit() },
14
+ * ]},
15
+ * { label: "Edit", role: "editMenu" },
16
+ * { label: "Window", role: "windowMenu" },
17
+ * ]);
18
+ * ```
19
+ */
20
+
21
+ import { getBridge } from "./bridge";
1
22
  import { Events } from "./events";
2
23
 
3
- /** Definition of a single menu item, including separators, checkboxes, and submenus. */
4
24
  export interface MenuItemDef {
5
- /** Unique ID for click events. Auto-generated if action is provided without id. */
6
25
  id?: string;
7
- /** Display text. Required unless type is "separator" or role is set. */
8
26
  label?: string;
9
- /** Item type. Default: "normal" */
10
27
  type?: "normal" | "separator" | "checkbox";
11
- /** Whether the item is clickable. Default: true */
12
28
  enabled?: boolean;
13
- /** For checkbox items. Default: false */
14
29
  checked?: boolean;
15
- /** Keyboard shortcut. Use "CmdOrCtrl" for cross-platform Cmd/Ctrl. */
16
30
  accelerator?: string;
17
- /** Built-in menu role. Overrides label/submenu with standard items. */
18
- role?: "editMenu" | "windowMenu" | "appMenu";
19
- /** Click handler. Sugar for menu.on(id, handler). */
31
+ role?: "editMenu" | "windowMenu" | "appMenu" | "copy" | "cut" | "paste" | "selectAll" | "undo" | "redo" | "quit";
20
32
  action?: () => void;
21
- /** Nested submenu items. */
22
33
  submenu?: MenuItemDef[];
23
34
  }
24
35
 
25
- /** Handle returned after building a menu, used to listen for item clicks. */
26
36
  export interface MenuHandle {
27
- /** Listen for a menu item click by its ID */
28
- on(itemId: string, handler: () => void): () => void;
29
- /** The raw menu definition (for re-serialization) */
30
37
  readonly items: MenuItemDef[];
31
38
  }
32
39
 
33
- /** API for building and installing native menus. */
34
- export interface MenuAPI {
35
- /** Build a menu from a definition array */
36
- build(items: MenuItemDef[]): MenuHandle;
37
- }
38
-
39
- let menuIdSeq = 0;
40
+ let menuActionCounter = 0;
40
41
 
41
- type ActionEntry = { id: string; handler: () => void };
42
+ function collectActions(items: MenuItemDef[]): Map<string, () => void> {
43
+ const actions = new Map<string, () => void>();
42
44
 
43
- function collectActions(items: MenuItemDef[], actions: ActionEntry[]): MenuItemDef[] {
44
- return items.map((item) => {
45
- const copy = { ...item };
46
- if (copy.action && !copy.id) {
47
- copy.id = `__menu_${++menuIdSeq}`;
48
- }
49
- if (copy.action && copy.id) {
50
- actions.push({ id: copy.id, handler: copy.action });
45
+ function walk(items: MenuItemDef[]) {
46
+ for (const item of items) {
47
+ if (item.action) {
48
+ if (!item.id) {
49
+ item.id = `__menu_${++menuActionCounter}`;
50
+ }
51
+ actions.set(item.id, item.action);
52
+ }
53
+ if (item.submenu) walk(item.submenu);
51
54
  }
52
- // Strip action from the serialized def (not JSON-serializable)
53
- delete copy.action;
54
- if (copy.submenu) {
55
- copy.submenu = collectActions(copy.submenu, actions);
56
- }
57
- return copy;
58
- });
59
- }
60
-
61
- function postMenu(items: MenuItemDef[]): void {
62
- const handler = (globalThis as unknown as Record<string, Record<string, Record<string, { postMessage?: (m: string) => void }>>>)
63
- .webkit?.messageHandlers?.zapp;
64
- const chromeWebview = (globalThis as unknown as Record<string, Record<string, { postMessage?: (m: string) => void }>>)
65
- .chrome?.webview;
55
+ }
66
56
 
67
- const msg = `app\nsetMenu\n${JSON.stringify({ items })}`;
57
+ walk(items);
58
+ return actions;
59
+ }
68
60
 
69
- if (handler?.postMessage) {
70
- handler.postMessage(msg);
71
- } else if (chromeWebview?.postMessage) {
72
- chromeWebview.postMessage(msg);
73
- }
61
+ function stripActions(items: MenuItemDef[]): any[] {
62
+ return items.map(item => {
63
+ const clean: any = { ...item };
64
+ delete clean.action;
65
+ if (clean.submenu) clean.submenu = stripActions(clean.submenu);
66
+ return clean;
67
+ });
74
68
  }
75
69
 
76
- /** The singleton menu API instance. */
77
- export const Menu: MenuAPI = {
70
+ export const Menu = {
78
71
  build(items: MenuItemDef[]): MenuHandle {
79
- const actions: ActionEntry[] = [];
80
- const cleanItems = collectActions(items, actions);
81
-
82
- // Post the cleaned (no functions) menu to native
83
- postMenu(cleanItems);
72
+ const actions = collectActions(items);
73
+ const clean = stripActions(items);
84
74
 
85
- // Wire up inline action handlers
86
- const offs: (() => void)[] = [];
87
- for (const { id, handler } of actions) {
88
- offs.push(Events.on(`menu:${id}`, handler as () => void));
75
+ // Wire up action event listeners
76
+ if (actions.size > 0) {
77
+ Events.on("__menu:click", (payload: any) => {
78
+ const id = typeof payload === "string" ? JSON.parse(payload).id : payload?.id;
79
+ const handler = actions.get(id);
80
+ if (handler) handler();
81
+ });
89
82
  }
90
83
 
91
- return {
92
- get items() { return cleanItems; },
93
- on(itemId: string, handler: () => void): () => void {
94
- return Events.on(`menu:${itemId}`, handler as () => void);
95
- },
96
- };
84
+ // Send to native
85
+ (getBridge() as any).post(JSON.stringify({ t: 4, m: "setMenu", a: { items: clean } }));
86
+
87
+ return { items };
97
88
  },
98
89
  };