plusui-native 0.2.64 → 0.2.66

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.64",
3
+ "version": "0.2.66",
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.63",
31
- "plusui-native-connect": "^0.1.63"
30
+ "plusui-native-builder": "^0.1.65",
31
+ "plusui-native-connect": "^0.1.65"
32
32
  },
33
33
  "peerDependencies": {
34
- "plusui-native-connect": "^0.1.63"
34
+ "plusui-native-connect": "^0.1.65"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -105,11 +105,20 @@ export class EnvironmentDoctor {
105
105
  for (const result of fixResults) {
106
106
  if (result.success) {
107
107
  console.log(chalk.green(`✓ ${result.name} installed successfully`));
108
+ if (result.pathRefreshNeeded) {
109
+ console.log(chalk.gray(' Note: open a new terminal for the updated PATH to take effect'));
110
+ }
108
111
  } else {
109
112
  console.log(chalk.red(`✗ ${result.name} installation failed`));
110
113
  if (result.message) {
111
114
  console.log(chalk.gray(` ${result.message}`));
112
115
  }
116
+ if (result.reason) {
117
+ console.log(chalk.gray(` Reason: ${result.reason}`));
118
+ }
119
+ if (result.downloadUrl) {
120
+ console.log(chalk.yellow(` Download manually: ${result.downloadUrl}`));
121
+ }
113
122
  if (result.instructions) {
114
123
  console.log(chalk.yellow('\n Manual installation required:'));
115
124
  result.instructions.forEach(line => {
@@ -9,7 +9,9 @@ async function tryCommand(command, timeout = 30000) {
9
9
  }).trim();
10
10
  return { success: true, output };
11
11
  } catch (error) {
12
- return { success: false, error };
12
+ const stderr = (error.stderr || '').toString().trim();
13
+ const stdout = (error.stdout || '').toString().trim();
14
+ return { success: false, error, stderr, stdout };
13
15
  }
14
16
  }
15
17
 
@@ -20,10 +22,14 @@ async function checkWinget() {
20
22
 
21
23
  const INSTALL_COMMANDS = {
22
24
  cmake: {
23
- command: 'winget install -e --id Kitware.CMake --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
25
+ // NOTE: Kitware.CMake does NOT support --scope user; omit it so winget
26
+ // uses the package's default (machine) scope. winget will request UAC
27
+ // elevation automatically when needed.
28
+ command: 'winget install -e --id Kitware.CMake --accept-package-agreements --accept-source-agreements --disable-interactivity',
24
29
  manual: 'https://cmake.org/download/',
25
30
  name: 'CMake',
26
- timeout: 300000
31
+ timeout: 300000,
32
+ requiresElevation: true
27
33
  },
28
34
  nodejs: {
29
35
  command: 'winget install -e --id OpenJS.NodeJS --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
@@ -91,18 +97,39 @@ export async function installTool(toolName) {
91
97
  return {
92
98
  success: true,
93
99
  message: `${tool.name} installed successfully`,
94
- output: result.output
100
+ output: result.output,
101
+ // Remind callers that the current process PATH won't include the new
102
+ // install – they should re-spawn or check known filesystem paths.
103
+ pathRefreshNeeded: true
95
104
  };
96
105
  }
97
106
 
98
- return {
107
+ // Build a human-readable reason from winget's own output
108
+ const reason = result.stderr || result.stdout ||
109
+ (result.error && result.error.message) ||
110
+ 'Unknown error';
111
+
112
+ const failResult = {
99
113
  success: false,
100
114
  autoInstallAvailable: true,
101
115
  error: result.error,
102
116
  message: `Failed to install ${tool.name} automatically`,
117
+ reason,
103
118
  manual: tool.manual,
104
119
  downloadUrl: tool.manual
105
120
  };
121
+
122
+ if (tool.requiresElevation) {
123
+ failResult.instructions = [
124
+ 'This tool requires administrator rights to install system-wide.',
125
+ 'Option 1: Re-run from an elevated (Run as Administrator) terminal:',
126
+ ' plusui doctor --fix',
127
+ `Option 2: Install manually via winget (elevated): ${tool.command}`,
128
+ `Option 3: Download from: ${tool.manual}`
129
+ ];
130
+ }
131
+
132
+ return failResult;
106
133
  }
107
134
 
108
135
  export function getInstallInstructions(toolName) {
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';
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect } from 'react';
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';
2
+ import plusui from 'plusui';
3
+ // Generated by `plusui connect` — channel objects auto-created from name.on / name.emit usage:
4
+ import { customFileDrop } from '../Connections/connections.gen';
5
5
 
6
6
  // Define routes for your app (optional - for SPA routing)
7
7
  const routes = {
@@ -17,11 +17,10 @@ function App() {
17
17
  const [canGoBack, setCanGoBack] = useState(false);
18
18
  const [canGoForward, setCanGoForward] = useState(false);
19
19
 
20
- // FileDrop state
21
- const [files, setFiles] = useState<FileInfo[]>([]);
20
+ // customFileDrop connect channel state
22
21
  const [isDragging, setIsDragging] = useState(false);
23
- const [dropZoneStyle, setDropZoneStyle] = useState('');
24
- const [dropAccent] = useState('#60a5fa');
22
+ const [droppedFiles, setDroppedFiles] = useState<{ name: string; size: number; type: string }[]>([]);
23
+ const [backendMsg, setBackendMsg] = useState<string | null>(null);
25
24
 
26
25
  useEffect(() => {
27
26
  plusui.win.show().catch(() => {
@@ -38,21 +37,11 @@ function App() {
38
37
  plusui.browser.canGoForward().then(setCanGoForward);
39
38
  });
40
39
 
41
- // Setup FileDrop listeners
42
- const unsubDrop = plusui.fileDrop.onFilesDropped((droppedFiles) => {
43
- console.log('Files dropped:', droppedFiles);
44
- setFiles(prev => [...prev, ...droppedFiles]);
45
- setIsDragging(false);
40
+ // Listen for responses emitted from C++ via ch.customFileDrop.emit(...) in main.cpp
41
+ const unsubChannel = customFileDrop.on((data: any) => {
42
+ setBackendMsg(data?.message ?? JSON.stringify(data));
46
43
  });
47
-
48
- const unsubEnter = plusui.fileDrop.onDragEnter(() => {
49
- setIsDragging(true);
50
- });
51
-
52
- const unsubLeave = plusui.fileDrop.onDragLeave(() => {
53
- setIsDragging(false);
54
- });
55
-
44
+
56
45
  // Get initial state
57
46
  plusui.browser.getUrl().then(setCurrentUrl);
58
47
  plusui.browser.canGoBack().then(setCanGoBack);
@@ -60,9 +49,7 @@ function App() {
60
49
 
61
50
  return () => {
62
51
  unsub();
63
- unsubDrop();
64
- unsubEnter();
65
- unsubLeave();
52
+ unsubChannel();
66
53
  };
67
54
  }, []);
68
55
 
@@ -90,6 +77,22 @@ function App() {
90
77
  // App control
91
78
  const handleQuit = async () => await plusui.app.quit();
92
79
 
80
+ // customFileDrop drag handlers — use HTML5 drag events to collect files, then
81
+ // emit them to the C++ backend via the connect channel
82
+ const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); };
83
+ const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); };
84
+ const handleDrop = (e: React.DragEvent) => {
85
+ e.preventDefault();
86
+ setIsDragging(false);
87
+ const items = Array.from(e.dataTransfer.files).map(f => ({
88
+ name: f.name, size: f.size, type: f.type || 'unknown',
89
+ }));
90
+ setDroppedFiles(items);
91
+ setBackendMsg(null);
92
+ // Emit to C++ — ch.customFileDrop.on() in main.cpp receives this
93
+ customFileDrop.emit({ files: items });
94
+ };
95
+
93
96
  return (
94
97
  <div className="app">
95
98
  <header className="app-header">
@@ -154,67 +157,57 @@ function App() {
154
157
  </div>
155
158
 
156
159
  <div className="card">
157
- <h2>FileDrop - Drag & Drop Files</h2>
160
+ <h2>Custom Channel File Drop</h2>
158
161
  <p style={{ fontSize: '0.85em', color: '#aaa', marginBottom: '1rem' }}>
159
- Native file drop is enabled. The webview's default drag & drop behavior is disabled
160
- to prevent files from opening in the browser, allowing your FileDrop handlers to work.
162
+ Drop files below. The frontend calls <code>customFileDrop.emit()</code> to send file
163
+ info to C++. The C++ handler calls <code>ch.customFileDrop.emit()</code> to reply.
164
+ The frontend receives the reply via <code>customFileDrop.on()</code>. Run{' '}
165
+ <code>plusui connect</code> to regenerate the channel bindings from both sides.
161
166
  </p>
162
167
 
163
- <div className="filedrop-control-row">
164
- <label className="filedrop-control-label">Style:</label>
165
- <select
166
- value={dropZoneStyle}
167
- onChange={(e) => setDropZoneStyle(e.target.value)}
168
- className="filedrop-control-select"
169
- >
170
- <option value="">Default</option>
171
- <option value="filedrop-compact">Compact</option>
172
- <option value="filedrop-inline">Inline</option>
173
- <option value="filedrop-minimal">Minimal</option>
174
- <option value="filedrop-bold">Bold</option>
175
- </select>
176
- </div>
177
-
178
168
  <div
179
- className={`filedrop-zone ${dropZoneStyle} ${isDragging ? 'filedrop-active' : ''}`}
180
- data-dropzone="true"
181
- data-filedrop-state={isDragging ? 'dragging' : 'idle'}
182
- data-filedrop-style={dropZoneStyle.replace('filedrop-', '') || 'default'}
183
- style={{ '--filedrop-accent': dropAccent } as React.CSSProperties}
169
+ className={`filedrop-zone ${isDragging ? 'filedrop-active' : ''}`}
170
+ onDragOver={handleDragOver}
171
+ onDragEnter={handleDragOver}
172
+ onDragLeave={handleDragLeave}
173
+ onDrop={handleDrop}
184
174
  >
185
175
  <div className="filedrop-content">
186
176
  <svg className="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
187
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
177
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
188
178
  d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
189
179
  </svg>
190
180
  <div className="filedrop-text">
191
- {isDragging ? 'Drop files here' : 'Drag & drop files'}
181
+ {isDragging ? 'Drop files here' : 'Drag & drop files to send to C++'}
192
182
  </div>
193
- <div className="filedrop-hint">All file types supported</div>
194
183
  </div>
195
184
  </div>
196
185
 
197
- {files.length > 0 && (
198
- <div className="filedrop-files">
199
- {files.map((file, i) => (
186
+ {droppedFiles.length > 0 && (
187
+ <div className="filedrop-files" style={{ marginTop: '1rem' }}>
188
+ <p style={{ fontSize: '0.8em', color: '#888', marginBottom: '0.5rem' }}>
189
+ Sent to C++ via <code>customFileDrop.emit()</code>:
190
+ </p>
191
+ {droppedFiles.map((file, i) => (
200
192
  <div key={i} className="filedrop-file-item">
201
193
  <div className="filedrop-file-icon">📄</div>
202
194
  <div className="filedrop-file-info">
203
195
  <div className="filedrop-file-name">{file.name}</div>
204
- <div className="filedrop-file-meta">
205
- {plusui.formatFileSize(file.size)} • {file.type}
206
- </div>
196
+ <div className="filedrop-file-meta">{plusui.formatFileSize(file.size)} • {file.type}</div>
207
197
  </div>
208
- <button
209
- className="filedrop-file-remove"
210
- onClick={() => setFiles(files.filter((_, idx) => idx !== i))}
211
- >
212
-
213
- </button>
214
198
  </div>
215
199
  ))}
216
200
  </div>
217
201
  )}
202
+
203
+ {backendMsg !== null && (
204
+ <div style={{ marginTop: '1rem', padding: '0.75rem 1rem', background: '#1a2e1a', border: '1px solid #2d5a2d', borderRadius: '6px' }}>
205
+ <p style={{ fontSize: '0.8em', color: '#4caf50', margin: 0 }}>
206
+ ✓ C++ replied via <code>ch.customFileDrop.emit()</code>:
207
+ </p>
208
+ <p style={{ fontSize: '0.9em', color: '#e0e0e0', margin: '0.4rem 0 0' }}>{backendMsg}</p>
209
+ </div>
210
+ )}
218
211
  </div>
219
212
 
220
213
  <div className="info">
@@ -184,14 +184,25 @@ int main() {
184
184
  Connections ch(connect); // use ch.name.on() / ch.name.emit()
185
185
 
186
186
  // ========================================
187
- // FILE DROP EVENTS (Native Drag & Drop API)
187
+ // CUSTOM FILE DROP CHANNEL
188
188
  // ========================================
189
- // Native FileDrop API - handles OS-level file drag-and-drop.
190
- // Frontend dragEnter/dragLeave events are dispatched from injected WebView script.
191
- mainWindow.onFileDrop([](const std::string& data) {
192
- std::cout << "Files dropped: " << data << std::endl;
193
- // Parse the JSON data to get file info
194
- // You can process files here in C++
189
+ // customFileDrop is a connect channel detected by `plusui connect`.
190
+ // Frontend drops files emits via customFileDrop.emit({ files: [...] })
191
+ // C++ receives here, processes, then emits back to the frontend.
192
+ // Frontend receives the reply via customFileDrop.on() in App.tsx.
193
+ ch.customFileDrop.on([&ch](const json& payload) {
194
+ auto files = payload.value("files", json::array());
195
+ int count = static_cast<int>(files.size());
196
+ std::cout << "customFileDrop: received " << count << " file(s) from frontend" << std::endl;
197
+ for (const auto& f : files) {
198
+ std::cout << " - " << f.value("name", "?") << " (" << f.value("size", 0) << " bytes)" << std::endl;
199
+ }
200
+ // Reply back to frontend — received by customFileDrop.on() in App.tsx
201
+ ch.customFileDrop.emit({
202
+ {"processed", true},
203
+ {"count", count},
204
+ {"message", "C++ received " + std::to_string(count) + " file(s)"}
205
+ });
195
206
  });
196
207
 
197
208
  // ========================================
@@ -1,7 +1,7 @@
1
1
  import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
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';
2
+ import plusui from 'plusui';
3
+ // Generated by `plusui connect` — channel objects auto-created from name.on / name.emit usage:
4
+ import { customFileDrop } from '../Connections/connections.gen';
5
5
 
6
6
  // Define routes for your app (optional - for SPA routing)
7
7
  const routes = {
@@ -17,11 +17,10 @@ function App() {
17
17
  const [canGoBack, setCanGoBack] = createSignal(false);
18
18
  const [canGoForward, setCanGoForward] = createSignal(false);
19
19
 
20
- // FileDrop state
21
- const [files, setFiles] = createSignal<FileInfo[]>([]);
20
+ // customFileDrop connect channel state
22
21
  const [isDragging, setIsDragging] = createSignal(false);
23
- const [dropZoneStyle, setDropZoneStyle] = createSignal('');
24
- const [dropAccent] = createSignal('#60a5fa');
22
+ const [droppedFiles, setDroppedFiles] = createSignal<{ name: string; size: number; type: string }[]>([]);
23
+ const [backendMsg, setBackendMsg] = createSignal<string | null>(null);
25
24
 
26
25
  onMount(() => {
27
26
  // Setup routes
@@ -39,26 +38,11 @@ function App() {
39
38
  plusui.browser.canGoBack().then(setCanGoBack);
40
39
  plusui.browser.canGoForward().then(setCanGoForward);
41
40
 
42
- // Setup FileDrop listeners
43
- const unsubDrop = plusui.fileDrop.onFilesDropped((droppedFiles) => {
44
- console.log('Files dropped:', droppedFiles);
45
- setFiles(prev => [...prev, ...droppedFiles]);
46
- setIsDragging(false);
47
- });
48
-
49
- const unsubEnter = plusui.fileDrop.onDragEnter(() => {
50
- setIsDragging(true);
51
- });
52
-
53
- const unsubLeave = plusui.fileDrop.onDragLeave(() => {
54
- setIsDragging(false);
55
- });
56
-
57
- onCleanup(() => {
58
- unsubDrop();
59
- unsubEnter();
60
- unsubLeave();
41
+ // Listen for responses emitted from C++ via ch.customFileDrop.emit(...) in main.cpp
42
+ const unsub = customFileDrop.on((data: any) => {
43
+ setBackendMsg(data?.message ?? JSON.stringify(data));
61
44
  });
45
+ onCleanup(() => unsub());
62
46
  });
63
47
 
64
48
  const handleMinimize = async () => await plusui.win.minimize();
@@ -85,6 +69,22 @@ function App() {
85
69
  // App control
86
70
  const handleQuit = async () => await plusui.app.quit();
87
71
 
72
+ // customFileDrop drag handlers — use HTML5 drag events to collect files, then
73
+ // emit them to the C++ backend via the connect channel
74
+ const handleDragOver = (e: DragEvent) => { e.preventDefault(); setIsDragging(true); };
75
+ const handleDragLeave = (e: DragEvent) => { e.preventDefault(); setIsDragging(false); };
76
+ const handleDrop = (e: DragEvent) => {
77
+ e.preventDefault();
78
+ setIsDragging(false);
79
+ const items = Array.from(e.dataTransfer?.files ?? []).map(f => ({
80
+ name: f.name, size: f.size, type: f.type || 'unknown',
81
+ }));
82
+ setDroppedFiles(items);
83
+ setBackendMsg(null);
84
+ // Emit to C++ — ch.customFileDrop.on() in main.cpp receives this
85
+ customFileDrop.emit({ files: items });
86
+ };
87
+
88
88
  return (
89
89
  <div class="app">
90
90
  <header class="app-header">
@@ -149,69 +149,59 @@ function App() {
149
149
  </div>
150
150
 
151
151
  <div class="card">
152
- <h2>FileDrop - Drag & Drop Files</h2>
152
+ <h2>Custom Channel File Drop</h2>
153
153
  <p style={{ 'font-size': '0.85em', color: '#aaa', 'margin-bottom': '1rem' }}>
154
- Native file drop is enabled. The webview's default drag & drop behavior is disabled
155
- to prevent files from opening in the browser, allowing your FileDrop handlers to work.
154
+ Drop files below. The frontend calls <code>customFileDrop.emit()</code> to send file
155
+ info to C++. The C++ handler calls <code>ch.customFileDrop.emit()</code> to reply.
156
+ The frontend receives the reply via <code>customFileDrop.on()</code>. Run{' '}
157
+ <code>plusui connect</code> to regenerate the channel bindings from both sides.
156
158
  </p>
157
159
 
158
- <div class="filedrop-control-row">
159
- <label class="filedrop-control-label">Style:</label>
160
- <select
161
- value={dropZoneStyle()}
162
- onChange={(e) => setDropZoneStyle(e.target.value)}
163
- class="filedrop-control-select"
164
- >
165
- <option value="">Default</option>
166
- <option value="filedrop-compact">Compact</option>
167
- <option value="filedrop-inline">Inline</option>
168
- <option value="filedrop-minimal">Minimal</option>
169
- <option value="filedrop-bold">Bold</option>
170
- </select>
171
- </div>
172
-
173
160
  <div
174
- class={`filedrop-zone ${dropZoneStyle()} ${isDragging() ? 'filedrop-active' : ''}`}
175
- data-dropzone="true"
176
- data-filedrop-state={isDragging() ? 'dragging' : 'idle'}
177
- data-filedrop-style={dropZoneStyle().replace('filedrop-', '') || 'default'}
178
- style={{ '--filedrop-accent': dropAccent() }}
161
+ class={`filedrop-zone ${isDragging() ? 'filedrop-active' : ''}`}
162
+ onDragOver={handleDragOver}
163
+ onDragEnter={handleDragOver}
164
+ onDragLeave={handleDragLeave}
165
+ onDrop={handleDrop}
179
166
  >
180
167
  <div class="filedrop-content">
181
168
  <svg class="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
182
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width={2}
169
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width={2}
183
170
  d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
184
171
  </svg>
185
172
  <div class="filedrop-text">
186
- {isDragging() ? 'Drop files here' : 'Drag & drop files'}
173
+ {isDragging() ? 'Drop files here' : 'Drag & drop files to send to C++'}
187
174
  </div>
188
- <div class="filedrop-hint">All file types supported</div>
189
175
  </div>
190
176
  </div>
191
177
 
192
- <Show when={files().length > 0}>
193
- <div class="filedrop-files">
194
- <For each={files()}>
195
- {(file, i) => (
178
+ <Show when={droppedFiles().length > 0}>
179
+ <div class="filedrop-files" style={{ 'margin-top': '1rem' }}>
180
+ <p style={{ 'font-size': '0.8em', color: '#888', 'margin-bottom': '0.5rem' }}>
181
+ Sent to C++ via <code>customFileDrop.emit()</code>:
182
+ </p>
183
+ <For each={droppedFiles()}>
184
+ {(file) => (
196
185
  <div class="filedrop-file-item">
197
186
  <div class="filedrop-file-icon">📄</div>
198
187
  <div class="filedrop-file-info">
199
188
  <div class="filedrop-file-name">{file.name}</div>
200
- <div class="filedrop-file-meta">
201
- {plusui.formatFileSize(file.size)} • {file.type}
202
- </div>
189
+ <div class="filedrop-file-meta">{plusui.formatFileSize(file.size)} • {file.type}</div>
203
190
  </div>
204
- <button
205
- class="filedrop-file-remove"
206
- onClick={() => setFiles(files().filter((_, idx) => idx !== i()))}
207
- >
208
-
209
- </button>
210
191
  </div>
211
192
  )}
212
193
  </For>
213
194
  </div>
214
195
  </Show>
196
+
197
+ <Show when={backendMsg() !== null}>
198
+ <div style={{ 'margin-top': '1rem', padding: '0.75rem 1rem', background: '#1a2e1a', border: '1px solid #2d5a2d', 'border-radius': '6px' }}>
199
+ <p style={{ 'font-size': '0.8em', color: '#4caf50', margin: '0' }}>
200
+ ✓ C++ replied via <code>ch.customFileDrop.emit()</code>:
201
+ </p>
202
+ <p style={{ 'font-size': '0.9em', color: '#e0e0e0', margin: '0.4rem 0 0' }}>{backendMsg()}</p>
203
+ </div>
204
+ </Show>
215
205
  </div>
216
206
 
217
207
  <div class="info">
@@ -178,14 +178,25 @@ int main() {
178
178
  Connections ch(connect); // use ch.name.on() / ch.name.emit()
179
179
 
180
180
  // ========================================
181
- // FILE DROP EVENTS (Native Drag & Drop API)
181
+ // CUSTOM FILE DROP CHANNEL
182
182
  // ========================================
183
- // Native FileDrop API - handles OS-level file drag-and-drop.
184
- // Frontend dragEnter/dragLeave events are dispatched from injected WebView script.
185
- mainWindow.onFileDrop([](const std::string& data) {
186
- std::cout << "Files dropped: " << data << std::endl;
187
- // Parse the JSON data to get file info
188
- // You can process files here in C++
183
+ // customFileDrop is a connect channel detected by `plusui connect`.
184
+ // Frontend drops files emits via customFileDrop.emit({ files: [...] })
185
+ // C++ receives here, processes, then emits back to the frontend.
186
+ // Frontend receives the reply via customFileDrop.on() in App.tsx.
187
+ ch.customFileDrop.on([&ch](const json& payload) {
188
+ auto files = payload.value("files", json::array());
189
+ int count = static_cast<int>(files.size());
190
+ std::cout << "customFileDrop: received " << count << " file(s) from frontend" << std::endl;
191
+ for (const auto& f : files) {
192
+ std::cout << " - " << f.value("name", "?") << " (" << f.value("size", 0) << " bytes)" << std::endl;
193
+ }
194
+ // Reply back to frontend — received by customFileDrop.on() in App.tsx
195
+ ch.customFileDrop.emit({
196
+ {"processed", true},
197
+ {"count", count},
198
+ {"message", "C++ received " + std::to_string(count) + " file(s)"}
199
+ });
189
200
  });
190
201
 
191
202
  // ========================================