@zappdev/runtime 0.1.0 → 0.5.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/app.ts +28 -81
- package/bridge.ts +23 -0
- package/context-menu.ts +84 -0
- package/dialog.ts +29 -103
- package/dock.ts +55 -0
- package/events.ts +104 -226
- package/index.ts +17 -25
- package/menu.ts +59 -68
- package/notification.ts +153 -0
- package/package.json +7 -5
- package/services.ts +46 -81
- package/sync.ts +60 -44
- package/window.ts +118 -0
- package/worker-globals.ts +47 -0
- package/worker.ts +104 -248
- package/README.md +0 -51
- package/bindings-contract.ts +0 -31
- package/protocol.ts +0 -94
- package/windows.ts +0 -222
package/notification.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification — native notifications with action buttons.
|
|
3
|
+
* Requires: app bundle + code signing (handled by `zapp dev` and `zapp package`).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { Notification } from "@zappdev/runtime";
|
|
8
|
+
*
|
|
9
|
+
* await Notification.requestPermission();
|
|
10
|
+
* await Notification.registerCategory({
|
|
11
|
+
* id: "message",
|
|
12
|
+
* actions: [{ id: "reply", title: "Reply" }, { id: "delete", title: "Delete", destructive: true }],
|
|
13
|
+
* hasReplyField: true,
|
|
14
|
+
* });
|
|
15
|
+
* await Notification.show({ title: "New Message", body: "Hello!", categoryId: "message" });
|
|
16
|
+
* Notification.on("response", (r) => console.log(r.actionId, r.userText));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getBridge } from "./bridge";
|
|
21
|
+
import { Events } from "./events";
|
|
22
|
+
|
|
23
|
+
export interface NotificationAction {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
destructive?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NotificationCategory {
|
|
30
|
+
id: string;
|
|
31
|
+
actions: NotificationAction[];
|
|
32
|
+
hasReplyField?: boolean;
|
|
33
|
+
replyPlaceholder?: string;
|
|
34
|
+
replyButtonTitle?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NotificationOptions {
|
|
38
|
+
title: string;
|
|
39
|
+
subtitle?: string;
|
|
40
|
+
body?: string;
|
|
41
|
+
/** Sound: "default", "none", or custom sound name. */
|
|
42
|
+
sound?: "default" | "none" | string;
|
|
43
|
+
/** Thread ID for grouping related notifications together. */
|
|
44
|
+
threadId?: string;
|
|
45
|
+
/** Category ID for action buttons (must register category first). */
|
|
46
|
+
categoryId?: string;
|
|
47
|
+
/** Arbitrary user data (returned in response). */
|
|
48
|
+
data?: Record<string, unknown>;
|
|
49
|
+
/** File path or file:// URL for an image/audio/video attachment. */
|
|
50
|
+
attachment?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Explicit notification ID. If provided, can be used to:
|
|
53
|
+
* - Update the notification later with Notification.update()
|
|
54
|
+
* - Remove it with Notification.removeDelivered()
|
|
55
|
+
* If omitted, a UUID is auto-generated.
|
|
56
|
+
*/
|
|
57
|
+
id?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ScheduleOptions extends NotificationOptions {
|
|
61
|
+
trigger: { seconds: number } | { year?: number; month?: number; day?: number; hour?: number; minute?: number };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface NotificationResponse {
|
|
65
|
+
id: string;
|
|
66
|
+
actionId: string; // "DEFAULT" for plain click, or action button ID
|
|
67
|
+
categoryId?: string;
|
|
68
|
+
userText?: string; // if reply field was used
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type PermissionStatus = "granted" | "denied" | "not-determined" | "provisional";
|
|
72
|
+
|
|
73
|
+
export const Notification = {
|
|
74
|
+
async requestPermission(): Promise<PermissionStatus> {
|
|
75
|
+
const result = await getBridge().invoke("__notif:requestPermission") as { status: string };
|
|
76
|
+
return result.status as PermissionStatus;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async getPermissionStatus(): Promise<PermissionStatus> {
|
|
80
|
+
const result = await getBridge().invoke("__notif:getPermission") as { status: string };
|
|
81
|
+
return result.status as PermissionStatus;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async show(options: NotificationOptions): Promise<string> {
|
|
85
|
+
const result = await getBridge().invoke("__notif:show", options as any) as { id: string };
|
|
86
|
+
return result.id;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async schedule(options: ScheduleOptions): Promise<string> {
|
|
90
|
+
const result = await getBridge().invoke("__notif:schedule", options as any) as { id: string };
|
|
91
|
+
return result.id;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async registerCategory(category: NotificationCategory): Promise<void> {
|
|
95
|
+
await getBridge().invoke("__notif:registerCategory", category as any);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async removeCategory(id: string): Promise<void> {
|
|
99
|
+
await getBridge().invoke("__notif:removeCategory", { id } as any);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async cancel(id: string): Promise<void> {
|
|
103
|
+
await getBridge().invoke("__notif:cancel", { id } as any);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async cancelAll(): Promise<void> {
|
|
107
|
+
await getBridge().invoke("__notif:cancelAll");
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/** Remove a specific delivered notification from notification center. */
|
|
111
|
+
async removeDelivered(id: string): Promise<void> {
|
|
112
|
+
await getBridge().invoke("__notif:removeDelivered", { id } as any);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/** Remove all delivered notifications from notification center. */
|
|
116
|
+
async removeAllDelivered(): Promise<void> {
|
|
117
|
+
await getBridge().invoke("__notif:removeAllDelivered");
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update an existing notification's content (replaces by ID).
|
|
122
|
+
* The notification must have been shown with an explicit `id`.
|
|
123
|
+
*/
|
|
124
|
+
async update(id: string, options: Partial<NotificationOptions>): Promise<void> {
|
|
125
|
+
await getBridge().invoke("__notif:update", { id, ...options } as any);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
on(event: "click" | "action" | "response", handler: ((response: NotificationResponse) => void) | ((notificationId: string, actionId?: string) => void)): () => void {
|
|
129
|
+
if (event === "response") {
|
|
130
|
+
// Unified handler — fires for both clicks and actions
|
|
131
|
+
const offClick = Events.on("__notif:click", (payload: any) => {
|
|
132
|
+
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
133
|
+
handler({ id: data.id, actionId: "DEFAULT" } as NotificationResponse);
|
|
134
|
+
});
|
|
135
|
+
const offAction = Events.on("__notif:action", (payload: any) => {
|
|
136
|
+
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
137
|
+
handler({
|
|
138
|
+
id: data.id,
|
|
139
|
+
actionId: data.action,
|
|
140
|
+
userText: data.userText,
|
|
141
|
+
} as NotificationResponse);
|
|
142
|
+
});
|
|
143
|
+
return () => { offClick(); offAction(); };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Legacy click/action handlers
|
|
147
|
+
const eventName = event === "click" ? "__notif:click" : "__notif:action";
|
|
148
|
+
return Events.on(eventName, (payload: any) => {
|
|
149
|
+
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
150
|
+
handler(data.id, data.action);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zappdev/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Frontend runtime API for Zapp desktop apps",
|
|
6
5
|
"main": "index.ts",
|
|
7
6
|
"types": "index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./worker-globals": "./worker-globals.ts"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"*.ts",
|
|
10
|
-
"
|
|
11
|
-
]
|
|
12
|
-
"license": "MIT"
|
|
13
|
+
"!*.test.ts"
|
|
14
|
+
]
|
|
13
15
|
}
|
package/services.ts
CHANGED
|
@@ -1,91 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
] as Bridge | undefined) ?? null;
|
|
17
|
-
|
|
18
|
-
const normalizeError = (error: unknown): ZappServiceInvokeError => {
|
|
19
|
-
if (error && typeof error === "object") {
|
|
20
|
-
const errObj = error as Record<string, unknown>;
|
|
21
|
-
if (typeof errObj.code === "string" && typeof errObj.message === "string") {
|
|
22
|
-
return {
|
|
23
|
-
code: errObj.code as ZappServiceInvokeError["code"],
|
|
24
|
-
message: errObj.message,
|
|
25
|
-
details: errObj.details,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (error instanceof Error) {
|
|
30
|
-
return { code: "INTERNAL_ERROR", message: error.message };
|
|
31
|
-
}
|
|
32
|
-
return { code: "INTERNAL_ERROR", message: "Service invocation failed" };
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const makeRequest = (method: string, args: unknown): ZappServiceInvokeRequest => ({
|
|
36
|
-
v: ZAPP_SERVICE_PROTOCOL_VERSION,
|
|
37
|
-
id: `svc-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
38
|
-
method,
|
|
39
|
-
args,
|
|
40
|
-
meta: {
|
|
41
|
-
sourceCtxId: `ctx-${Math.random().toString(36).slice(2)}`,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/** API for invoking native services defined in the Zapp backend. */
|
|
46
|
-
export interface ServicesAPI {
|
|
47
|
-
/** Call a named service method and return the result. */
|
|
48
|
-
invoke<T = unknown>(method: string, args?: unknown): Promise<T>;
|
|
49
|
-
/** Retrieve the parsed bindings manifest describing available services, or null. */
|
|
50
|
-
getBindingsManifest(): unknown;
|
|
1
|
+
/**
|
|
2
|
+
* Services — call native Zen-C services from JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Services } from "@zappdev/runtime";
|
|
7
|
+
* const result = await Services.invoke("greet", { name: "World" });
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getBridge } from "./bridge";
|
|
12
|
+
|
|
13
|
+
export interface InvokeOptions {
|
|
14
|
+
/** Timeout in milliseconds. Default: 15000. */
|
|
15
|
+
timeout?: number;
|
|
51
16
|
}
|
|
52
17
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error("Service method must be a non-empty string.");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const bridge = getBridge();
|
|
61
|
-
if (!bridge?.invoke) {
|
|
62
|
-
throw new Error("Native invoke bridge is unavailable.");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const req = makeRequest(method, args ?? {});
|
|
66
|
-
let response: ZappServiceInvokeResponse;
|
|
67
|
-
try {
|
|
68
|
-
response = await bridge.invoke(req);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const normalized = normalizeError(error);
|
|
71
|
-
throw new Error(`${normalized.code}: ${normalized.message}`);
|
|
72
|
-
}
|
|
18
|
+
export interface CancellablePromise<T> extends Promise<T> {
|
|
19
|
+
/** Cancel the pending invoke. Rejects with CancelledError. */
|
|
20
|
+
cancel(): void;
|
|
21
|
+
}
|
|
73
22
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
23
|
+
export const Services = {
|
|
24
|
+
/**
|
|
25
|
+
* Invoke a native service by name.
|
|
26
|
+
* In WebView/backend: async bridge call (returns CancellablePromise).
|
|
27
|
+
* In workers: sync host object call (returns resolved Promise).
|
|
28
|
+
*/
|
|
29
|
+
invoke<T = unknown>(method: string, args?: Record<string, unknown>, opts?: InvokeOptions): CancellablePromise<T> {
|
|
30
|
+
// Worker/backend context: use host object for sync invocation
|
|
31
|
+
const hostBridge = (globalThis as any).__zappBridge;
|
|
32
|
+
if (hostBridge?.invokeService) {
|
|
33
|
+
const result = hostBridge.invokeService(method, args) as T;
|
|
34
|
+
const p = Promise.resolve(result) as CancellablePromise<T>;
|
|
35
|
+
p.cancel = () => {};
|
|
36
|
+
// Also expose sync result directly for worker convenience
|
|
37
|
+
(p as any).value = result;
|
|
38
|
+
return p;
|
|
77
39
|
}
|
|
78
40
|
|
|
79
|
-
|
|
41
|
+
// WebView context: async bridge call
|
|
42
|
+
return getBridge().invoke(method, args, opts) as CancellablePromise<T>;
|
|
80
43
|
},
|
|
81
44
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Invoke a native service synchronously (workers/backend only).
|
|
47
|
+
* Throws if called from WebView context.
|
|
48
|
+
*/
|
|
49
|
+
invokeSync<T = unknown>(method: string, args?: Record<string, unknown>): T {
|
|
50
|
+
const hostBridge = (globalThis as any).__zappBridge;
|
|
51
|
+
if (!hostBridge?.invokeService) {
|
|
52
|
+
throw new Error("[zapp] invokeSync is only available in workers and backend contexts");
|
|
89
53
|
}
|
|
54
|
+
return hostBridge.invokeService(method, args) as T;
|
|
90
55
|
},
|
|
91
56
|
};
|
package/sync.ts
CHANGED
|
@@ -1,62 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sync — cross-context wait/notify coordination.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Sync } from "@zappdev/runtime";
|
|
7
|
+
*
|
|
8
|
+
* // In one context (window or worker):
|
|
9
|
+
* const result = await Sync.wait("data-ready", 5000);
|
|
10
|
+
* // result: "notified" | "timed-out"
|
|
11
|
+
*
|
|
12
|
+
* // In another context:
|
|
13
|
+
* Sync.notify("data-ready");
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
export type SyncWaitOptions = {
|
|
13
|
-
/** Timeout in milliseconds, or null to wait indefinitely. */
|
|
14
|
-
timeoutMs?: number | null;
|
|
15
|
-
/** An AbortSignal to cancel the wait. */
|
|
16
|
-
signal?: AbortSignal;
|
|
17
|
-
};
|
|
17
|
+
import { getBridge } from "./bridge";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/** API for cross-context synchronization primitives (wait/notify). */
|
|
25
|
-
export interface SyncAPI {
|
|
26
|
-
/** Block until the given key is notified or the timeout elapses. */
|
|
27
|
-
wait(key: string, timeoutOrOptions?: number | SyncWaitOptions | null): Promise<"notified" | "timed-out">;
|
|
28
|
-
/** Wake up to `count` waiters blocked on the given key. */
|
|
29
|
-
notify(key: string, count?: number): boolean;
|
|
19
|
+
/** Options for {@link Sync.wait}. */
|
|
20
|
+
export interface SyncWaitOptions {
|
|
21
|
+
/** Timeout in milliseconds, or null/undefined to wait indefinitely. */
|
|
22
|
+
timeoutMs?: number | null;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
export const Sync = {
|
|
26
|
+
/**
|
|
27
|
+
* Block until the given key is notified or the timeout elapses.
|
|
28
|
+
* @param key - Sync key (non-empty string)
|
|
29
|
+
* @param timeoutOrOptions - Timeout in ms, options object, or null for indefinite
|
|
30
|
+
* @returns "notified" or "timed-out"
|
|
31
|
+
*/
|
|
34
32
|
async wait(
|
|
35
33
|
key: string,
|
|
36
34
|
timeoutOrOptions: number | SyncWaitOptions | null = 30000
|
|
37
35
|
): Promise<"notified" | "timed-out"> {
|
|
38
|
-
const bridge = getBridge();
|
|
36
|
+
const bridge = getBridge() as any;
|
|
39
37
|
if (!bridge?.syncWait) {
|
|
40
38
|
throw new Error("Sync bridge is unavailable.");
|
|
41
39
|
}
|
|
42
40
|
if (typeof key !== "string" || key.trim().length === 0) {
|
|
43
41
|
throw new Error("Sync key must be a non-empty string.");
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
|
|
44
|
+
const timeoutMs =
|
|
45
|
+
typeof timeoutOrOptions === "number"
|
|
46
|
+
? timeoutOrOptions
|
|
47
|
+
: timeoutOrOptions?.timeoutMs ?? null;
|
|
48
|
+
|
|
49
|
+
return await bridge.syncWait(key.trim(), timeoutMs);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wake up to `count` waiters blocked on the given key.
|
|
54
|
+
*
|
|
55
|
+
* Defaults to **1** — same semantics as `pthread_cond_signal` /
|
|
56
|
+
* `Object.notify()`. Use {@link Sync.notifyAll} to wake every current waiter.
|
|
57
|
+
*
|
|
58
|
+
* @param key - Sync key
|
|
59
|
+
* @param count - Number of waiters to wake (default 1)
|
|
60
|
+
*/
|
|
61
|
+
notify(key: string, count = 1): void {
|
|
62
|
+
const bridge = getBridge() as any;
|
|
63
|
+
if (!bridge?.syncNotify) return;
|
|
64
|
+
if (typeof key !== "string" || key.trim().length === 0) return;
|
|
65
|
+
bridge.syncNotify(key.trim(), Math.max(1, Math.min(count, 65535)));
|
|
54
66
|
},
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Wake every waiter currently blocked on the given key — broadcast.
|
|
70
|
+
* Equivalent to `pthread_cond_broadcast` / `Object.notifyAll()`.
|
|
71
|
+
*/
|
|
72
|
+
notifyAll(key: string): void {
|
|
73
|
+
const bridge = getBridge() as any;
|
|
74
|
+
if (!bridge?.syncNotify) return;
|
|
75
|
+
if (typeof key !== "string" || key.trim().length === 0) return;
|
|
76
|
+
bridge.syncNotify(key.trim(), 65535);
|
|
61
77
|
},
|
|
62
78
|
};
|
package/window.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window — per-window handle with scoped event listening.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Window, WindowEvent } from "@zappdev/runtime";
|
|
7
|
+
*
|
|
8
|
+
* const win = Window.current();
|
|
9
|
+
* win.on(WindowEvent.READY, () => win.show());
|
|
10
|
+
* win.on(WindowEvent.RESIZE, (payload) => console.log(payload.size));
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getBridge } from "./bridge";
|
|
15
|
+
import { WindowEvent, eventName, type WindowSizePayload, type WindowPayload } from "./events";
|
|
16
|
+
|
|
17
|
+
/** Options for creating a window (mirrors native WindowOptions). */
|
|
18
|
+
export interface WindowOptions {
|
|
19
|
+
title?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
x?: number;
|
|
24
|
+
y?: number;
|
|
25
|
+
visible?: boolean;
|
|
26
|
+
resizable?: boolean;
|
|
27
|
+
closable?: boolean;
|
|
28
|
+
minimizable?: boolean;
|
|
29
|
+
maximizable?: boolean;
|
|
30
|
+
fullscreen?: boolean;
|
|
31
|
+
borderless?: boolean;
|
|
32
|
+
transparent?: boolean;
|
|
33
|
+
alwaysOnTop?: boolean;
|
|
34
|
+
titleBarStyle?: "default" | "hidden" | "hiddenInset";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Size events that include width/height/position data. */
|
|
38
|
+
type SizeEvent = WindowEvent.RESIZE | WindowEvent.MOVE | WindowEvent.MAXIMIZE | WindowEvent.RESTORE;
|
|
39
|
+
|
|
40
|
+
/** A handle to a specific window. */
|
|
41
|
+
export interface WindowHandle {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
|
|
44
|
+
on(event: SizeEvent, handler: (payload: WindowSizePayload) => void): () => void;
|
|
45
|
+
on(event: WindowEvent, handler: (payload: WindowPayload) => void): () => void;
|
|
46
|
+
|
|
47
|
+
show(): void;
|
|
48
|
+
hide(): void;
|
|
49
|
+
close(): void;
|
|
50
|
+
setTitle(title: string): void;
|
|
51
|
+
setSize(width: number, height: number): void;
|
|
52
|
+
setPosition(x: number, y: number): void;
|
|
53
|
+
minimize(): void;
|
|
54
|
+
maximize(): void;
|
|
55
|
+
setFullscreen(on: boolean): void;
|
|
56
|
+
setAlwaysOnTop(on: boolean): void;
|
|
57
|
+
setCloseGuard(enabled: boolean): void;
|
|
58
|
+
loadUrl(url: string): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Send a window action to native. Uses message type 4 (WINDOW_ACTION). */
|
|
62
|
+
function windowAction(action: string, args: Record<string, unknown> = {}): void {
|
|
63
|
+
const bridge = getBridge();
|
|
64
|
+
// Post raw message with t:4 for window actions (fire-and-forget)
|
|
65
|
+
const msg = JSON.stringify({ t: 4, m: action, a: args });
|
|
66
|
+
(bridge as any).post ? (bridge as any).post(msg) : bridge.emit("__window_action:" + action, args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createWindowHandle(windowId: string): WindowHandle {
|
|
70
|
+
const bridge = getBridge();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: windowId,
|
|
74
|
+
|
|
75
|
+
on(event: WindowEvent, handler: (payload: any) => void): () => void {
|
|
76
|
+
const name = eventName(event);
|
|
77
|
+
return bridge.on(name, (payload: any) => {
|
|
78
|
+
if (payload?.windowId === windowId) {
|
|
79
|
+
handler(payload);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
show() { windowAction("show", { windowId }); },
|
|
85
|
+
hide() { windowAction("hide", { windowId }); },
|
|
86
|
+
close() { windowAction("close", { windowId }); },
|
|
87
|
+
setTitle(title: string) { windowAction("setTitle", { windowId, title }); },
|
|
88
|
+
setSize(width: number, h: number) { windowAction("setSize", { windowId, width, height: h }); },
|
|
89
|
+
setPosition(x: number, y: number) { windowAction("setPosition", { windowId, x, y }); },
|
|
90
|
+
minimize() { windowAction("minimize", { windowId }); },
|
|
91
|
+
maximize() { windowAction("maximize", { windowId }); },
|
|
92
|
+
setFullscreen(on: boolean) { windowAction("setFullscreen", { windowId, on }); },
|
|
93
|
+
setAlwaysOnTop(on: boolean) { windowAction("setAlwaysOnTop", { windowId, on }); },
|
|
94
|
+
setCloseGuard(on: boolean) { windowAction("setCloseGuard", { windowId, on }); },
|
|
95
|
+
loadUrl(url: string) { windowAction("loadUrl", { windowId, url }); },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getCurrentWindowId(): string | null {
|
|
100
|
+
return (globalThis as any)[Symbol.for("zapp.windowId")] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const Window = {
|
|
104
|
+
/** Get the current window handle. Only available in WebView context. */
|
|
105
|
+
current(): WindowHandle {
|
|
106
|
+
const id = getCurrentWindowId();
|
|
107
|
+
if (!id) {
|
|
108
|
+
throw new Error("[zapp] Window.current() is only available in WebView context. Use Window.create() in backend/workers.");
|
|
109
|
+
}
|
|
110
|
+
return createWindowHandle(id);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** Create a new window. Returns a handle for the new window. */
|
|
114
|
+
async create(opts?: Partial<WindowOptions>): Promise<WindowHandle> {
|
|
115
|
+
const result = await getBridge().invoke("__window:create", opts ?? {}) as { windowId: string };
|
|
116
|
+
return createWindowHandle(result.windowId);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker globals — typed declarations for Zapp Worker contexts.
|
|
3
|
+
* Import this in worker scripts for TypeScript support.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* /// <reference path="@zappdev/runtime/worker-globals" />
|
|
8
|
+
* // or
|
|
9
|
+
* import "@zappdev/runtime/worker-globals";
|
|
10
|
+
*
|
|
11
|
+
* send("channel", { data: 123 });
|
|
12
|
+
* receive("result", (data) => console.log(data));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export {};
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
/** Send a message on a named channel to the owning WebView. */
|
|
20
|
+
function send(channel: string, data: unknown): void;
|
|
21
|
+
|
|
22
|
+
/** Listen for messages on a named channel. Returns unsubscribe function. */
|
|
23
|
+
function receive(channel: string, handler: (data: unknown) => void): () => void;
|
|
24
|
+
|
|
25
|
+
/** Post a raw message to the owning WebView. */
|
|
26
|
+
function postMessage(data: unknown): void;
|
|
27
|
+
|
|
28
|
+
/** Raw message handler — receives all messages from the WebView. */
|
|
29
|
+
var onmessage: ((event: { data: unknown }) => void) | null;
|
|
30
|
+
|
|
31
|
+
/** The native bridge — host objects for direct C calls. */
|
|
32
|
+
var __zappBridge: {
|
|
33
|
+
/** Invoke a native service synchronously. Returns the result directly. */
|
|
34
|
+
invokeService(method: string, args?: unknown): unknown;
|
|
35
|
+
|
|
36
|
+
/** Post data back to the owning WebView. */
|
|
37
|
+
postToWebview(data: unknown): void;
|
|
38
|
+
|
|
39
|
+
/** Sync wait — register a waiter on a key. */
|
|
40
|
+
syncWait(key: string, timeoutMs?: number): void;
|
|
41
|
+
|
|
42
|
+
/** Sync notify — wake waiters on a key. */
|
|
43
|
+
syncNotify(key: string, count?: number): void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
var self: typeof globalThis;
|
|
47
|
+
}
|