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,99 +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
- show: async () => invoke('window.show', []),
77
- hide: async () => invoke('window.hide', []),
78
- close: async () => invoke('window.close', []),
79
- setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
80
- getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
81
- 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),
82
286
  };
83
287
 
288
+ // ─── browser ──────────────────────────────────────────────────────────────────
289
+ const _browserEvents = createFeatureConnect('browser');
290
+
84
291
  export const browser = {
85
- getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
86
- goBack: async () => invoke('browser.goBack', []),
87
- goForward: async () => invoke('browser.goForward', []),
88
- reload: async () => invoke('browser.reload', []),
89
- 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>,
90
298
  canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
91
299
  onNavigate: (handler: (url: string) => void) => {
92
- if (typeof window === 'undefined') {
93
- return () => {};
94
- }
95
-
96
- const eventHandler = (event: Event) => {
97
- const custom = event as CustomEvent<{ url?: string }>;
98
- const nextUrl = custom.detail?.url ?? '';
99
- handler(nextUrl);
100
- };
101
-
102
- window.addEventListener('plusui:navigate', eventHandler);
103
- 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);
104
304
  },
305
+ on: _browserEvents.on.bind(_browserEvents),
306
+ emit: _browserEvents.emit.bind(_browserEvents),
105
307
  };
106
308
 
309
+ // ─── router ───────────────────────────────────────────────────────────────────
107
310
  export const router = {
108
- setRoutes: (routes: RouteMap) => {
109
- _routes = routes;
110
- },
111
- push: async (path: string) => {
112
- const target = _routes[path] ?? path;
113
- return invoke('browser.navigate', [target]);
114
- },
311
+ setRoutes: (routes: RouteMap) => { _routes = routes; },
312
+ push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
115
313
  };
116
314
 
315
+ // ─── app ──────────────────────────────────────────────────────────────────────
316
+ const _appEvents = createFeatureConnect('app');
317
+
117
318
  export const app = {
118
- quit: async () => invoke('app.quit', []),
319
+ quit: async () => invoke('app.quit', []),
320
+ on: _appEvents.on.bind(_appEvents),
321
+ emit: _appEvents.emit.bind(_appEvents),
119
322
  };
120
323
 
121
- // FileDrop API
122
- export interface FileInfo {
123
- path: string;
124
- name: string;
125
- type: string;
126
- size: number;
127
- }
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; }
128
338
 
129
339
  export const fileDrop = {
130
340
  setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
131
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
341
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
132
342
  onFilesDropped: (handler: (files: FileInfo[]) => void) => {
133
343
  if (typeof window === 'undefined') return () => {};
134
- const eventHandler = (event: Event) => {
135
- const custom = event as CustomEvent<{ files?: FileInfo[] }>;
136
- handler(custom.detail?.files ?? []);
137
- };
138
- window.addEventListener('plusui:fileDrop.filesDropped', eventHandler);
139
- 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);
140
347
  },
141
348
  onDragEnter: (handler: () => void) => {
142
349
  if (typeof window === 'undefined') return () => {};
143
- const eventHandler = () => handler();
144
- window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
145
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
350
+ window.addEventListener('plusui:fileDrop.dragEnter', handler);
351
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
146
352
  },
147
353
  onDragLeave: (handler: () => void) => {
148
354
  if (typeof window === 'undefined') return () => {};
149
- const eventHandler = () => handler();
150
- window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
151
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
355
+ window.addEventListener('plusui:fileDrop.dragLeave', handler);
356
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
152
357
  },
153
358
  };
154
359
 
155
- // Helper functions
360
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
156
361
  export function formatFileSize(bytes: number): string {
157
362
  if (bytes === 0) return '0 Bytes';
158
363
  const k = 1024;
@@ -161,6 +366,36 @@ export function formatFileSize(bytes: number): string {
161
366
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
162
367
  }
163
368
 
164
- export function isImageFile(file: FileInfo): boolean {
165
- return file.type.startsWith('image/');
166
- }
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;
@@ -14,7 +14,11 @@
14
14
  "strict": true,
15
15
  "noUnusedLocals": true,
16
16
  "noUnusedParameters": true,
17
- "noFallthroughCasesInSwitch": true
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "plusui": ["./src/plusui.ts"]
21
+ }
18
22
  },
19
23
  "include": ["src"],
20
24
  "references": [{ "path": "./tsconfig.node.json" }]
@@ -1,8 +1,15 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath, URL } from 'node:url';
3
4
 
4
5
  export default defineConfig({
5
6
  plugins: [react()],
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
 
@@ -104,6 +106,14 @@ struct WebGPUConfig {
104
106
  // ============================================================================
105
107
  // MAIN - Application Entry Point
106
108
  // ============================================================================
109
+ // ── Connect instance ─────────────────────────────────────────────────────────
110
+ // connect is the bridge between C++ and the frontend.
111
+ // Run `plusui connect` to generate Connections/ from your name.on / name.emit usage.
112
+ // Then declare: Connections ch(connect);
113
+ // and use: ch.myEvent.on([](const json& p) { ... });
114
+ // ch.myEvent.emit({{"value", 42}});
115
+ static Connect connect;
116
+
107
117
  int main() {
108
118
  // Build the app with configuration
109
119
  auto appBuilder = createApp()
@@ -164,12 +174,14 @@ int main() {
164
174
  #endif
165
175
 
166
176
  // ========================================
167
- // CUSTOM C++ FUNCTIONS (expose to frontend)
177
+ // CONNECT bind frontend backend
168
178
  // ========================================
169
- // {{PROJECT_NAME_LOWER}}.bind("getVersion", [](const std::string& args) {
170
- // return "\"" + appConfig.version + "\"";
171
- // });
172
- // Call from JS: const version = await app.invoke('getVersion');
179
+ // Wires the connect object to this window.
180
+ // Connections ch gives you named channel objects — same API as TypeScript:
181
+ // ch.myEvent.on([](const json& p) { ... }); // receive from frontend
182
+ // ch.myEvent.emit({{"value", 42}}); // send to frontend
183
+ bindConnect(mainWindow, connect);
184
+ Connections ch(connect); // use ch.name.on() / ch.name.emit()
173
185
 
174
186
  // ========================================
175
187
  // FILE DROP EVENTS (Native Drag & Drop API)
@@ -194,7 +206,15 @@ int main() {
194
206
  // ============================================================================
195
207
  // FRONTEND API REFERENCE
196
208
  // ============================================================================
197
- // import { win, browser, router, app } from './frontend/src/plusui';
209
+ // import plusui from 'plusui';
210
+ // import { connect, win, clipboard, app, browser, router, fileDrop } from 'plusui';
211
+ //
212
+ // CONNECT (custom channels — same API on both sides):
213
+ // Run `plusui connect` to generate Connections/ from your name.on / name.emit calls.
214
+ // C++: ch.myEvent.on([](const json& p) { ... }); // receive
215
+ // ch.myEvent.emit({{"value", 42}}); // send
216
+ // TS: myEvent.on((data) => { ... }); // receive
217
+ // myEvent.emit({ value: 42 }); // send
198
218
  //
199
219
  // WINDOW: win.minimize(), win.maximize(), win.close(), win.center(),
200
220
  // win.setSize(w, h), win.setPosition(x, y), win.setTitle(str),
@@ -1,5 +1,7 @@
1
1
  import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
2
- import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
2
+ import plusui, { type FileInfo } from 'plusui';
3
+ // Custom channels (generated by `plusui connect`) — import what you use:
4
+ // import { greeting, download } from '../Connections/connections.gen';
3
5
 
4
6
  // Define routes for your app (optional - for SPA routing)
5
7
  const routes = {
@@ -23,32 +25,32 @@ function App() {
23
25
 
24
26
  onMount(() => {
25
27
  // Setup routes
26
- router.setRoutes(routes);
28
+ plusui.router.setRoutes(routes);
27
29
 
28
30
  // Listen for navigation changes
29
- browser.onNavigate((url) => {
31
+ plusui.browser.onNavigate((url) => {
30
32
  setCurrentUrl(url);
31
- browser.canGoBack().then(setCanGoBack);
32
- browser.canGoForward().then(setCanGoForward);
33
+ plusui.browser.canGoBack().then(setCanGoBack);
34
+ plusui.browser.canGoForward().then(setCanGoForward);
33
35
  });
34
36
 
35
37
  // Get initial state
36
- browser.getUrl().then(setCurrentUrl);
37
- browser.canGoBack().then(setCanGoBack);
38
- browser.canGoForward().then(setCanGoForward);
38
+ plusui.browser.getUrl().then(setCurrentUrl);
39
+ plusui.browser.canGoBack().then(setCanGoBack);
40
+ plusui.browser.canGoForward().then(setCanGoForward);
39
41
 
40
42
  // Setup FileDrop listeners
41
- const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
43
+ const unsubDrop = plusui.fileDrop.onFilesDropped((droppedFiles) => {
42
44
  console.log('Files dropped:', droppedFiles);
43
45
  setFiles(prev => [...prev, ...droppedFiles]);
44
46
  setIsDragging(false);
45
47
  });
46
48
 
47
- const unsubEnter = fileDrop.onDragEnter(() => {
49
+ const unsubEnter = plusui.fileDrop.onDragEnter(() => {
48
50
  setIsDragging(true);
49
51
  });
50
52
 
51
- const unsubLeave = fileDrop.onDragLeave(() => {
53
+ const unsubLeave = plusui.fileDrop.onDragLeave(() => {
52
54
  setIsDragging(false);
53
55
  });
54
56
 
@@ -59,29 +61,29 @@ function App() {
59
61
  });
60
62
  });
61
63
 
62
- const handleMinimize = async () => await win.minimize();
63
- const handleMaximize = async () => await win.maximize();
64
- const handleClose = async () => await win.close();
64
+ const handleMinimize = async () => await plusui.win.minimize();
65
+ const handleMaximize = async () => await plusui.win.maximize();
66
+ const handleClose = async () => await plusui.win.close();
65
67
  const handleGetSize = async () => {
66
- const size = await win.getSize();
68
+ const size = await plusui.win.getSize();
67
69
  setWindowSize(size);
68
70
  };
69
71
  const handleGetPosition = async () => {
70
- const pos = await win.getPosition();
72
+ const pos = await plusui.win.getPosition();
71
73
  setWindowPos(pos);
72
74
  };
73
75
 
74
76
  // Browser navigation
75
- const handleGoBack = async () => await browser.goBack();
76
- const handleGoForward = async () => await browser.goForward();
77
- const handleReload = async () => await browser.reload();
77
+ const handleGoBack = async () => await plusui.browser.goBack();
78
+ const handleGoForward = async () => await plusui.browser.goForward();
79
+ const handleReload = async () => await plusui.browser.reload();
78
80
 
79
81
  // Router navigation
80
- const handleGoHome = async () => await router.push('/');
81
- const handleGoSettings = async () => await router.push('/settings');
82
+ const handleGoHome = async () => await plusui.router.push('/');
83
+ const handleGoSettings = async () => await plusui.router.push('/settings');
82
84
 
83
85
  // App control
84
- const handleQuit = async () => await app.quit();
86
+ const handleQuit = async () => await plusui.app.quit();
85
87
 
86
88
  return (
87
89
  <div class="app">
@@ -112,8 +114,8 @@ function App() {
112
114
  <h2>Window Position</h2>
113
115
  <div class="button-group">
114
116
  <button onClick={handleGetPosition} class="button">Get Position</button>
115
- <button onClick={() => win.setPosition(100, 100)} class="button">Move Left</button>
116
- <button onClick={() => win.setPosition(800, 100)} class="button">Move Right</button>
117
+ <button onClick={() => plusui.win.setPosition(100, 100)} class="button">Move Left</button>
118
+ <button onClick={() => plusui.win.setPosition(800, 100)} class="button">Move Right</button>
117
119
  </div>
118
120
  <Show when={windowPos().x !== 0}>
119
121
  <p class="info-text">Position: {windowPos().x}, {windowPos().y}</p>
@@ -137,7 +139,7 @@ function App() {
137
139
  <button onClick={handleGoSettings} class="button">Settings</button>
138
140
  </div>
139
141
  <p style={{ 'font-size': '0.85em', color: '#666', 'margin-top': '10px' }}>
140
- Define routes with <code>router.setRoutes({'{ ... }'})</code> then navigate with <code>router.push('/path')</code>
142
+ Define routes with <code>plusui.router.setRoutes({'{ ... }'})</code> then navigate with <code>plusui.router.push('/path')</code>
141
143
  </p>
142
144
  </div>
143
145
 
@@ -196,7 +198,7 @@ function App() {
196
198
  <div class="filedrop-file-info">
197
199
  <div class="filedrop-file-name">{file.name}</div>
198
200
  <div class="filedrop-file-meta">
199
- {formatFileSize(file.size)} • {file.type}
201
+ {plusui.formatFileSize(file.size)} • {file.type}
200
202
  </div>
201
203
  </div>
202
204
  <button