plusui-native 0.2.57 → 0.2.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native",
3
- "version": "0.2.57",
3
+ "version": "0.2.61",
4
4
  "description": "PlusUI CLI - Build C++ desktop apps modern UI ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "semver": "^7.6.0",
28
28
  "which": "^4.0.0",
29
29
  "execa": "^8.0.1",
30
- "plusui-native-builder": "^0.1.56",
31
- "plusui-native-connect": "^0.1.56"
30
+ "plusui-native-builder": "^0.1.60",
31
+ "plusui-native-connect": "^0.1.60"
32
32
  },
33
33
  "peerDependencies": {
34
- "plusui-native-connect": "^0.1.56"
34
+ "plusui-native-connect": "^0.1.60"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { mkdir, readFile, stat, rm, readdir, writeFile, copyFile } from 'fs/promises';
4
4
  import { existsSync, watch, statSync, mkdirSync } from 'fs';
@@ -400,8 +400,8 @@ async function updatePlusUIPackages() {
400
400
  function getAppBindgenPaths() {
401
401
  return {
402
402
  featuresDir: join(process.cwd(), 'src', 'features'),
403
- outputDir: join(process.cwd(), 'src', 'Bindings'),
404
- frontendOutputDir: join(process.cwd(), 'frontend', 'src', 'Bindings'),
403
+ // Connections/ is now at the project root — shared by C++ and TS
404
+ outputDir: join(process.cwd(), 'Connections'),
405
405
  };
406
406
  }
407
407
 
@@ -1089,14 +1089,12 @@ async function runBindgen(providedArgs = null, options = {}) {
1089
1089
 
1090
1090
  let usedDefaultAppMode = false;
1091
1091
  let defaultOutputDir = null;
1092
- let defaultFrontendOutputDir = null;
1093
1092
 
1094
1093
  if (bindgenArgs.length === 0) {
1095
- const { outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
1094
+ const { outputDir: appOutputDir } = getAppBindgenPaths();
1096
1095
  bindgenArgs = [process.cwd(), appOutputDir];
1097
1096
  usedDefaultAppMode = true;
1098
1097
  defaultOutputDir = appOutputDir;
1099
- defaultFrontendOutputDir = frontendOutputDir;
1100
1098
  log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
1101
1099
  }
1102
1100
 
@@ -1110,9 +1108,6 @@ async function runBindgen(providedArgs = null, options = {}) {
1110
1108
  proc.on('close', async (code) => {
1111
1109
  if (code === 0) {
1112
1110
  try {
1113
- if (usedDefaultAppMode && defaultOutputDir && defaultFrontendOutputDir) {
1114
- await syncGeneratedTsBindings(defaultOutputDir, defaultFrontendOutputDir);
1115
- }
1116
1111
  log('\nBindgen complete!', 'green');
1117
1112
  resolve();
1118
1113
  } catch (syncErr) {
@@ -40,23 +40,19 @@ npm run connect
40
40
 
41
41
  Default connect generator paths:
42
42
  - Input: project root scan (frontend + backend files)
43
- - Output: `src/Bindings`
43
+ - Output: `Connections/` (at project root)
44
44
 
45
45
  Generated structure:
46
- - `src/Bindings/bindings.gen.ts`
47
- - `src/Bindings/bindings.gen.hpp`
48
- - `src/Bindings/connect.manifest.json`
46
+ - `Connections/connections.gen.ts` — typed TS channel exports (regenerated)
47
+ - `Connections/connections.gen.hpp` — C++ `Connections` struct (regenerated)
48
+ - `Connections/connect.manifest.json` — detected channel list (regenerated)
49
+
50
+ All three files are always regenerated — do not manually edit them.
49
51
 
50
52
  Scan extensions:
51
53
  - `WEB_IO`: `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.html`
52
54
  - `CPP_IO`: `.h`, `.hpp`, `.hh`, `.hxx`, `.cpp`, `.cc`, `.cxx`
53
55
 
54
- Custom binding kinds detected:
55
- - `method`
56
- - `service`
57
- - `stream`
58
- - `event`
59
-
60
56
  `plusui connect` scans your project structure and does not require a schema file.
61
57
 
62
58
  You can also pass custom paths:
@@ -163,10 +159,12 @@ just help # Show all commands
163
159
 
164
160
  ## Calling C++ from TypeScript
165
161
 
166
- Use the local `frontend/src/plusui.ts` bridge to interact with the native backend:
162
+ All features and custom channels are available via a single import:
167
163
 
168
164
  ```tsx
169
- import { win } from './plusui';
165
+ import plusui from 'plusui';
166
+ // or named imports:
167
+ import { connect, win, clipboard, app } from 'plusui';
170
168
 
171
169
  // Window controls
172
170
  await win.minimize();
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import { EnvironmentDoctor } from '../src/doctor/index.js';
6
6
  import chalk from 'chalk';
7
7
  import { spawn } from 'child_process';
8
+ import { execSync } from 'child_process';
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -24,6 +25,58 @@ export class TemplateManager {
24
25
  return this.cliPackageJson.version;
25
26
  }
26
27
 
28
+ async loadCliPackageJson() {
29
+ if (!this.cliPackageJson) {
30
+ const packageJsonPath = join(this.templatesDir, '..', 'package.json');
31
+ const content = await readFile(packageJsonPath, 'utf8');
32
+ this.cliPackageJson = JSON.parse(content);
33
+ }
34
+ return this.cliPackageJson;
35
+ }
36
+
37
+ getLatestPublishedVersion(packageName) {
38
+ try {
39
+ const version = execSync(`npm view ${packageName} version`, {
40
+ encoding: 'utf8',
41
+ stdio: ['pipe', 'pipe', 'ignore']
42
+ }).trim();
43
+ return version || null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ toCompatibleRange(version) {
50
+ if (!version) {
51
+ return null;
52
+ }
53
+ return version.startsWith('^') || version.startsWith('~') ? version : `^${version}`;
54
+ }
55
+
56
+ async resolveTemplatePackageVersions() {
57
+ const cliPkg = await this.loadCliPackageJson();
58
+ const cliVersion = cliPkg.version;
59
+ const [major, minor] = cliVersion.split('.');
60
+
61
+ const fallbackCliRange = `^${major}.${minor}.0`;
62
+ const fallbackToolsRange = `^${major}.1.0`;
63
+
64
+ const publishedCli = this.getLatestPublishedVersion('plusui-native');
65
+ const publishedCore = this.getLatestPublishedVersion('plusui-native-core');
66
+ const publishedBuilder = this.getLatestPublishedVersion('plusui-native-builder');
67
+ const publishedConnect = this.getLatestPublishedVersion('plusui-native-connect');
68
+
69
+ const cliDependencyBuilder = cliPkg.dependencies?.['plusui-native-builder'] || null;
70
+ const cliDependencyConnect = cliPkg.dependencies?.['plusui-native-connect'] || null;
71
+
72
+ return {
73
+ cli: this.toCompatibleRange(publishedCli) || fallbackCliRange,
74
+ core: this.toCompatibleRange(publishedCore) || fallbackToolsRange,
75
+ builder: this.toCompatibleRange(publishedBuilder) || cliDependencyBuilder || fallbackToolsRange,
76
+ connect: this.toCompatibleRange(publishedConnect) || cliDependencyConnect || fallbackToolsRange
77
+ };
78
+ }
79
+
27
80
  async create(projectName, options = {}) {
28
81
  const template = options.template || 'react';
29
82
  const templatePath = join(this.templatesDir, template);
@@ -53,32 +106,22 @@ export class TemplateManager {
53
106
 
54
107
  await mkdir(projectPath, { recursive: true });
55
108
 
56
- // 3. Get current package versions
57
- const cliVersion = await this.loadCliVersion();
58
-
59
- // Use version ranges that match latest published versions
60
- // For CLI (plusui-native): Use major.minor.0 to get latest compatible
61
- const [major, minor, patch] = cliVersion.split('.');
62
- const cliVersionRange = `^${major}.${minor}.0`;
63
-
64
- // Core version follows 0.1.x pattern when CLI is 0.2.x
65
- const coreVersionRange = `^${major}.1.0`;
66
-
67
- // Builder and connect generator also follow 0.1.x pattern
68
- const builderVersionRange = `^${major}.1.0`;
69
- const connectVersionRange = `^${major}.1.0`;
109
+ // 3. Resolve package versions for generated app dependencies
110
+ const resolvedVersions = await this.resolveTemplatePackageVersions();
70
111
 
71
112
  // 4. Prepare template variables
72
113
  const variables = {
73
114
  PROJECT_NAME: projectName,
74
115
  PROJECT_NAME_LOWER: projectName.toLowerCase(),
75
116
  PROJECT_VERSION: '0.1.0',
76
- PLUSUI_CLI_VERSION: cliVersionRange,
77
- PLUSUI_CORE_VERSION: coreVersionRange,
78
- PLUSUI_BUILDER_VERSION: builderVersionRange,
79
- PLUSUI_CONNECT_VERSION: connectVersionRange
117
+ PLUSUI_CLI_VERSION: resolvedVersions.cli,
118
+ PLUSUI_CORE_VERSION: resolvedVersions.core,
119
+ PLUSUI_BUILDER_VERSION: resolvedVersions.builder,
120
+ PLUSUI_CONNECT_VERSION: resolvedVersions.connect
80
121
  };
81
122
 
123
+ console.log(chalk.dim(`Using versions: core=${variables.PLUSUI_CORE_VERSION}, builder=${variables.PLUSUI_BUILDER_VERSION}, connect=${variables.PLUSUI_CONNECT_VERSION}`));
124
+
82
125
  // 5. Copy template files
83
126
  await this.copyTemplate(templatePath, projectPath, variables);
84
127
 
@@ -1,5 +1,7 @@
1
1
  import { useState, useEffect } from 'react';
2
- import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
2
+ import { win, browser, router, app, fileDrop, formatFileSize, 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 = {
@@ -1,3 +1,18 @@
1
+ // ============================================================
2
+ // PlusUI — single import entrypoint
3
+ //
4
+ // import plusui from 'plusui';
5
+ // import { connect, win, clipboard, app } from 'plusui';
6
+ //
7
+ // Everything your app needs — built-in features AND your own
8
+ // custom connect channels — is available from this one import.
9
+ //
10
+ // After running `plusui connect`, Connections/connections.gen.ts
11
+ // is auto-imported here so all discovered channels become typed.
12
+ // ============================================================
13
+
14
+ // ─── Bridge bootstrap ────────────────────────────────────────────────────────
15
+
1
16
  type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
2
17
  type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
3
18
 
@@ -25,16 +40,12 @@ function initBridge() {
25
40
  return new Promise((resolve, reject) => {
26
41
  const id = Math.random().toString(36).slice(2, 11);
27
42
  const request = JSON.stringify({ id, method, params: args ?? [] });
28
-
29
43
  _pending[id] = { resolve, reject };
30
44
 
31
45
  if (typeof w.__native_invoke__ === 'function') {
32
46
  w.__native_invoke__(request);
33
47
  } else {
34
- setTimeout(() => {
35
- delete _pending[id];
36
- resolve(null);
37
- }, 0);
48
+ setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
38
49
  }
39
50
 
40
51
  setTimeout(() => {
@@ -48,10 +59,7 @@ function initBridge() {
48
59
 
49
60
  w.__response__ = (id: string, result: unknown) => {
50
61
  const pending = _pending[id];
51
- if (pending) {
52
- pending.resolve(result);
53
- delete _pending[id];
54
- }
62
+ if (pending) { pending.resolve(result); delete _pending[id]; }
55
63
  };
56
64
 
57
65
  _invoke = w.__invoke__ as InvokeFn;
@@ -60,99 +68,292 @@ function initBridge() {
60
68
  async function invoke(method: string, args?: unknown[]) {
61
69
  if (!_invoke) {
62
70
  initBridge();
63
- if (!_invoke) {
64
- throw new Error('PlusUI bridge not initialized');
65
- }
71
+ if (!_invoke) throw new Error('PlusUI bridge not initialized');
66
72
  }
67
-
68
73
  return _invoke(method, args);
69
74
  }
70
75
 
71
76
  initBridge();
72
77
 
78
+ // ─── Connection (on / emit) ───────────────────────────────────────────────────
79
+ //
80
+ // TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
81
+ //
82
+ // connect.emit('myEvent', { value: 42 }); // TS → C++
83
+ // connect.on('myEvent', (data) => { ... }); // C++ → TS
84
+ //
85
+ // Built-in features use their feature name as a scope:
86
+ // clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
87
+ // win.on('resized', (data) => { ... }) // 'window.resized'
88
+ //
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ type MessageCallback = (payload: any) => void;
92
+
93
+ class ConnectionClient {
94
+ private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
95
+ private listeners = new Map<string, Set<MessageCallback>>();
96
+
97
+ constructor() {
98
+ const host = globalThis as any;
99
+ host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
100
+ if (typeof window !== 'undefined') {
101
+ window.addEventListener('plusui:connection:message', (ev: Event) => {
102
+ this.handleIncoming((ev as CustomEvent<unknown>).detail);
103
+ });
104
+ }
105
+ }
106
+
107
+ private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
108
+
109
+ private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
110
+ const host = globalThis as any;
111
+ if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
112
+ if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
113
+ return null;
114
+ }
115
+
116
+ private decode(message: unknown): any | null {
117
+ if (!message) return null;
118
+ if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
119
+ if (typeof message === 'object') return message;
120
+ return null;
121
+ }
122
+
123
+ private handleIncoming(message: unknown): void {
124
+ const env = this.decode(message);
125
+ if (!env) return;
126
+ if ((env.kind === 'result' || env.kind === 'error') && env.id) {
127
+ const entry = this.pending.get(env.id);
128
+ if (!entry) return;
129
+ this.pending.delete(env.id);
130
+ if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
131
+ else entry.resolve(env.payload);
132
+ return;
133
+ }
134
+ if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
135
+ const handlers = this.listeners.get(env.name);
136
+ if (handlers) for (const h of handlers) h(env.payload);
137
+ }
138
+ }
139
+
140
+ async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
141
+ const id = this.nextId();
142
+ const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
143
+ await this.send({ kind: 'call', id, name, payload });
144
+ return promise;
145
+ }
146
+
147
+ fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
148
+ void this.send({ kind: 'fire', name, payload });
149
+ }
150
+
151
+ on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
152
+ const set = this.listeners.get(name) ?? new Set<MessageCallback>();
153
+ set.add(callback as MessageCallback);
154
+ this.listeners.set(name, set);
155
+ return () => {
156
+ const cur = this.listeners.get(name);
157
+ if (!cur) return;
158
+ cur.delete(callback as MessageCallback);
159
+ if (cur.size === 0) this.listeners.delete(name);
160
+ };
161
+ }
162
+
163
+ stream<TData = unknown>(name: string) {
164
+ return {
165
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
166
+ void this.send({ kind: 'sub', name });
167
+ const off = this.on<TData>(name, cb);
168
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
169
+ },
170
+ };
171
+ }
172
+
173
+ channel<TData = unknown>(name: string) {
174
+ return {
175
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
176
+ void this.send({ kind: 'sub', name });
177
+ const off = this.on<TData>(name, cb);
178
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
179
+ },
180
+ publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
181
+ };
182
+ }
183
+ }
184
+
185
+ const _client = new ConnectionClient();
186
+
187
+ // ─── FeatureConnect ───────────────────────────────────────────────────────────
188
+
189
+ export type FeatureConnect = {
190
+ on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
191
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
192
+ call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
193
+ stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
194
+ channel: <TData = unknown>(name: string) => {
195
+ subscribe: (cb: (payload: TData) => void) => (() => void);
196
+ publish: (payload: TData) => void;
197
+ };
198
+ scoped: (scope: string) => FeatureConnect;
199
+ };
200
+
201
+ function _scopeName(scope: string, name: string): string {
202
+ return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
203
+ }
204
+
205
+ async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
206
+ const host = globalThis as any;
207
+ if (typeof host.__invoke__ !== 'function') return undefined;
208
+ return host.__invoke__(method, payload === undefined ? [] : [payload]);
209
+ }
210
+
211
+ export function createFeatureConnect(scope: string): FeatureConnect {
212
+ return {
213
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
214
+ const s = _scopeName(scope, name);
215
+ void _invokeScoped(s, payload);
216
+ _client.fire(s, payload);
217
+ if (typeof window !== 'undefined') {
218
+ window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
219
+ }
220
+ },
221
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
222
+ const s = _scopeName(scope, name);
223
+ const off = _client.on<TData>(s, cb);
224
+ if (typeof window === 'undefined') return off;
225
+ const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
226
+ window.addEventListener(`plusui:${s}`, dom as EventListener);
227
+ return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
228
+ },
229
+ call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
230
+ const s = _scopeName(scope, name);
231
+ const host = globalThis as any;
232
+ if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
233
+ return _client.call<TOut, TIn>(s, payload);
234
+ },
235
+ stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
236
+ channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
237
+ scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
238
+ };
239
+ }
240
+
241
+ // ─── connect — custom channels (your app-specific messages) ──────────────────
242
+ export const connect = {
243
+ /** Send a message to C++ backend */
244
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
245
+ _client.fire(name, payload);
246
+ },
247
+ /** Listen for messages from C++ backend. Returns unsubscribe fn. */
248
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
249
+ return _client.on<TData>(name, cb);
250
+ },
251
+ /** Scoped feature connection (auto-prefixes names) */
252
+ feature: createFeatureConnect,
253
+ };
254
+
255
+ /** Advanced: raw connection client — call / stream / channel */
256
+ export const connection = _client;
257
+
258
+ // ─── win — window management ──────────────────────────────────────────────────
259
+ const _winEvents = createFeatureConnect('window');
260
+
73
261
  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>,
262
+ minimize: async () => invoke('window.minimize', []),
263
+ maximize: async () => invoke('window.maximize', []),
264
+ show: async () => invoke('window.show', []),
265
+ hide: async () => invoke('window.hide', []),
266
+ close: async () => invoke('window.close', []),
267
+ center: async () => invoke('window.center', []),
268
+ setTitle: async (title: string) => invoke('window.setTitle', [title]),
269
+ setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
270
+ setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
271
+ setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
272
+ setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
273
+ setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
274
+ setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
275
+ setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
276
+ getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
277
+ getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
278
+ isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
279
+ isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
280
+ on: _winEvents.on.bind(_winEvents),
281
+ emit: _winEvents.emit.bind(_winEvents),
82
282
  };
83
283
 
284
+ // ─── browser ──────────────────────────────────────────────────────────────────
285
+ const _browserEvents = createFeatureConnect('browser');
286
+
84
287
  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>,
288
+ getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
289
+ navigate: async (url: string) => invoke('browser.navigate', [url]),
290
+ goBack: async () => invoke('browser.goBack', []),
291
+ goForward: async () => invoke('browser.goForward', []),
292
+ reload: async () => invoke('browser.reload', []),
293
+ canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
90
294
  canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
91
295
  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);
296
+ if (typeof window === 'undefined') return () => {};
297
+ const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
298
+ window.addEventListener('plusui:navigate', h);
299
+ return () => window.removeEventListener('plusui:navigate', h);
104
300
  },
301
+ on: _browserEvents.on.bind(_browserEvents),
302
+ emit: _browserEvents.emit.bind(_browserEvents),
105
303
  };
106
304
 
305
+ // ─── router ───────────────────────────────────────────────────────────────────
107
306
  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
- },
307
+ setRoutes: (routes: RouteMap) => { _routes = routes; },
308
+ push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
115
309
  };
116
310
 
311
+ // ─── app ──────────────────────────────────────────────────────────────────────
312
+ const _appEvents = createFeatureConnect('app');
313
+
117
314
  export const app = {
118
- quit: async () => invoke('app.quit', []),
315
+ quit: async () => invoke('app.quit', []),
316
+ on: _appEvents.on.bind(_appEvents),
317
+ emit: _appEvents.emit.bind(_appEvents),
119
318
  };
120
319
 
121
- // FileDrop API
122
- export interface FileInfo {
123
- path: string;
124
- name: string;
125
- type: string;
126
- size: number;
127
- }
320
+ // ─── clipboard ────────────────────────────────────────────────────────────────
321
+ const _clipboardEvents = createFeatureConnect('clipboard');
322
+
323
+ export const clipboard = {
324
+ getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
325
+ setText: async (text: string) => invoke('clipboard.setText', [text]),
326
+ clear: async () => invoke('clipboard.clear', []),
327
+ hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
328
+ on: _clipboardEvents.on.bind(_clipboardEvents),
329
+ emit: _clipboardEvents.emit.bind(_clipboardEvents),
330
+ };
331
+
332
+ // ─── fileDrop ─────────────────────────────────────────────────────────────────
333
+ export interface FileInfo { path: string; name: string; type: string; size: number; }
128
334
 
129
335
  export const fileDrop = {
130
336
  setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
131
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
337
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
132
338
  onFilesDropped: (handler: (files: FileInfo[]) => void) => {
133
339
  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);
340
+ const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
341
+ window.addEventListener('plusui:fileDrop.filesDropped', h);
342
+ return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
140
343
  },
141
344
  onDragEnter: (handler: () => void) => {
142
345
  if (typeof window === 'undefined') return () => {};
143
- const eventHandler = () => handler();
144
- window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
145
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
346
+ window.addEventListener('plusui:fileDrop.dragEnter', handler);
347
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
146
348
  },
147
349
  onDragLeave: (handler: () => void) => {
148
350
  if (typeof window === 'undefined') return () => {};
149
- const eventHandler = () => handler();
150
- window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
151
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
351
+ window.addEventListener('plusui:fileDrop.dragLeave', handler);
352
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
152
353
  },
153
354
  };
154
355
 
155
- // Helper functions
356
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
156
357
  export function formatFileSize(bytes: number): string {
157
358
  if (bytes === 0) return '0 Bytes';
158
359
  const k = 1024;
@@ -161,6 +362,40 @@ export function formatFileSize(bytes: number): string {
161
362
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
162
363
  }
163
364
 
164
- export function isImageFile(file: FileInfo): boolean {
165
- return file.type.startsWith('image/');
166
- }
365
+ export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
366
+
367
+ // ─── Top-level on / emit ─────────────────────────────────────────────────────
368
+ //
369
+ // Custom app events — import directly, no wrapper prefix:
370
+ //
371
+ // import { on, emit } from 'plusui';
372
+ //
373
+ // emit('myEvent', { value: 42 }); // TS → C++
374
+ // on('myEvent', (data) => { ... }); // C++ → TS
375
+ //
376
+ // Framework features use their own namespace:
377
+ //
378
+ // import { win, clipboard } from 'plusui';
379
+ //
380
+ // win.minimize();
381
+ // clipboard.on('changed', (data) => { ... });
382
+ //
383
+ export const on = connect.on.bind(connect) as typeof connect.on;
384
+ export const emit = connect.emit.bind(connect) as typeof connect.emit;
385
+
386
+ // ─── Default export — everything under one roof ───────────────────────────────
387
+ const plusui = {
388
+ /** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
389
+ feature: createFeatureConnect,
390
+ connection,
391
+ win,
392
+ browser,
393
+ router,
394
+ app,
395
+ clipboard,
396
+ fileDrop,
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 { win, browser, router, app, fileDrop, formatFileSize, 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 = {
@@ -1,3 +1,18 @@
1
+ // ============================================================
2
+ // PlusUI — single import entrypoint
3
+ //
4
+ // import plusui from 'plusui';
5
+ // import { connect, win, clipboard, app } from 'plusui';
6
+ //
7
+ // Everything your app needs — built-in features AND your own
8
+ // custom connect channels — is available from this one import.
9
+ //
10
+ // After running `plusui connect`, Connections/connections.gen.ts
11
+ // is auto-imported here so all discovered channels become typed.
12
+ // ============================================================
13
+
14
+ // ─── Bridge bootstrap ────────────────────────────────────────────────────────
15
+
1
16
  type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;
2
17
  type PendingMap = Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }>;
3
18
 
@@ -25,16 +40,12 @@ function initBridge() {
25
40
  return new Promise((resolve, reject) => {
26
41
  const id = Math.random().toString(36).slice(2, 11);
27
42
  const request = JSON.stringify({ id, method, params: args ?? [] });
28
-
29
43
  _pending[id] = { resolve, reject };
30
44
 
31
45
  if (typeof w.__native_invoke__ === 'function') {
32
46
  w.__native_invoke__(request);
33
47
  } else {
34
- setTimeout(() => {
35
- delete _pending[id];
36
- resolve(null);
37
- }, 0);
48
+ setTimeout(() => { delete _pending[id]; resolve(null); }, 0);
38
49
  }
39
50
 
40
51
  setTimeout(() => {
@@ -48,10 +59,7 @@ function initBridge() {
48
59
 
49
60
  w.__response__ = (id: string, result: unknown) => {
50
61
  const pending = _pending[id];
51
- if (pending) {
52
- pending.resolve(result);
53
- delete _pending[id];
54
- }
62
+ if (pending) { pending.resolve(result); delete _pending[id]; }
55
63
  };
56
64
 
57
65
  _invoke = w.__invoke__ as InvokeFn;
@@ -60,97 +68,292 @@ function initBridge() {
60
68
  async function invoke(method: string, args?: unknown[]) {
61
69
  if (!_invoke) {
62
70
  initBridge();
63
- if (!_invoke) {
64
- throw new Error('PlusUI bridge not initialized');
65
- }
71
+ if (!_invoke) throw new Error('PlusUI bridge not initialized');
66
72
  }
67
-
68
73
  return _invoke(method, args);
69
74
  }
70
75
 
71
76
  initBridge();
72
77
 
78
+ // ─── Connection (on / emit) ───────────────────────────────────────────────────
79
+ //
80
+ // TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
81
+ //
82
+ // connect.emit('myEvent', { value: 42 }); // TS → C++
83
+ // connect.on('myEvent', (data) => { ... }); // C++ → TS
84
+ //
85
+ // Built-in features use their feature name as a scope:
86
+ // clipboard.on('changed', (data) => { ... }) // 'clipboard.changed'
87
+ // win.on('resized', (data) => { ... }) // 'window.resized'
88
+ //
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ type MessageCallback = (payload: any) => void;
92
+
93
+ class ConnectionClient {
94
+ private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
95
+ private listeners = new Map<string, Set<MessageCallback>>();
96
+
97
+ constructor() {
98
+ const host = globalThis as any;
99
+ host.__plusuiConnectionMessage = (message: unknown) => this.handleIncoming(message);
100
+ if (typeof window !== 'undefined') {
101
+ window.addEventListener('plusui:connection:message', (ev: Event) => {
102
+ this.handleIncoming((ev as CustomEvent<unknown>).detail);
103
+ });
104
+ }
105
+ }
106
+
107
+ private nextId() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
108
+
109
+ private async send(env: { kind: string; id?: string; name: string; payload?: unknown }): Promise<any> {
110
+ const host = globalThis as any;
111
+ if (typeof host.__invoke__ === 'function') return host.__invoke__('connection.dispatch', env);
112
+ if (host.ipc?.postMessage) host.ipc.postMessage(JSON.stringify(env));
113
+ return null;
114
+ }
115
+
116
+ private decode(message: unknown): any | null {
117
+ if (!message) return null;
118
+ if (typeof message === 'string') { try { return JSON.parse(message); } catch { return null; } }
119
+ if (typeof message === 'object') return message;
120
+ return null;
121
+ }
122
+
123
+ private handleIncoming(message: unknown): void {
124
+ const env = this.decode(message);
125
+ if (!env) return;
126
+ if ((env.kind === 'result' || env.kind === 'error') && env.id) {
127
+ const entry = this.pending.get(env.id);
128
+ if (!entry) return;
129
+ this.pending.delete(env.id);
130
+ if (env.kind === 'error') entry.reject(new Error(env.error || 'Connection call failed'));
131
+ else entry.resolve(env.payload);
132
+ return;
133
+ }
134
+ if (env.kind === 'event' || env.kind === 'stream' || env.kind === 'publish') {
135
+ const handlers = this.listeners.get(env.name);
136
+ if (handlers) for (const h of handlers) h(env.payload);
137
+ }
138
+ }
139
+
140
+ async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
141
+ const id = this.nextId();
142
+ const promise = new Promise<TOut>((resolve, reject) => this.pending.set(id, { resolve, reject }));
143
+ await this.send({ kind: 'call', id, name, payload });
144
+ return promise;
145
+ }
146
+
147
+ fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
148
+ void this.send({ kind: 'fire', name, payload });
149
+ }
150
+
151
+ on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
152
+ const set = this.listeners.get(name) ?? new Set<MessageCallback>();
153
+ set.add(callback as MessageCallback);
154
+ this.listeners.set(name, set);
155
+ return () => {
156
+ const cur = this.listeners.get(name);
157
+ if (!cur) return;
158
+ cur.delete(callback as MessageCallback);
159
+ if (cur.size === 0) this.listeners.delete(name);
160
+ };
161
+ }
162
+
163
+ stream<TData = unknown>(name: string) {
164
+ return {
165
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
166
+ void this.send({ kind: 'sub', name });
167
+ const off = this.on<TData>(name, cb);
168
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
169
+ },
170
+ };
171
+ }
172
+
173
+ channel<TData = unknown>(name: string) {
174
+ return {
175
+ subscribe: (cb: (payload: TData) => void): (() => void) => {
176
+ void this.send({ kind: 'sub', name });
177
+ const off = this.on<TData>(name, cb);
178
+ return () => { off(); void this.send({ kind: 'unsub', name }); };
179
+ },
180
+ publish: (payload: TData): void => { void this.send({ kind: 'publish', name, payload }); },
181
+ };
182
+ }
183
+ }
184
+
185
+ const _client = new ConnectionClient();
186
+
187
+ // ─── FeatureConnect ───────────────────────────────────────────────────────────
188
+
189
+ export type FeatureConnect = {
190
+ on: <TData = unknown>(name: string, cb: (payload: TData) => void) => (() => void);
191
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
192
+ call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
193
+ stream: <TData = unknown>(name: string) => { subscribe: (cb: (payload: TData) => void) => (() => void) };
194
+ channel: <TData = unknown>(name: string) => {
195
+ subscribe: (cb: (payload: TData) => void) => (() => void);
196
+ publish: (payload: TData) => void;
197
+ };
198
+ scoped: (scope: string) => FeatureConnect;
199
+ };
200
+
201
+ function _scopeName(scope: string, name: string): string {
202
+ return name.startsWith(`${scope}.`) ? name : `${scope}.${name}`;
203
+ }
204
+
205
+ async function _invokeScoped(method: string, payload?: unknown): Promise<unknown> {
206
+ const host = globalThis as any;
207
+ if (typeof host.__invoke__ !== 'function') return undefined;
208
+ return host.__invoke__(method, payload === undefined ? [] : [payload]);
209
+ }
210
+
211
+ export function createFeatureConnect(scope: string): FeatureConnect {
212
+ return {
213
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn) {
214
+ const s = _scopeName(scope, name);
215
+ void _invokeScoped(s, payload);
216
+ _client.fire(s, payload);
217
+ if (typeof window !== 'undefined') {
218
+ window.dispatchEvent(new CustomEvent(`plusui:${s}`, { detail: payload }));
219
+ }
220
+ },
221
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
222
+ const s = _scopeName(scope, name);
223
+ const off = _client.on<TData>(s, cb);
224
+ if (typeof window === 'undefined') return off;
225
+ const dom = (e: Event) => cb((e as CustomEvent<TData>).detail);
226
+ window.addEventListener(`plusui:${s}`, dom as EventListener);
227
+ return () => { off(); window.removeEventListener(`plusui:${s}`, dom as EventListener); };
228
+ },
229
+ call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
230
+ const s = _scopeName(scope, name);
231
+ const host = globalThis as any;
232
+ if (typeof host.__invoke__ === 'function') return _invokeScoped(s, payload) as Promise<TOut>;
233
+ return _client.call<TOut, TIn>(s, payload);
234
+ },
235
+ stream<TData = unknown>(name: string) { return _client.stream<TData>(_scopeName(scope, name)); },
236
+ channel<TData = unknown>(name: string) { return _client.channel<TData>(_scopeName(scope, name)); },
237
+ scoped: (child: string) => createFeatureConnect(_scopeName(scope, child)),
238
+ };
239
+ }
240
+
241
+ // ─── connect — custom channels (your app-specific messages) ──────────────────
242
+ export const connect = {
243
+ /** Send a message to C++ backend */
244
+ emit<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
245
+ _client.fire(name, payload);
246
+ },
247
+ /** Listen for messages from C++ backend. Returns unsubscribe fn. */
248
+ on<TData = unknown>(name: string, cb: (payload: TData) => void): () => void {
249
+ return _client.on<TData>(name, cb);
250
+ },
251
+ /** Scoped feature connection (auto-prefixes names) */
252
+ feature: createFeatureConnect,
253
+ };
254
+
255
+ /** Advanced: raw connection client — call / stream / channel */
256
+ export const connection = _client;
257
+
258
+ // ─── win — window management ──────────────────────────────────────────────────
259
+ const _winEvents = createFeatureConnect('window');
260
+
73
261
  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>,
262
+ minimize: async () => invoke('window.minimize', []),
263
+ maximize: async () => invoke('window.maximize', []),
264
+ show: async () => invoke('window.show', []),
265
+ hide: async () => invoke('window.hide', []),
266
+ close: async () => invoke('window.close', []),
267
+ center: async () => invoke('window.center', []),
268
+ setTitle: async (title: string) => invoke('window.setTitle', [title]),
269
+ setSize: async (w: number, h: number) => invoke('window.setSize', [w, h]),
270
+ setMinSize: async (w: number, h: number) => invoke('window.setMinSize', [w, h]),
271
+ setMaxSize: async (w: number, h: number) => invoke('window.setMaxSize', [w, h]),
272
+ setPosition: async (x: number, y: number) => invoke('window.setPosition', [x, y]),
273
+ setAlwaysOnTop: async (v: boolean) => invoke('window.setAlwaysOnTop', [v]),
274
+ setFullscreen: async (v: boolean) => invoke('window.setFullscreen', [v]),
275
+ setOpacity: async (v: number) => invoke('window.setOpacity', [v]),
276
+ getSize: async (): Promise<WindowSize> => invoke('window.getSize', []) as Promise<WindowSize>,
277
+ getPosition: async (): Promise<WindowPosition> => invoke('window.getPosition', []) as Promise<WindowPosition>,
278
+ isMaximized: async (): Promise<boolean> => invoke('window.isMaximized', []) as Promise<boolean>,
279
+ isVisible: async (): Promise<boolean> => invoke('window.isVisible', []) as Promise<boolean>,
280
+ on: _winEvents.on.bind(_winEvents),
281
+ emit: _winEvents.emit.bind(_winEvents),
80
282
  };
81
283
 
284
+ // ─── browser ──────────────────────────────────────────────────────────────────
285
+ const _browserEvents = createFeatureConnect('browser');
286
+
82
287
  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>,
288
+ getUrl: async (): Promise<string> => invoke('browser.getUrl', []) as Promise<string>,
289
+ navigate: async (url: string) => invoke('browser.navigate', [url]),
290
+ goBack: async () => invoke('browser.goBack', []),
291
+ goForward: async () => invoke('browser.goForward', []),
292
+ reload: async () => invoke('browser.reload', []),
293
+ canGoBack: async (): Promise<boolean> => invoke('browser.canGoBack', []) as Promise<boolean>,
88
294
  canGoForward: async (): Promise<boolean> => invoke('browser.canGoForward', []) as Promise<boolean>,
89
295
  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);
296
+ if (typeof window === 'undefined') return () => {};
297
+ const h = (e: Event) => handler((e as CustomEvent<{ url?: string }>).detail?.url ?? '');
298
+ window.addEventListener('plusui:navigate', h);
299
+ return () => window.removeEventListener('plusui:navigate', h);
102
300
  },
301
+ on: _browserEvents.on.bind(_browserEvents),
302
+ emit: _browserEvents.emit.bind(_browserEvents),
103
303
  };
104
304
 
305
+ // ─── router ───────────────────────────────────────────────────────────────────
105
306
  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
- },
307
+ setRoutes: (routes: RouteMap) => { _routes = routes; },
308
+ push: async (path: string) => invoke('browser.navigate', [_routes[path] ?? path]),
113
309
  };
114
310
 
311
+ // ─── app ──────────────────────────────────────────────────────────────────────
312
+ const _appEvents = createFeatureConnect('app');
313
+
115
314
  export const app = {
116
- quit: async () => invoke('app.quit', []),
315
+ quit: async () => invoke('app.quit', []),
316
+ on: _appEvents.on.bind(_appEvents),
317
+ emit: _appEvents.emit.bind(_appEvents),
117
318
  };
118
319
 
119
- // FileDrop API
120
- export interface FileInfo {
121
- path: string;
122
- name: string;
123
- type: string;
124
- size: number;
125
- }
320
+ // ─── clipboard ────────────────────────────────────────────────────────────────
321
+ const _clipboardEvents = createFeatureConnect('clipboard');
322
+
323
+ export const clipboard = {
324
+ getText: async (): Promise<string> => invoke('clipboard.getText', []) as Promise<string>,
325
+ setText: async (text: string) => invoke('clipboard.setText', [text]),
326
+ clear: async () => invoke('clipboard.clear', []),
327
+ hasText: async (): Promise<boolean> => invoke('clipboard.hasText', []) as Promise<boolean>,
328
+ on: _clipboardEvents.on.bind(_clipboardEvents),
329
+ emit: _clipboardEvents.emit.bind(_clipboardEvents),
330
+ };
331
+
332
+ // ─── fileDrop ─────────────────────────────────────────────────────────────────
333
+ export interface FileInfo { path: string; name: string; type: string; size: number; }
126
334
 
127
335
  export const fileDrop = {
128
336
  setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
129
- isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
337
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
130
338
  onFilesDropped: (handler: (files: FileInfo[]) => void) => {
131
339
  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);
340
+ const h = (e: Event) => handler((e as CustomEvent<{ files?: FileInfo[] }>).detail?.files ?? []);
341
+ window.addEventListener('plusui:fileDrop.filesDropped', h);
342
+ return () => window.removeEventListener('plusui:fileDrop.filesDropped', h);
138
343
  },
139
344
  onDragEnter: (handler: () => void) => {
140
345
  if (typeof window === 'undefined') return () => {};
141
- const eventHandler = () => handler();
142
- window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
143
- return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
346
+ window.addEventListener('plusui:fileDrop.dragEnter', handler);
347
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', handler);
144
348
  },
145
349
  onDragLeave: (handler: () => void) => {
146
350
  if (typeof window === 'undefined') return () => {};
147
- const eventHandler = () => handler();
148
- window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
149
- return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
351
+ window.addEventListener('plusui:fileDrop.dragLeave', handler);
352
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', handler);
150
353
  },
151
354
  };
152
355
 
153
- // Helper functions
356
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
154
357
  export function formatFileSize(bytes: number): string {
155
358
  if (bytes === 0) return '0 Bytes';
156
359
  const k = 1024;
@@ -159,6 +362,41 @@ export function formatFileSize(bytes: number): string {
159
362
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
160
363
  }
161
364
 
162
- export function isImageFile(file: FileInfo): boolean {
163
- return file.type.startsWith('image/');
164
- }
365
+ export function isImageFile(file: FileInfo): boolean { return file.type.startsWith('image/'); }
366
+
367
+ // ─── Top-level on / emit ─────────────────────────────────────────────────────
368
+ //
369
+ // Custom app events — import directly, no wrapper prefix:
370
+ //
371
+ // import { on, emit } from 'plusui';
372
+ //
373
+ // emit('myEvent', { value: 42 }); // TS → C++
374
+ // on('myEvent', (data) => { ... }); // C++ → TS
375
+ //
376
+ // Framework features use their own namespace:
377
+ //
378
+ // import { win, clipboard } from 'plusui';
379
+ //
380
+ // win.minimize();
381
+ // clipboard.on('changed', (data) => { ... });
382
+ //
383
+ export const on = connect.on.bind(connect) as typeof connect.on;
384
+ export const emit = connect.emit.bind(connect) as typeof connect.emit;
385
+
386
+ // ─── Default export — everything under one roof ───────────────────────────────
387
+ const plusui = {
388
+ /** Create a named custom scope: const search = feature('search'); search.on/emit(...) */
389
+ feature: createFeatureConnect,
390
+ connection,
391
+ win,
392
+ browser,
393
+ router,
394
+ app,
395
+ clipboard,
396
+ fileDrop,
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),