@zappdev/runtime 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.
- package/README.md +51 -0
- package/app.ts +91 -0
- package/bindings-contract.ts +31 -0
- package/dialog.ts +148 -0
- package/events.ts +249 -0
- package/index.ts +39 -0
- package/menu.ts +98 -0
- package/package.json +13 -0
- package/protocol.ts +94 -0
- package/services.ts +91 -0
- package/sync.ts +62 -0
- package/windows.ts +222 -0
- package/worker.ts +287 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @zapp/runtime
|
|
2
|
+
|
|
3
|
+
Frontend runtime API for Zapp desktop apps. Provides type-safe access to native window management, events, dialogs, menus, services, and more from your web frontend.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @zapp/runtime
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @zapp/runtime
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { App, Window, Events, Dialog, Menu } from "@zapp/runtime";
|
|
19
|
+
|
|
20
|
+
// Listen for window events
|
|
21
|
+
Events.on("window:resize", (payload) => {
|
|
22
|
+
console.log("Window resized:", payload.width, payload.height);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Open a file dialog
|
|
26
|
+
const result = await Dialog.openFile({
|
|
27
|
+
title: "Select a file",
|
|
28
|
+
filters: [{ name: "Images", extensions: ["png", "jpg"] }],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Create a new window
|
|
32
|
+
await Window.open({ title: "My Window", width: 800, height: 600 });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Window management** -- create, resize, move, and close native windows
|
|
38
|
+
- **Event system** -- subscribe to window and app lifecycle events
|
|
39
|
+
- **Dialogs** -- native open/save file dialogs and message boxes
|
|
40
|
+
- **Menus** -- build native application and context menus
|
|
41
|
+
- **Services** -- communicate with backend services
|
|
42
|
+
- **Workers** -- spawn Web Workers and Shared Workers with native integration
|
|
43
|
+
- **Sync primitives** -- cross-thread synchronization utilities
|
|
44
|
+
|
|
45
|
+
## Docs
|
|
46
|
+
|
|
47
|
+
See the full documentation in [`../../docs/`](../../docs/).
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/app.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Events } from "./events";
|
|
2
|
+
|
|
3
|
+
/** Configuration for a Zapp application. */
|
|
4
|
+
export interface AppConfig {
|
|
5
|
+
/** The display name of the application. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Whether the app should quit when its last window is closed. */
|
|
8
|
+
applicationShouldTerminateAfterLastWindowClosed: boolean;
|
|
9
|
+
/** Whether web content can be inspected via dev tools. */
|
|
10
|
+
webContentInspectable: boolean;
|
|
11
|
+
/** Maximum number of concurrent workers. */
|
|
12
|
+
maxWorkers?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type AppBridge = {
|
|
16
|
+
getConfig?: () => AppConfig | null;
|
|
17
|
+
appAction?: (action: string, payload?: unknown) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ReadyCallback = () => void | Promise<void>;
|
|
21
|
+
|
|
22
|
+
function getBridge(): AppBridge | null {
|
|
23
|
+
return ((globalThis as unknown as Record<symbol, unknown>)[
|
|
24
|
+
Symbol.for("zapp.bridge")
|
|
25
|
+
] as AppBridge | undefined) ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isMainContext(): boolean {
|
|
29
|
+
return (globalThis as unknown as Record<symbol, unknown>)[
|
|
30
|
+
Symbol.for("zapp.context")
|
|
31
|
+
] !== "worker";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultConfig: AppConfig = {
|
|
35
|
+
name: "Zapp App",
|
|
36
|
+
applicationShouldTerminateAfterLastWindowClosed: false,
|
|
37
|
+
webContentInspectable: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Top-level application API for lifecycle, visibility, and configuration. */
|
|
41
|
+
export interface AppAPI {
|
|
42
|
+
/** Returns the current application configuration. */
|
|
43
|
+
getConfig(): AppConfig;
|
|
44
|
+
/** Registers a callback to run when the app is ready. */
|
|
45
|
+
onReady(callback: ReadyCallback): void;
|
|
46
|
+
/** Quits the application. */
|
|
47
|
+
quit(): void;
|
|
48
|
+
/** Hides the application. */
|
|
49
|
+
hide(): void;
|
|
50
|
+
/** Shows the application. */
|
|
51
|
+
show(): void;
|
|
52
|
+
/** Opens a URL in the user's default browser. */
|
|
53
|
+
openExternal(url: string): void;
|
|
54
|
+
/** Sets the application menu from a menu definition. */
|
|
55
|
+
setMenu(menu: { items: unknown[] }): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The singleton application API instance. */
|
|
59
|
+
export const App: AppAPI = {
|
|
60
|
+
getConfig(): AppConfig {
|
|
61
|
+
return getBridge()?.getConfig?.() ?? defaultConfig;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
onReady(callback: ReadyCallback): void {
|
|
65
|
+
if (!isMainContext()) {
|
|
66
|
+
console.warn("App.onReady() is only available in the main/webview context.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
Events.on("ready", callback);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
quit(): void {
|
|
73
|
+
getBridge()?.appAction?.("quit");
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
hide(): void {
|
|
77
|
+
getBridge()?.appAction?.("hide");
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
show(): void {
|
|
81
|
+
getBridge()?.appAction?.("show");
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
openExternal(url: string): void {
|
|
85
|
+
getBridge()?.appAction?.("openExternal", { url });
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
setMenu(menu: { items: unknown[] }): void {
|
|
89
|
+
getBridge()?.appAction?.("setMenu", { items: menu.items });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Current version of the bindings schema format. */
|
|
2
|
+
export const ZAPP_BINDINGS_SCHEMA_VERSION = 1;
|
|
3
|
+
|
|
4
|
+
/** Describes a single method exposed by a service binding. */
|
|
5
|
+
export type ZappServiceBindingMethod = {
|
|
6
|
+
name: string;
|
|
7
|
+
requestType?: string;
|
|
8
|
+
responseType?: string;
|
|
9
|
+
capability?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Describes a service and the methods it exposes. */
|
|
13
|
+
export type ZappServiceBinding = {
|
|
14
|
+
name: string;
|
|
15
|
+
namespace?: string;
|
|
16
|
+
methods: ZappServiceBindingMethod[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Top-level manifest listing all available service bindings. */
|
|
20
|
+
export type ZappBindingsManifest = {
|
|
21
|
+
v: typeof ZAPP_BINDINGS_SCHEMA_VERSION;
|
|
22
|
+
generatedAt: string;
|
|
23
|
+
services: ZappServiceBinding[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Create an empty bindings manifest with the current schema version. */
|
|
27
|
+
export const emptyBindingsManifest = (): ZappBindingsManifest => ({
|
|
28
|
+
v: ZAPP_BINDINGS_SCHEMA_VERSION,
|
|
29
|
+
generatedAt: new Date().toISOString(),
|
|
30
|
+
services: [],
|
|
31
|
+
});
|
package/dialog.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Events } from "./events";
|
|
2
|
+
|
|
3
|
+
/** A file type filter for open/save dialogs. */
|
|
4
|
+
export interface FileFilter {
|
|
5
|
+
/** Display name for the filter (e.g. "Images"). */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Allowed file extensions without dots (e.g. ["png", "jpg"]). */
|
|
8
|
+
extensions: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Options for the open-file dialog. */
|
|
12
|
+
export interface OpenFileOptions {
|
|
13
|
+
/** Dialog window title. */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Initial directory or file path. */
|
|
16
|
+
defaultPath?: string;
|
|
17
|
+
/** File type filters shown in the dialog. */
|
|
18
|
+
filters?: FileFilter[];
|
|
19
|
+
/** Whether the user can select multiple files. */
|
|
20
|
+
multiple?: boolean;
|
|
21
|
+
/** Whether to select directories instead of files. */
|
|
22
|
+
directory?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options for the save-file dialog. */
|
|
26
|
+
export interface SaveFileOptions {
|
|
27
|
+
/** Dialog window title. */
|
|
28
|
+
title?: string;
|
|
29
|
+
/** Initial directory path. */
|
|
30
|
+
defaultPath?: string;
|
|
31
|
+
/** Default file name pre-filled in the dialog. */
|
|
32
|
+
defaultName?: string;
|
|
33
|
+
/** File type filters shown in the dialog. */
|
|
34
|
+
filters?: FileFilter[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Options for a message dialog (alert/confirm). */
|
|
38
|
+
export interface MessageOptions {
|
|
39
|
+
/** Dialog window title. */
|
|
40
|
+
title?: string;
|
|
41
|
+
/** The message body text. */
|
|
42
|
+
message: string;
|
|
43
|
+
/** Severity level controlling the dialog icon. */
|
|
44
|
+
kind?: "info" | "warning" | "critical";
|
|
45
|
+
/** Button labels to display (e.g. ["OK", "Cancel"]). */
|
|
46
|
+
buttons?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result from an open-file dialog. */
|
|
50
|
+
export interface OpenFileResult {
|
|
51
|
+
/** Whether the dialog completed successfully. */
|
|
52
|
+
ok: boolean;
|
|
53
|
+
/** True if the user cancelled the dialog. */
|
|
54
|
+
cancelled?: boolean;
|
|
55
|
+
/** Selected file or directory paths. */
|
|
56
|
+
paths?: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Result from a save-file dialog. */
|
|
60
|
+
export interface SaveFileResult {
|
|
61
|
+
/** Whether the dialog completed successfully. */
|
|
62
|
+
ok: boolean;
|
|
63
|
+
/** True if the user cancelled the dialog. */
|
|
64
|
+
cancelled?: boolean;
|
|
65
|
+
/** The chosen save path. */
|
|
66
|
+
path?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Result from a message dialog. */
|
|
70
|
+
export interface MessageResult {
|
|
71
|
+
/** Whether the dialog completed successfully. */
|
|
72
|
+
ok: boolean;
|
|
73
|
+
/** Zero-based index of the button the user clicked. */
|
|
74
|
+
button: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** API for showing native file and message dialogs. */
|
|
78
|
+
export interface DialogAPI {
|
|
79
|
+
/** Show a native open-file dialog. */
|
|
80
|
+
openFile(options?: OpenFileOptions): Promise<OpenFileResult>;
|
|
81
|
+
/** Show a native save-file dialog. */
|
|
82
|
+
saveFile(options?: SaveFileOptions): Promise<SaveFileResult>;
|
|
83
|
+
/** Show a native message dialog (alert/confirm). */
|
|
84
|
+
message(options: MessageOptions): Promise<MessageResult>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let dialogSeq = 0;
|
|
88
|
+
|
|
89
|
+
function assertNotWorker(): void {
|
|
90
|
+
const ctx = (globalThis as unknown as Record<symbol, unknown>)[Symbol.for("zapp.context")];
|
|
91
|
+
if (ctx === "worker") {
|
|
92
|
+
throw new Error("Dialog APIs are not available in a worker context.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function postDialog<T>(action: string, params: Record<string, unknown>): Promise<T> {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const requestId = `dialog-${Date.now()}-${++dialogSeq}`;
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
off();
|
|
101
|
+
reject(new Error("Dialog timed out."));
|
|
102
|
+
}, 300000);
|
|
103
|
+
|
|
104
|
+
// Listen for the result via the bridge event system
|
|
105
|
+
const off = Events.on(`__zapp:dialog:${requestId}`, (payload) => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
const result = { ...(payload as Record<string, unknown>) };
|
|
108
|
+
delete result.requestId;
|
|
109
|
+
resolve(result as T);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Post the request to native
|
|
113
|
+
const handler = (globalThis as unknown as Record<string, Record<string, Record<string, { postMessage?: (m: string) => void }>>>)
|
|
114
|
+
.webkit?.messageHandlers?.zapp;
|
|
115
|
+
const chromeWebview = (globalThis as unknown as Record<string, Record<string, { postMessage?: (m: string) => void }>>)
|
|
116
|
+
.chrome?.webview;
|
|
117
|
+
|
|
118
|
+
const msg = `dialog\n${action}\n${JSON.stringify({ requestId, ...params })}`;
|
|
119
|
+
|
|
120
|
+
if (handler?.postMessage) {
|
|
121
|
+
handler.postMessage(msg);
|
|
122
|
+
} else if (chromeWebview?.postMessage) {
|
|
123
|
+
chromeWebview.postMessage(msg);
|
|
124
|
+
} else {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
off();
|
|
127
|
+
reject(new Error("Dialog bridge unavailable."));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The singleton dialog API instance. Not available in worker contexts. */
|
|
133
|
+
export const Dialog: DialogAPI = {
|
|
134
|
+
openFile(options: OpenFileOptions = {}): Promise<OpenFileResult> {
|
|
135
|
+
assertNotWorker();
|
|
136
|
+
return postDialog<OpenFileResult>("openFile", options as unknown as Record<string, unknown>);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
saveFile(options: SaveFileOptions = {}): Promise<SaveFileResult> {
|
|
140
|
+
assertNotWorker();
|
|
141
|
+
return postDialog<SaveFileResult>("saveFile", options as unknown as Record<string, unknown>);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
message(options: MessageOptions): Promise<MessageResult> {
|
|
145
|
+
assertNotWorker();
|
|
146
|
+
return postDialog<MessageResult>("message", options as unknown as Record<string, unknown>);
|
|
147
|
+
},
|
|
148
|
+
};
|
package/events.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/** Numeric identifiers for window lifecycle and state-change events. */
|
|
2
|
+
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,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Numeric identifiers for application lifecycle events. */
|
|
28
|
+
export enum AppEvent {
|
|
29
|
+
/** The application has started. */
|
|
30
|
+
STARTED = 100,
|
|
31
|
+
/** The application is shutting down. */
|
|
32
|
+
SHUTDOWN = 101,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
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",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const APP_EVENT_NAMES: Record<number, string> = {
|
|
50
|
+
[AppEvent.STARTED]: "app:started",
|
|
51
|
+
[AppEvent.SHUTDOWN]: "app:shutdown",
|
|
52
|
+
};
|
|
53
|
+
|
|
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 };
|
|
104
|
+
}
|
|
105
|
+
|
|
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 };
|
|
112
|
+
}
|
|
113
|
+
|
|
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;
|
|
142
|
+
|
|
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;
|
|
149
|
+
|
|
150
|
+
/** Event name type — known names get autocomplete, arbitrary strings still work */
|
|
151
|
+
export type EventName = KnownEventName | (string & {});
|
|
152
|
+
|
|
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;
|
|
157
|
+
|
|
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;
|
|
180
|
+
}
|
|
181
|
+
|
|
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
|
+
},
|
|
226
|
+
|
|
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
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { App, Window, WindowEvent, Dialog, Menu, Events } from "@zapp/runtime";
|
|
11
|
+
*
|
|
12
|
+
* Window.current().on(WindowEvent.READY, () => {
|
|
13
|
+
* Window.current().show();
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* const result = await Dialog.message({
|
|
17
|
+
* message: "Hello from Zapp!",
|
|
18
|
+
* buttons: ["OK"],
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
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";
|