plusui-native 0.2.59 → 0.2.62

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.
@@ -1,3 +1,22 @@
1
+ // ============================================================
2
+ // PlusUI — single import entrypoint
3
+ //
4
+ // import plusui from 'plusui';
5
+ //
6
+ // plusui.win.minimize();
7
+ // plusui.fileDrop.onFilesDropped(...);
8
+ // plusui.formatFileSize(bytes);
9
+ //
10
+ // For TypeScript types only:
11
+ // import type { FileInfo } from 'plusui';
12
+ //
13
+ // After running `plusui connect`, custom channel objects are
14
+ // exported from Connections/connections.gen.ts:
15
+ // import { myChannel } from '../Connections/connections.gen';
16
+ // ============================================================
17
+
18
+ // ─── Bridge bootstrap ────────────────────────────────────────────────────────
19
+
1
20
  type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
2
21
  type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
3
22
 
@@ -25,16 +44,12 @@ function initBridge() {
25
44
  return new Promise((resolve, reject) => {
26
45
  const id = Math.random().toString(36).slice(2, 11);
27
46
  const request = JSON.stringify({ id, method, params: args ?? [] });
28
-
29
47
  _pending[id] = { resolve, reject };
30
48
 
31
49
  if (typeof w.__native_invoke__ === 'function') {
32
50
  w.__native_invoke__(request);
33
51
  } else {
34
- setTimeout(() => {
35
- delete _pending[id];
36
- resolve(null);
37
- }, 0);
52
+ setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
38
53
  }
39
54
 
40
55
  setTimeout(() => {
@@ -48,10 +63,7 @@ function initBridge() {
48
63
 
49
64
  w.__response__ = (id: string, result: unknown) => {
50
65
  const pending = _pending[id];
51
- if (pending) {
52
- pending.resolve(result);
53
- delete _pending[id];
54
- }
66
+ if (pending) { pending.resolve(result); delete _pending[id]; }
55
67
  };
56
68
 
57
69
  _invoke = w.__invoke__ as InvokeFn;
@@ -60,97 +72,292 @@ function initBridge() {
60
72
  async function invoke(method: string, args?: unknown[]) {
61
73
  if (!_invoke) {
62
74
  initBridge();
63
- if (!_invoke) {
64
- throw new Error('PlusUI bridge not initialized');
65
- }
75
+ if (!_invoke) throw new Error('PlusUI bridge not initialized');
66
76
  }
67
-
68
77
  return _invoke(method, args);
69
78
  }
70
79
 
71
80
  initBridge();
72
81
 
82
+ // ─── Connection (on / emit) ───────────────────────────────────────────────────
83
+ //
84
+ // TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
85
+ //
86
+ // connect.emit('myEvent', { value: 42 }); // TS → C++
87
+ // connect.on('myEvent', (data) => { ... }); // C++ → TS
88
+ //
89
+ // Built-in features use their feature name as a scope:
90
+ // clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
91
+ // win.on('resized', (data) => { ... }) // 'window.resized'
92
+ //
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ type MessageCallback = (payload: any) => void;
96
+
97
+ class ConnectionClient {
98
+ private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
99
+ private listeners = new Map<string, Set<MessageCallback>>();
100
+
101
+ constructor() {
102
+ const host = globalThis as any;
103
+ host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
104
+ if (typeof window !== 'undefined') {
105
+ window.addEventListener('plusui:connection:message', (ev: Event) => {
106
+ this.handleIncoming((ev as CustomEvent<unknown>).detail);
107
+ });
108
+ }
109
+ }
110
+
111
+ private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
112
+
113
+ private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
114
+ const host = globalThis as any;
115
+ if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
116
+ if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
117
+ return null;
118
+ }
119
+
120
+ private decode(message: unknown): any | null {
121
+ if (!message) return null;
122
+ if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
123
+ if (typeof message === 'object') return message;
124
+ return null;
125
+ }
126
+
127
+ private handleIncoming(message: unknown): void {
128
+ const env = this.decode(message);
129
+ if (!env) return;
130
+ if ((env.kind === 'result' || env.kind === 'error') && env.id) {
131
+ const entry = this.pending.get(env.id);
132
+ if (!entry) return;
133
+ this.pending.delete(env.id);
134
+ if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
135
+ else entry.resolve(env.payload);
136
+ return;
137
+ }
138
+ if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
139
+ const handlers = this.listeners.get(env.name);
140
+ if (handlers) for (const h of handlers) h(env.payload);
141
+ }
142
+ }
143
+
144
+ async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
145
+ const id = this.nextId();
146
+ const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
147
+ await this.send({ kind: 'call', id, name, payload });
148
+ return promise;
149
+ }
150
+
151
+ fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
152
+ void this.send({ kind: 'fire', name, payload });
153
+ }
154
+
155
+ on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
156
+ const set = this.listeners.get(name) ?? new Set<MessageCallback>();
157
+ set.add(callback as MessageCallback);
158
+ this.listeners.set(name, set);
159
+ return () => {
160
+ const cur = this.listeners.get(name);
161
+ if (!cur) return;
162
+ cur.delete(callback as MessageCallback);
163
+ if (cur.size === 0) this.listeners.delete(name);
164
+ };
165
+ }
166
+
167
+ stream<TData = unknown>(name: string) {
168
+ return {
169
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
170
+ void this.send({ kind: 'sub', name });
171
+ const off = this.on<TData>(name, cb);
172
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
173
+ },
174
+ };
175
+ }
176
+
177
+ channel<TData = unknown>(name: string) {
178
+ return {
179
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
180
+ void this.send({ kind: 'sub', name });
181
+ const off = this.on<TData>(name, cb);
182
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
183
+ },
184
+ publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
185
+ };
186
+ }
187
+ }
188
+
189
+ const _client = new ConnectionClient();
190
+
191
+ // ─── FeatureConnect ───────────────────────────────────────────────────────────
192
+
193
+ export type FeatureConnect = {
194
+ on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
195
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
196
+ call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
197
+ stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
198
+ channel: <TData = unknown>(name: string) => {
199
+ subscribe: (cb: (payload: TData) => void) => (() => void);
200
+ publish: (payload: TData) => void;
201
+ };
202
+ scoped: (scope: string) => FeatureConnect;
203
+ };
204
+
205
+ function _scopeName(scope: string, name: string): string {
206
+ return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
207
+ }
208
+
209
+ async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
210
+ const host = globalThis as any;
211
+ if (typeof host.__invoke__ !== 'function') return undefined;
212
+ return host.__invoke__(method, payload === undefined ? [] : [payload]);
213
+ }
214
+
215
+ export function createFeatureConnect(scope: string): FeatureConnect {
216
+ return {
217
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
218
+ const s = _scopeName(scope, name);
219
+ void _invokeScoped(s, payload);
220
+ _client.fire(s, payload);
221
+ if (typeof window !== 'undefined') {
222
+ window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
223
+ }
224
+ },
225
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
226
+ const s = _scopeName(scope, name);
227
+ const off = _client.on<TData>(s, cb);
228
+ if (typeof window === 'undefined') return off;
229
+ const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
230
+ window.addEventListener(`plusui:${s}`, dom as EventListener);
231
+ return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
232
+ },
233
+ call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
234
+ const s = _scopeName(scope, name);
235
+ const host = globalThis as any;
236
+ if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
237
+ return _client.call<TOut, TIn>(s, payload);
238
+ },
239
+ stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
240
+ channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
241
+ scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
242
+ };
243
+ }
244
+
245
+ // ─── connect — custom channels (your app-specific messages) ──────────────────
246
+ export const connect = {
247
+ /** Send a message to C++ backend */
248
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
249
+ _client.fire(name, payload);
250
+ },
251
+ /** Listen for messages from C++ backend. Returns unsubscribe fn. */
252
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
253
+ return _client.on<TData>(name, cb);
254
+ },
255
+ /** Scoped feature connection (auto-prefixes names) */
256
+ feature: createFeatureConnect,
257
+ };
258
+
259
+ /** Advanced: raw connection client — call / stream / channel */
260
+ export const connection = _client;
261
+
262
+ // ─── win — window management ──────────────────────────────────────────────────
263
+ const _winEvents = createFeatureConnect('window');
264
+
73
265
  export const win = {
74
- minimize: async () => invoke('window.minimize', []),
75
- maximize: async () => invoke('window.maximize', []),
76
- close: async () => invoke('window.close', []),
77
- setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
78
- getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
79
- getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
266
+ minimize: async () => invoke('window.minimize', []),
267
+ maximize: async () => invoke('window.maximize', []),
268
+ show: async () => invoke('window.show', []),
269
+ hide: async () => invoke('window.hide', []),
270
+ close: async () => invoke('window.close', []),
271
+ center: async () => invoke('window.center', []),
272
+ setTitle: async (title: string) => invoke('window.setTitle', [title]),
273
+ setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
274
+ setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
275
+ setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
276
+ setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
277
+ setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
278
+ setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
279
+ setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
280
+ getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
281
+ getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
282
+ isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
283
+ isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
284
+ on: _winEvents.on.bind(_winEvents),
285
+ emit: _winEvents.emit.bind(_winEvents),
80
286
  };
81
287
 
288
+ // ─── browser ──────────────────────────────────────────────────────────────────
289
+ const _browserEvents = createFeatureConnect('browser');
290
+
82
291
  export const browser = {
83
- getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
84
- goBack: async () => invoke('browser.goBack', []),
85
- goForward: async () => invoke('browser.goForward', []),
86
- reload: async () => invoke('browser.reload', []),
87
- canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
292
+ getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
293
+ navigate: async (url: string) => invoke('browser.navigate', [url]),
294
+ goBack: async () => invoke('browser.goBack', []),
295
+ goForward: async () => invoke('browser.goForward', []),
296
+ reload: async () => invoke('browser.reload', []),
297
+ canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
88
298
  canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
89
299
  onNavigate: (handler: (url: string) => void) => {
90
- if (typeof window === 'undefined') {
91
- return () => {};
92
- }
93
-
94
- const eventHandler = (event: Event) => {
95
- const custom = event as CustomEvent<{ url?: string }>;
96
- const nextUrl = custom.detail?.url ?? '';
97
- handler(nextUrl);
98
- };
99
-
100
- window.addEventListener('plusui:navigate', eventHandler);
101
- return () => window.removeEventListener('plusui:navigate', eventHandler);
300
+ if (typeof window === 'undefined') return () => {};
301
+ const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
302
+ window.addEventListener('plusui:navigate', h);
303
+ return () => window.removeEventListener('plusui:navigate', h);
102
304
  },
305
+ on: _browserEvents.on.bind(_browserEvents),
306
+ emit: _browserEvents.emit.bind(_browserEvents),
103
307
  };
104
308
 
309
+ // ─── router ───────────────────────────────────────────────────────────────────
105
310
  export const router = {
106
- setRoutes: (routes: RouteMap) => {
107
- _routes = routes;
108
- },
109
- push: async (path: string) => {
110
- const target = _routes[path] ?? path;
111
- return invoke('browser.navigate', [target]);
112
- },
311
+ setRoutes: (routes: RouteMap) => { _routes = routes; },
312
+ push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
113
313
  };
114
314
 
315
+ // ─── app ──────────────────────────────────────────────────────────────────────
316
+ const _appEvents = createFeatureConnect('app');
317
+
115
318
  export const app = {
116
- quit: async () => invoke('app.quit', []),
319
+ quit: async () => invoke('app.quit', []),
320
+ on: _appEvents.on.bind(_appEvents),
321
+ emit: _appEvents.emit.bind(_appEvents),
117
322
  };
118
323
 
119
- // FileDrop API
120
- export interface FileInfo {
121
- path: string;
122
- name: string;
123
- type: string;
124
- size: number;
125
- }
324
+ // ─── clipboard ────────────────────────────────────────────────────────────────
325
+ const _clipboardEvents = createFeatureConnect('clipboard');
326
+
327
+ export const clipboard = {
328
+ getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
329
+ setText: async (text: string) => invoke('clipboard.setText', [text]),
330
+ clear: async () => invoke('clipboard.clear', []),
331
+ hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
332
+ on: _clipboardEvents.on.bind(_clipboardEvents),
333
+ emit: _clipboardEvents.emit.bind(_clipboardEvents),
334
+ };
335
+
336
+ // ─── fileDrop ─────────────────────────────────────────────────────────────────
337
+ export interface FileInfo { path: string; name: string; type: string; size: number; }
126
338
 
127
339
  export const fileDrop = {
128
340
  setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
129
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
341
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
130
342
  onFilesDropped: (handler: (files: FileInfo[]) => void) => {
131
343
  if (typeof window === 'undefined') return () => {};
132
- const eventHandler = (event: Event) => {
133
- const custom = event as CustomEvent<{ files?: FileInfo[] }>;
134
- handler(custom.detail?.files ?? []);
135
- };
136
- window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
137
- return () => window.removeEventListener('plusui:fileDrop.filesDropped', eventHandler);
344
+ const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
345
+ window.addEventListener('plusui:fileDrop.filesDropped', h);
346
+ return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
138
347
  },
139
348
  onDragEnter: (handler: () => void) => {
140
349
  if (typeof window === 'undefined') return () => {};
141
- const eventHandler = () => handler();
142
- window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
143
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
350
+ window.addEventListener('plusui:fileDrop.dragEnter', handler);
351
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
144
352
  },
145
353
  onDragLeave: (handler: () => void) => {
146
354
  if (typeof window === 'undefined') return () => {};
147
- const eventHandler = () => handler();
148
- window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
149
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
355
+ window.addEventListener('plusui:fileDrop.dragLeave', handler);
356
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
150
357
  },
151
358
  };
152
359
 
153
- // Helper functions
360
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
154
361
  export function formatFileSize(bytes: number): string {
155
362
  if (bytes === 0) return '0 Bytes';
156
363
  const k = 1024;
@@ -159,6 +366,37 @@ export function formatFileSize(bytes: number): string {
159
366
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
160
367
  }
161
368
 
162
- export function isImageFile(file: FileInfo): boolean {
163
- return file.type.startsWith('image/');
164
- }
369
+ export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
370
+
371
+ // ─── Top-level on / emit ─────────────────────────────────────────────────────
372
+ //
373
+ // import plusui from 'plusui';
374
+ //
375
+ // plusui.emit('myEvent', { value: 42 }); // TS → C++
376
+ // plusui.on('myEvent', (data) => { ... }); // C++ → TS
377
+ //
378
+ // plusui.win.minimize();
379
+ // plusui.clipboard.on('changed', (data) => { ... });
380
+ //
381
+ export const on = connect.on.bind(connect) as typeof connect.on;
382
+ export const emit = connect.emit.bind(connect) as typeof connect.emit;
383
+
384
+ // ─── Default export — everything under one roof ───────────────────────────────
385
+ const plusui = {
386
+ /** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
387
+ feature: createFeatureConnect,
388
+ connection,
389
+ win,
390
+ browser,
391
+ router,
392
+ app,
393
+ clipboard,
394
+ fileDrop,
395
+ formatFileSize,
396
+ isImageFile,
397
+ on,
398
+ emit,
399
+ };
400
+
401
+ export default plusui;
402
+
@@ -15,7 +15,11 @@
15
15
  "strict": true,
16
16
  "noUnusedLocals": true,
17
17
  "noUnusedParameters": true,
18
- "noFallthroughCasesInSwitch": true
18
+ "noFallthroughCasesInSwitch": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "plusui": ["./src/plusui.ts"]
22
+ }
19
23
  },
20
24
  "include": ["src"],
21
25
  "references": [{ "path": "./tsconfig.node.json" }]
@@ -1,8 +1,15 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import solid from 'vite-plugin-solid';
3
+ import { fileURLToPath, URL } from 'node:url';
3
4
 
4
5
  export default defineConfig({
5
6
  plugins: [solid()],
7
+ resolve: {
8
+ alias: {
9
+ // `import plusui from 'plusui'` resolves to your local plusui.ts
10
+ plusui: fileURLToPath(new URL('./src/plusui.ts', import.meta.url)),
11
+ },
12
+ },
6
13
  build: {
7
14
  outDir: 'dist',
8
15
  emptyOutDir: true,
@@ -1,6 +1,8 @@
1
1
  #include <plusui/plusui.hpp> // All-in-one framework header
2
2
  #include <iostream>
3
3
  #include "generated/assets.h"
4
+ // ── Generated channel bindings (run `plusui connect` to regenerate) ──────────
5
+ #include "Connections/connections.gen.hpp"
4
6
 
5
7
  using namespace plusui;
6
8
 
@@ -103,6 +105,14 @@ struct WebGPUConfig {
103
105
  // ============================================================================
104
106
  // MAIN - Application Entry Point
105
107
  // ============================================================================
108
+ // ── Connect instance ─────────────────────────────────────────────────────────
109
+ // connect is the bridge between C++ and the frontend.
110
+ // Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
111
+ // Then declare: Connections ch(connect);
112
+ // and use: ch.myEvent.on([](const json& p) { ... });
113
+ // ch.myEvent.emit({{"value", 42}});
114
+ static Connect connect;
115
+
106
116
  int main() {
107
117
  // Build the app with configuration
108
118
  auto appBuilder = createApp()
@@ -158,12 +168,14 @@ int main() {
158
168
  #endif
159
169
 
160
170
  // ========================================
161
- // CUSTOM C++ FUNCTIONS (expose to frontend)
171
+ // CONNECT bind frontend backend
162
172
  // ========================================
163
- // {{PROJECT_NAME_LOWER}}.bind("getVersion", [](const std::string& args) {
164
- // return "\"" + appConfig.version + "\"";
165
- // });
166
- // Call from JS: const version = await app.invoke('getVersion');
173
+ // Wires the connect object to this window.
174
+ // Connections ch gives you named channel objects — same API as TypeScript:
175
+ // ch.myEvent.on([](const json& p) { ... }); // receive from frontend
176
+ // ch.myEvent.emit({{"value", 42}}); // send to frontend
177
+ bindConnect(mainWindow, connect);
178
+ Connections ch(connect); // use ch.name.on() / ch.name.emit()
167
179
 
168
180
  // ========================================
169
181
  // FILE DROP EVENTS (Native Drag & Drop API)
@@ -188,7 +200,15 @@ int main() {
188
200
  // ============================================================================
189
201
  // FRONTEND API REFERENCE
190
202
  // ============================================================================
191
- // import { win, browser, router, app } from './frontend/src/plusui';
203
+ // import plusui from 'plusui';
204
+ // import { connect, win, clipboard, app, browser, router, fileDrop } from 'plusui';
205
+ //
206
+ // CONNECT (custom channels — same API on both sides):
207
+ // Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
208
+ // C++: ch.myEvent.on([](const json& p) { ... }); // receive
209
+ // ch.myEvent.emit({{"value", 42}}); // send
210
+ // TS: myEvent.on((data) => { ... }); // receive
211
+ // myEvent.emit({ value: 42 }); // send
192
212
  //
193
213
  // WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
194
214
  // win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),