plusui-native-core 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.
Files changed (70) hide show
  1. package/Core/CMakeLists.txt +34 -0
  2. package/Core/README.md +29 -0
  3. package/Core/build/win32/x64/ALL_BUILD.vcxproj +185 -0
  4. package/Core/build/win32/x64/ALL_BUILD.vcxproj.filters +8 -0
  5. package/Core/build/win32/x64/CMakeCache.txt +335 -0
  6. package/Core/build/win32/x64/CMakeFiles/11f7f2f432927ec8d1861dc42d4bd679/INSTALL_force.rule +1 -0
  7. package/Core/build/win32/x64/CMakeFiles/11f7f2f432927ec8d1861dc42d4bd679/generate.stamp.rule +1 -0
  8. package/Core/build/win32/x64/CMakeFiles/4.2.3/CMakeCXXCompiler.cmake +104 -0
  9. package/Core/build/win32/x64/CMakeFiles/4.2.3/CMakeDetermineCompilerABI_CXX.bin +0 -0
  10. package/Core/build/win32/x64/CMakeFiles/4.2.3/CMakeRCCompiler.cmake +6 -0
  11. package/Core/build/win32/x64/CMakeFiles/4.2.3/CMakeSystem.cmake +15 -0
  12. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/CMakeCXXCompilerId.cpp +949 -0
  13. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/CompilerIdCXX.exe +0 -0
  14. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/CompilerIdCXX.vcxproj +72 -0
  15. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CMakeCXXCompilerId.obj +0 -0
  16. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.exe.recipe +11 -0
  17. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.command.1.tlog +2 -0
  18. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.read.1.tlog +4 -0
  19. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.write.1.tlog +2 -0
  20. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/Cl.items.tlog +1 -0
  21. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CompilerIdCXX.lastbuildstate +2 -0
  22. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.command.1.tlog +2 -0
  23. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.read.1.tlog +22 -0
  24. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.secondary.1.tlog +1 -0
  25. package/Core/build/win32/x64/CMakeFiles/4.2.3/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.write.1.tlog +2 -0
  26. package/Core/build/win32/x64/CMakeFiles/4.2.3/VCTargetsPath/x64/Debug/VCTargetsPath.recipe +11 -0
  27. package/Core/build/win32/x64/CMakeFiles/4.2.3/VCTargetsPath/x64/Debug/VCTargetsPath.tlog/VCTargetsPath.lastbuildstate +2 -0
  28. package/Core/build/win32/x64/CMakeFiles/4.2.3/VCTargetsPath.txt +1 -0
  29. package/Core/build/win32/x64/CMakeFiles/4.2.3/VCTargetsPath.vcxproj +31 -0
  30. package/Core/build/win32/x64/CMakeFiles/CMakeConfigureLog.yaml +2698 -0
  31. package/Core/build/win32/x64/CMakeFiles/InstallScripts.json +7 -0
  32. package/Core/build/win32/x64/CMakeFiles/TargetDirectories.txt +4 -0
  33. package/Core/build/win32/x64/CMakeFiles/cmake.check_cache +1 -0
  34. package/Core/build/win32/x64/CMakeFiles/generate.stamp +1 -0
  35. package/Core/build/win32/x64/CMakeFiles/generate.stamp.depend +34 -0
  36. package/Core/build/win32/x64/CMakeFiles/generate.stamp.list +1 -0
  37. package/Core/build/win32/x64/Capsule.sln +67 -0
  38. package/Core/build/win32/x64/INSTALL.vcxproj +209 -0
  39. package/Core/build/win32/x64/INSTALL.vcxproj.filters +13 -0
  40. package/Core/build/win32/x64/ZERO_CHECK.vcxproj +179 -0
  41. package/Core/build/win32/x64/ZERO_CHECK.vcxproj.filters +13 -0
  42. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/CL.command.1.tlog +2 -0
  43. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/CustomBuild.command.1.tlog +10 -0
  44. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/CustomBuild.read.1.tlog +33 -0
  45. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/CustomBuild.write.1.tlog +2 -0
  46. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/capsule.lastbuildstate +2 -0
  47. package/Core/build/win32/x64/capsule.dir/Release/capsule.tlog/unsuccessfulbuild +0 -0
  48. package/Core/build/win32/x64/capsule.vcxproj +332 -0
  49. package/Core/build/win32/x64/capsule.vcxproj.filters +25 -0
  50. package/Core/build/win32/x64/cmake_install.cmake +72 -0
  51. package/Core/build/win32/x64/x64/Release/ZERO_CHECK/ZERO_CHECK.recipe +11 -0
  52. package/Core/build/win32/x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.command.1.tlog +10 -0
  53. package/Core/build/win32/x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.read.1.tlog +34 -0
  54. package/Core/build/win32/x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.write.1.tlog +2 -0
  55. package/Core/build/win32/x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/ZERO_CHECK.lastbuildstate +2 -0
  56. package/Core/include/app.hpp +121 -0
  57. package/Core/include/display.hpp +90 -0
  58. package/Core/include/keyboard.hpp +135 -0
  59. package/Core/include/menu.hpp +79 -0
  60. package/Core/include/tray.hpp +81 -0
  61. package/Core/include/window.hpp +106 -0
  62. package/Core/src/app.cpp +311 -0
  63. package/Core/src/display.cpp +424 -0
  64. package/Core/src/tray.cpp +275 -0
  65. package/Core/src/window.cpp +528 -0
  66. package/Core/vendor/webview.h +551 -0
  67. package/dist/index.d.ts +205 -0
  68. package/dist/index.js +198 -0
  69. package/package.json +19 -0
  70. package/src/index.ts +574 -0
package/src/index.ts ADDED
@@ -0,0 +1,574 @@
1
+ // PlusUI Core SDK
2
+ // Matches Core/Features structure - each feature has backend (C++) and frontend (TS)
3
+
4
+ type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
5
+ type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
6
+
7
+ let _invoke: InvokeFn | null = null;
8
+ let _pending: PendingMap = {};
9
+
10
+ function initBridge() {
11
+ if (typeof window === 'undefined') return;
12
+
13
+ const w = window as any;
14
+ if (w.__invoke__) {
15
+ _invoke = w.__invoke__;
16
+ return;
17
+ }
18
+
19
+ _pending = {};
20
+ w.__pending__ = _pending;
21
+
22
+ w.__invoke__ = (method: string, args?: unknown[]): Promise<unknown> => {
23
+ return new Promise((resolve, reject) => {
24
+ const id = Math.random().toString(36).substr(2, 9);
25
+ const request = JSON.stringify({ id, method, params: args || [] });
26
+
27
+ _pending[id] = { resolve, reject };
28
+
29
+ if (typeof w.__native_invoke__ === 'function') {
30
+ w.__native_invoke__(request);
31
+ } else {
32
+ console.warn(`[PlusUI] Native binding not available for: ${method}`);
33
+ setTimeout(() => {
34
+ delete _pending[id];
35
+ resolve(null);
36
+ }, 0);
37
+ }
38
+
39
+ setTimeout(() => {
40
+ if (_pending[id]) {
41
+ delete _pending[id];
42
+ reject(new Error(`${method} timed out`));
43
+ }
44
+ }, 30000);
45
+ });
46
+ };
47
+
48
+ w.__response__ = (id: string, result: unknown) => {
49
+ const pending = _pending[id];
50
+ if (pending) {
51
+ pending.resolve(result);
52
+ delete _pending[id];
53
+ }
54
+ };
55
+
56
+ _invoke = w.__invoke__;
57
+ }
58
+
59
+ initBridge();
60
+
61
+ async function invoke(method: string, args?: unknown[]): Promise<unknown> {
62
+ if (!_invoke) {
63
+ initBridge();
64
+ if (!_invoke) {
65
+ throw new Error('Bridge not initialized');
66
+ }
67
+ }
68
+ return _invoke(method, args);
69
+ }
70
+
71
+ // ============================================================================
72
+ // Types - Match Core/Features/*/include/*.hpp
73
+ // ============================================================================
74
+
75
+ export interface WindowSize { width: number; height: number; }
76
+ export interface WindowPosition { x: number; y: number; }
77
+ export interface WindowRect { x: number; y: number; width: number; height: number; }
78
+ export type WindowId = string;
79
+
80
+ // ============================================================================
81
+ // Window API - Core/Features/Window
82
+ // ============================================================================
83
+
84
+ export const win = {
85
+ minimize: async (id?: WindowId) => await invoke('window.minimize', id ? [id] : []),
86
+ maximize: async (id?: WindowId) => await invoke('window.maximize', id ? [id] : []),
87
+ restore: async (id?: WindowId) => await invoke('window.restore', id ? [id] : []),
88
+ close: async (id?: WindowId) => await invoke('window.close', id ? [id] : []),
89
+ show: async (id?: WindowId) => await invoke('window.show', id ? [id] : []),
90
+ hide: async (id?: WindowId) => await invoke('window.hide', id ? [id] : []),
91
+ center: async (id?: WindowId) => await invoke('window.center', id ? [id] : []),
92
+
93
+ setSize: async (w: number, h: number, id?: WindowId) => await invoke('window.setSize', id ? [w, h, id] : [w, h]),
94
+ setPosition: async (x: number, y: number, id?: WindowId) => await invoke('window.setPosition', id ? [x, y, id] : [x, y]),
95
+ getSize: async (id?: WindowId): Promise<WindowSize> => await invoke('window.getSize', id ? [id] : []) as Promise<WindowSize>,
96
+ getPosition: async (id?: WindowId): Promise<WindowPosition> => await invoke('window.getPosition', id ? [id] : []) as Promise<WindowPosition>,
97
+
98
+ setTitle: async (title: string, id?: WindowId) => await invoke('window.setTitle', id ? [title, id] : [title]),
99
+ setFullscreen: async (enabled: boolean, id?: WindowId) => await invoke('window.setFullscreen', id ? [enabled, id] : [enabled]),
100
+ setAlwaysOnTop: async (enabled: boolean, id?: WindowId) => await invoke('window.setAlwaysOnTop', id ? [enabled, id] : [enabled]),
101
+ setResizable: async (enabled: boolean, id?: WindowId) => await invoke('window.setResizable', id ? [enabled, id] : [enabled]),
102
+ setDecorations: async (enabled: boolean, id?: WindowId) => await invoke('window.setDecorations', id ? [enabled, id] : [enabled]),
103
+ setTransparency: async (alpha: number, id?: WindowId) => await invoke('window.setTransparency', id ? [alpha, id] : [alpha]),
104
+
105
+ isMaximized: async (id?: WindowId): Promise<boolean> => await invoke('window.isMaximized', id ? [id] : []) as Promise<boolean>,
106
+ isMinimized: async (id?: WindowId): Promise<boolean> => await invoke('window.isMinimized', id ? [id] : []) as Promise<boolean>,
107
+ isVisible: async (id?: WindowId): Promise<boolean> => await invoke('window.isVisible', id ? [id] : []) as Promise<boolean>,
108
+ };
109
+
110
+ // ============================================================================
111
+ // Tray API - Core/Features/Tray
112
+ // ============================================================================
113
+
114
+ export const tray = {
115
+ setIcon: async (iconPath: string) => await invoke('tray.setIcon', [iconPath]),
116
+ setTooltip: async (tooltip: string) => await invoke('tray.setTooltip', [tooltip]),
117
+ setVisible: async (visible: boolean) => await invoke('tray.setVisible', [visible]),
118
+ };
119
+
120
+ // ============================================================================
121
+ // App API - Core/Features/App
122
+ // ============================================================================
123
+
124
+ export const app = {
125
+ quit: async () => await invoke('app.quit', []),
126
+ invoke: async (method: string, args?: unknown[]) => await invoke(method, args),
127
+ };
128
+
129
+ // ============================================================================
130
+ // Display API - Core/Features/Display
131
+ // ============================================================================
132
+
133
+ export interface Display {
134
+ id: number;
135
+ name: string;
136
+ x: number;
137
+ y: number;
138
+ width: number;
139
+ height: number;
140
+ resolution: {
141
+ width: number;
142
+ height: number;
143
+ };
144
+ scale: number;
145
+ isPrimary: boolean;
146
+ }
147
+
148
+ export const display = {
149
+ getAll: async (): Promise<Display[]> => await invoke('display.getAll', []) as Promise<Display[]>,
150
+ getPrimary: async (): Promise<Display> => await invoke('display.getPrimary', []) as Promise<Display>,
151
+ getCurrent: async (): Promise<Display> => await invoke('display.getCurrent', []) as Promise<Display>,
152
+ };
153
+
154
+ // ============================================================================
155
+ // Clipboard API - Core/Features/Clipboard
156
+ // ============================================================================
157
+
158
+ export const clipboard = {
159
+ writeText: async (text: string) => await invoke('clipboard.writeText', [text]),
160
+ readText: async (): Promise<string> => await invoke('clipboard.readText', []) as Promise<string>,
161
+ writeImage: async (base64: string) => await invoke('clipboard.writeImage', [base64]),
162
+ readImage: async (): Promise<string> => await invoke('clipboard.readImage', []) as Promise<string>,
163
+ clear: async () => await invoke('clipboard.clear', []),
164
+ };
165
+
166
+ // ============================================================================
167
+ // WebGPU API - Core/Features/WebGPU (Optional - requires PLUSUI_ENABLE_WEBGPU)
168
+ // ============================================================================
169
+
170
+ export interface GPUAdapter {
171
+ requestDevice(descriptor?: any): Promise<GPUDevice>;
172
+ features: Set<string>;
173
+ limits: Record<string, number>;
174
+ info?: {
175
+ vendor?: string;
176
+ architecture?: string;
177
+ device?: string;
178
+ };
179
+ }
180
+
181
+ export interface GPUDevice {
182
+ createBuffer(descriptor: any): any;
183
+ createTexture(descriptor: any): any;
184
+ createShaderModule(descriptor: any): any;
185
+ createRenderPipeline(descriptor: any): any;
186
+ createCommandEncoder(): any;
187
+ queue: {
188
+ submit(commandBuffers: any[]): void;
189
+ writeBuffer(buffer: any, offset: number, data: ArrayBuffer): void;
190
+ };
191
+ destroy(): void;
192
+ }
193
+
194
+ export interface GPURequestAdapterOptions {
195
+ powerPreference?: 'low-power' | 'high-performance';
196
+ forceFallbackAdapter?: boolean;
197
+ }
198
+
199
+ export const webgpu = {
200
+ requestAdapter: async (options?: GPURequestAdapterOptions): Promise<GPUAdapter | null> => {
201
+ const result = await invoke('webgpu.requestAdapter', [options || {}]);
202
+ return result as GPUAdapter | null;
203
+ },
204
+
205
+ getPreferredCanvasFormat: (): string => 'bgra8unorm',
206
+ };
207
+
208
+ // Initialize navigator.gpu polyfill if not already present
209
+ if (typeof window !== 'undefined' && typeof (window as any).navigator !== 'undefined') {
210
+ const nav = (window as any).navigator;
211
+ if (!nav.gpu) {
212
+ nav.gpu = {
213
+ requestAdapter: webgpu.requestAdapter,
214
+ getPreferredCanvasFormat: webgpu.getPreferredCanvasFormat,
215
+ };
216
+ }
217
+ }
218
+
219
+ // ============================================================================
220
+ // Keyboard API - Core/Features/Keyboard
221
+ // ============================================================================
222
+
223
+ export enum KeyCode {
224
+ Unknown = 0,
225
+ Space = 32,
226
+ Escape = 256,
227
+ Enter = 257,
228
+ Tab = 258,
229
+ Backspace = 259,
230
+ Delete = 261,
231
+ Right = 262,
232
+ Left = 263,
233
+ Down = 264,
234
+ Up = 265,
235
+ F1 = 290, F2 = 291, F3 = 292, F4 = 293, F5 = 294, F6 = 295,
236
+ F7 = 296, F8 = 297, F9 = 298, F10 = 299, F11 = 300, F12 = 301,
237
+ LeftShift = 340,
238
+ LeftControl = 341,
239
+ LeftAlt = 342,
240
+ }
241
+
242
+ export enum KeyMod {
243
+ None = 0,
244
+ Shift = 1,
245
+ Control = 2,
246
+ Alt = 4,
247
+ Super = 8,
248
+ }
249
+
250
+ export interface KeyEvent {
251
+ key: KeyCode;
252
+ scancode: number;
253
+ mods: KeyMod;
254
+ pressed: boolean;
255
+ repeat: boolean;
256
+ keyName: string;
257
+ }
258
+
259
+ export interface Shortcut {
260
+ key: KeyCode;
261
+ mods: KeyMod;
262
+ }
263
+
264
+ type KeyEventCallback = (event: KeyEvent) => void;
265
+ type ShortcutCallback = () => void;
266
+
267
+ const _keyDownCallbacks: KeyEventCallback[] = [];
268
+ const _keyUpCallbacks: KeyEventCallback[] = [];
269
+ const _shortcutHandlers: Map<string, ShortcutCallback> = new Map();
270
+
271
+ // Listen for keyboard events from backend
272
+ if (typeof window !== 'undefined') {
273
+ (window as any).__onKeyDown__ = (event: KeyEvent) => {
274
+ _keyDownCallbacks.forEach(cb => cb(event));
275
+ };
276
+ (window as any).__onKeyUp__ = (event: KeyEvent) => {
277
+ _keyUpCallbacks.forEach(cb => cb(event));
278
+ };
279
+ (window as any).__onShortcut__ = (id: string) => {
280
+ const handler = _shortcutHandlers.get(id);
281
+ if (handler) handler();
282
+ };
283
+ }
284
+
285
+ export const keyboard = {
286
+ isKeyPressed: async (key: KeyCode): Promise<boolean> =>
287
+ await invoke('keyboard.isKeyPressed', [key]) as Promise<boolean>,
288
+
289
+ setAutoRepeat: async (enabled: boolean) =>
290
+ await invoke('keyboard.setAutoRepeat', [enabled]),
291
+
292
+ getAutoRepeat: async (): Promise<boolean> =>
293
+ await invoke('keyboard.getAutoRepeat', []) as Promise<boolean>,
294
+
295
+ onKeyDown: (callback: KeyEventCallback) => {
296
+ _keyDownCallbacks.push(callback);
297
+ return () => {
298
+ const idx = _keyDownCallbacks.indexOf(callback);
299
+ if (idx > -1) _keyDownCallbacks.splice(idx, 1);
300
+ };
301
+ },
302
+
303
+ onKeyUp: (callback: KeyEventCallback) => {
304
+ _keyUpCallbacks.push(callback);
305
+ return () => {
306
+ const idx = _keyUpCallbacks.indexOf(callback);
307
+ if (idx > -1) _keyUpCallbacks.splice(idx, 1);
308
+ };
309
+ },
310
+
311
+ registerShortcut: async (id: string, shortcut: Shortcut | string, callback: ShortcutCallback): Promise<boolean> => {
312
+ _shortcutHandlers.set(id, callback);
313
+ const sc = typeof shortcut === 'string' ? keyboard.parseShortcut(shortcut) : shortcut;
314
+ return await invoke('keyboard.registerShortcut', [id, sc]) as Promise<boolean>;
315
+ },
316
+
317
+ unregisterShortcut: async (id: string): Promise<boolean> => {
318
+ _shortcutHandlers.delete(id);
319
+ return await invoke('keyboard.unregisterShortcut', [id]) as Promise<boolean>;
320
+ },
321
+
322
+ clearShortcuts: async () => {
323
+ _shortcutHandlers.clear();
324
+ await invoke('keyboard.clearShortcuts', []);
325
+ },
326
+
327
+ parseShortcut: (str: string): Shortcut => {
328
+ const parts = str.toLowerCase().split('+');
329
+ let mods = KeyMod.None;
330
+ let key = KeyCode.Unknown;
331
+ const keyMap: Record<string, KeyCode> = {
332
+ 'space': KeyCode.Space, 'escape': KeyCode.Escape, 'enter': KeyCode.Enter,
333
+ 'tab': KeyCode.Tab, 'backspace': KeyCode.Backspace, 'delete': KeyCode.Delete,
334
+ 'right': KeyCode.Right, 'left': KeyCode.Left, 'down': KeyCode.Down, 'up': KeyCode.Up,
335
+ 'f1': KeyCode.F1, 'f2': KeyCode.F2, 'f3': KeyCode.F3, 'f4': KeyCode.F4,
336
+ 'f5': KeyCode.F5, 'f6': KeyCode.F6, 'f7': KeyCode.F7, 'f8': KeyCode.F8,
337
+ 'f9': KeyCode.F9, 'f10': KeyCode.F10, 'f11': KeyCode.F11, 'f12': KeyCode.F12,
338
+ };
339
+ for (const part of parts) {
340
+ const t = part.trim();
341
+ if (t === 'ctrl' || t === 'control') mods |= KeyMod.Control;
342
+ else if (t === 'alt') mods |= KeyMod.Alt;
343
+ else if (t === 'shift') mods |= KeyMod.Shift;
344
+ else if (t === 'super' || t === 'win' || t === 'cmd') mods |= KeyMod.Super;
345
+ else key = keyMap[t] || KeyCode.Unknown;
346
+ }
347
+ return { key, mods };
348
+ },
349
+ };
350
+
351
+ // ============================================================================
352
+ // Menu API - Core/Features/Menu
353
+ // ============================================================================
354
+
355
+ export type MenuItemType = 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio';
356
+
357
+ export interface MenuItem {
358
+ id: string;
359
+ label: string;
360
+ accelerator?: string;
361
+ icon?: string;
362
+ type?: MenuItemType;
363
+ enabled?: boolean;
364
+ checked?: boolean;
365
+ submenu?: MenuItem[];
366
+ click?: (menuItem: MenuItem) => void;
367
+ data?: Record<string, unknown>;
368
+ }
369
+
370
+ const _menuClickHandlers: Map<string, (item: MenuItem) => void> = new Map();
371
+
372
+ // Listen for menu click events from backend
373
+ if (typeof window !== 'undefined') {
374
+ (window as any).__onMenuItemClick__ = (id: string) => {
375
+ const handler = _menuClickHandlers.get(id);
376
+ if (handler) handler({ id, label: '' });
377
+ };
378
+ }
379
+
380
+ function registerMenuClickHandlers(items: MenuItem[]): void {
381
+ for (const item of items) {
382
+ if (item.click) _menuClickHandlers.set(item.id, item.click);
383
+ if (item.submenu) registerMenuClickHandlers(item.submenu);
384
+ }
385
+ }
386
+
387
+ function stripMenuFunctions(items: MenuItem[]): unknown[] {
388
+ return items.map(item => {
389
+ const { click, submenu, ...rest } = item;
390
+ const clean: Record<string, unknown> = { ...rest };
391
+ if (submenu) clean.submenu = stripMenuFunctions(submenu);
392
+ return clean;
393
+ });
394
+ }
395
+
396
+ export const menu = {
397
+ create: async (items: MenuItem[]): Promise<string> => {
398
+ registerMenuClickHandlers(items);
399
+ return await invoke('menu.create', [stripMenuFunctions(items)]) as Promise<string>;
400
+ },
401
+
402
+ popup: async (menuId: string, x?: number, y?: number) =>
403
+ await invoke('menu.popup', [menuId, x ?? 0, y ?? 0]),
404
+
405
+ popupAtCursor: async (menuId: string) =>
406
+ await invoke('menu.popupAtCursor', [menuId]),
407
+
408
+ close: async (menuId: string) =>
409
+ await invoke('menu.close', [menuId]),
410
+
411
+ destroy: async (menuId: string) =>
412
+ await invoke('menu.destroy', [menuId]),
413
+
414
+ setApplicationMenu: async (items: MenuItem[]) => {
415
+ registerMenuClickHandlers(items);
416
+ await invoke('menu.setApplicationMenu', [stripMenuFunctions(items)]);
417
+ },
418
+
419
+ getApplicationMenu: async (): Promise<MenuItem[]> =>
420
+ await invoke('menu.getApplicationMenu', []) as Promise<MenuItem[]>,
421
+
422
+ showContextMenu: async (items: MenuItem[], x?: number, y?: number) => {
423
+ registerMenuClickHandlers(items);
424
+ const menuId = await invoke('menu.create', [stripMenuFunctions(items)]) as string;
425
+ await invoke('menu.popup', [menuId, x ?? 0, y ?? 0]);
426
+ },
427
+
428
+ onMenuItemClick: (callback: (id: string) => void) => {
429
+ if (typeof window !== 'undefined') {
430
+ const handler = (window as any).__onMenuItemClick__;
431
+ (window as any).__onMenuItemClick__ = (id: string) => {
432
+ handler?.(id);
433
+ callback(id);
434
+ };
435
+ }
436
+ },
437
+
438
+ // Predefined menu templates
439
+ templates: {
440
+ editMenu: (handlers?: Partial<{ undo: () => void; redo: () => void; cut: () => void; copy: () => void; paste: () => void; selectAll: () => void }>): MenuItem => ({
441
+ id: 'edit', label: '&Edit', submenu: [
442
+ { id: 'undo', label: 'Undo', accelerator: 'Ctrl+Z', click: handlers?.undo },
443
+ { id: 'redo', label: 'Redo', accelerator: 'Ctrl+Y', click: handlers?.redo },
444
+ { id: 'sep1', label: '', type: 'separator' },
445
+ { id: 'cut', label: 'Cut', accelerator: 'Ctrl+X', click: handlers?.cut },
446
+ { id: 'copy', label: 'Copy', accelerator: 'Ctrl+C', click: handlers?.copy },
447
+ { id: 'paste', label: 'Paste', accelerator: 'Ctrl+V', click: handlers?.paste },
448
+ { id: 'sep2', label: '', type: 'separator' },
449
+ { id: 'selectAll', label: 'Select All', accelerator: 'Ctrl+A', click: handlers?.selectAll },
450
+ ]
451
+ }),
452
+ fileMenu: (handlers?: Partial<{ new: () => void; open: () => void; save: () => void; saveAs: () => void; exit: () => void }>): MenuItem => ({
453
+ id: 'file', label: '&File', submenu: [
454
+ { id: 'new', label: 'New', accelerator: 'Ctrl+N', click: handlers?.new },
455
+ { id: 'open', label: 'Open...', accelerator: 'Ctrl+O', click: handlers?.open },
456
+ { id: 'sep1', label: '', type: 'separator' },
457
+ { id: 'save', label: 'Save', accelerator: 'Ctrl+S', click: handlers?.save },
458
+ { id: 'saveAs', label: 'Save As...', accelerator: 'Ctrl+Shift+S', click: handlers?.saveAs },
459
+ { id: 'sep2', label: '', type: 'separator' },
460
+ { id: 'exit', label: 'Exit', accelerator: 'Alt+F4', click: handlers?.exit },
461
+ ]
462
+ }),
463
+ viewMenu: (handlers?: Partial<{ zoomIn: () => void; zoomOut: () => void; resetZoom: () => void; fullscreen: () => void; devtools: () => void }>): MenuItem => ({
464
+ id: 'view', label: '&View', submenu: [
465
+ { id: 'zoomIn', label: 'Zoom In', accelerator: 'Ctrl++', click: handlers?.zoomIn },
466
+ { id: 'zoomOut', label: 'Zoom Out', accelerator: 'Ctrl+-', click: handlers?.zoomOut },
467
+ { id: 'resetZoom', label: 'Reset Zoom', accelerator: 'Ctrl+0', click: handlers?.resetZoom },
468
+ { id: 'sep1', label: '', type: 'separator' },
469
+ { id: 'fullscreen', label: 'Toggle Fullscreen', accelerator: 'F11', click: handlers?.fullscreen },
470
+ { id: 'sep2', label: '', type: 'separator' },
471
+ { id: 'devtools', label: 'Developer Tools', accelerator: 'F12', click: handlers?.devtools },
472
+ ]
473
+ }),
474
+ },
475
+ };
476
+
477
+ // ============================================================================
478
+ // Browser API - Navigation & Routing
479
+ // ============================================================================
480
+
481
+ export interface BrowserState {
482
+ url: string;
483
+ title: string;
484
+ canGoBack: boolean;
485
+ canGoForward: boolean;
486
+ isLoading: boolean;
487
+ }
488
+
489
+ type NavigateCallback = (url: string) => void;
490
+ type StateCallback = (state: BrowserState) => void;
491
+
492
+ const _navigateCallbacks: NavigateCallback[] = [];
493
+ const _stateCallbacks: StateCallback[] = [];
494
+ let _currentState: BrowserState = { url: '', title: '', canGoBack: false, canGoForward: false, isLoading: false };
495
+
496
+ // Listen for navigation events from backend
497
+ if (typeof window !== 'undefined') {
498
+ (window as any).__onNavigate__ = (url: string) => {
499
+ _currentState.url = url;
500
+ _navigateCallbacks.forEach(cb => cb(url));
501
+ };
502
+ (window as any).__onBrowserState__ = (state: BrowserState) => {
503
+ _currentState = state;
504
+ _stateCallbacks.forEach(cb => cb(state));
505
+ };
506
+ }
507
+
508
+ export const browser = {
509
+ navigate: async (url: string) => await invoke('browser.navigate', [url]),
510
+ goBack: async () => await invoke('browser.goBack', []),
511
+ goForward: async () => await invoke('browser.goForward', []),
512
+ reload: async () => await invoke('browser.reload', []),
513
+ stop: async () => await invoke('browser.stop', []),
514
+
515
+ getUrl: async (): Promise<string> => await invoke('browser.getUrl', []) as Promise<string>,
516
+ getTitle: async (): Promise<string> => await invoke('browser.getTitle', []) as Promise<string>,
517
+ getState: (): BrowserState => _currentState,
518
+
519
+ canGoBack: async (): Promise<boolean> => await invoke('browser.canGoBack', []) as Promise<boolean>,
520
+ canGoForward: async (): Promise<boolean> => await invoke('browser.canGoForward', []) as Promise<boolean>,
521
+
522
+ onNavigate: (callback: NavigateCallback) => {
523
+ _navigateCallbacks.push(callback);
524
+ return () => {
525
+ const idx = _navigateCallbacks.indexOf(callback);
526
+ if (idx > -1) _navigateCallbacks.splice(idx, 1);
527
+ };
528
+ },
529
+
530
+ onStateChange: (callback: StateCallback) => {
531
+ _stateCallbacks.push(callback);
532
+ return () => {
533
+ const idx = _stateCallbacks.indexOf(callback);
534
+ if (idx > -1) _stateCallbacks.splice(idx, 1);
535
+ };
536
+ },
537
+ };
538
+
539
+ // ============================================================================
540
+ // Router API - Simple window routing for SPAs
541
+ // ============================================================================
542
+
543
+ let _routes: Record<string, string> = {};
544
+ let _currentRoute: string = '/';
545
+
546
+ export const router = {
547
+ setRoutes: (routes: Record<string, string>) => {
548
+ _routes = routes;
549
+ },
550
+
551
+ setInitialRoute: async (route: string) => {
552
+ _currentRoute = route;
553
+ const url = _routes[route] || route;
554
+ await browser.navigate(url);
555
+ },
556
+
557
+ push: async (route: string) => {
558
+ _currentRoute = route;
559
+ const url = _routes[route] || route;
560
+ await browser.navigate(url);
561
+ },
562
+
563
+ replace: async (route: string) => {
564
+ _currentRoute = route;
565
+ const url = _routes[route] || route;
566
+ await browser.navigate(url);
567
+ },
568
+
569
+ getCurrentRoute: () => _currentRoute,
570
+
571
+ getRoutes: () => ({ ..._routes }),
572
+ };
573
+
574
+ export default { win, tray, app, display, clipboard, keyboard, menu, webgpu, browser, router };