plusui-native-core 0.1.103 → 0.1.105

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,76 +1,44 @@
1
1
  /**
2
- * PlusUI Connect API — Semantic Frontend ↔ Backend Communication
2
+ * PlusUI Connect API — Custom Frontend ↔ Backend Communication
3
3
  *
4
- * ZERO CONFIG. SEMANTIC SYNTAX. ALL 5 PATTERNS.
5
- *
6
- * IMPORTANT: `connect` is for CUSTOM user-defined communication only!
7
- * Built-in features (window, clipboard, app, etc.) use direct imports:
8
- * import plusui from 'plusui';
9
- * plusui.window.minimize();
10
- * plusui.clipboard.setText('hello');
11
- *
12
- * For custom communication, use `connect.namespace.method()`:
4
+ * Use `connect` for YOUR custom communication channels only!
13
5
  *
14
6
  * import { connect } from 'plusui';
15
7
  *
16
- * // Request/Response — await a call to the backend
8
+ * // Request/Response
17
9
  * const user = await connect.user.fetch(123);
18
10
  *
19
- * // Fire & Forget one-way notification
11
+ * // Fire & Forget (one-way)
20
12
  * connect.files.upload({ file: myFile });
21
13
  *
22
- * // Event Listener — listen for backend events
14
+ * // Subscribe to events
23
15
  * connect.app.onNotify((msg) => console.log(msg));
24
16
  *
25
- * // Register a handler backend can call frontend
17
+ * // Register handler (backend calls frontend)
26
18
  * connect.ui.handlePrompt = async (data) => {
27
19
  * return await Dialog.confirm(data.msg);
28
20
  * };
29
21
  *
30
- * Then run `plusui connect` to generate typed bindings.
22
+ * Run `plusui connect` to generate typed bindings.
31
23
  *
32
- * Pattern detection is automatic:
33
- * - `await connect.x.y()` → CALL (Request/Response)
34
- * - `connect.x.onY(cb)` → EVENT (Subscribe)
35
- * - `connect.x.handleY = fn` → HANDLER (Backend calls frontend)
36
- * - `connect.x.y()` → FIRE (Simplex, one-way)
24
+ * BUILT-IN FEATURES use direct imports:
25
+ * import plusui from 'plusui';
26
+ * plusui.window.minimize();
27
+ * plusui.clipboard.setText('hello');
37
28
  */
38
29
 
39
30
  import { connect, _client, createFeatureConnect } from '../Connection/connect';
40
31
  import type { FeatureConnect, ConnectionKind, ConnectionEnvelope } from '../Connection/connect';
41
32
 
42
- /**
43
- * connect Semantic channel proxy
44
- *
45
- * Usage:
46
- * connect.namespace.method(...args)
47
- *
48
- * The proxy routes to the correct wire format automatically:
49
- * connect.user.fetch(123) → _client.call('user.fetch', 123)
50
- * connect.app.onNotify(cb) → _client.on('app.onNotify', cb)
51
- * connect.ui.handlePrompt = fn → _client.handle('ui.handlePrompt', fn)
52
- * connect.system.minimize() → _client.fire('system.minimize', {})
53
- *
54
- * Works dynamically even before running `plusui connect`.
55
- * After running `plusui connect`, you get IDE autocomplete.
56
- */
57
- export { connect };
58
-
59
- /**
60
- * _client — Low-level connection client
61
- *
62
- * Direct access to the underlying client for advanced use cases.
63
- * Prefer using `connect.namespace.method()` for normal communication.
64
- */
65
- export { _client };
33
+ export { connect, _client, createFeatureConnect };
34
+ export type { FeatureConnect, ConnectionKind, ConnectionEnvelope };
66
35
 
67
36
  /**
68
- * createFeatureConnect Scoped connect for framework internals
69
- *
70
- * Creates a connect API scoped to a feature namespace.
71
- * Used internally by PlusUI features like window, clipboard, etc.
37
+ * Low-level client methods (use `connect` instead when possible)
72
38
  */
73
- export { createFeatureConnect };
74
-
75
- export type { FeatureConnect, ConnectionKind, ConnectionEnvelope };
76
- export default connect;
39
+ export type ClientAPI = {
40
+ call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
41
+ fire: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
42
+ on: <TData = unknown>(name: string, callback: (payload: TData) => void) => () => void;
43
+ handle: (name: string, handler: (...args: any[]) => any) => () => void;
44
+ };
@@ -1,2 +1,13 @@
1
+ export {
2
+ fileDrop,
3
+ createDropZone,
4
+ formatFileSize,
5
+ filterFilesByExtension,
6
+ filterFilesByMimeType,
7
+ isImageFile,
8
+ isVideoFile,
9
+ isAudioFile,
10
+ isTextFile
11
+ } from '../FileDrop/filedrop';
12
+
1
13
  export type { FileInfo, DropZone } from '../FileDrop/filedrop';
2
- export { fileDrop, formatFileSize, createDropZone } from '../FileDrop/filedrop';
@@ -10,7 +10,7 @@ export { router } from './router-api';
10
10
  export { keyboard, KeyCode, KeyMod } from './keyboard-api';
11
11
  export { tray } from './tray-api';
12
12
  export { display } from './display-api';
13
- export { fileDrop, formatFileSize, createDropZone } from './filedrop-api';
13
+ export { fileDrop, formatFileSize, createDropZone, filterFilesByExtension, filterFilesByMimeType, isImageFile, isVideoFile, isAudioFile, isTextFile } from './filedrop-api';
14
14
  export { menu } from './menu-api';
15
15
  export { gpu, GPUBufferUsage, GPUTextureUsage, GPUMapMode, GPUShaderStage, GPUColorWrite } from './webgpu-api';
16
16
 
@@ -77,38 +77,38 @@ App::Builder &App::Builder::transparent(bool transparent) {
77
77
  App::Builder &App::Builder::decorations(bool decorations) {
78
78
  config.decorations = decorations;
79
79
  return *this;
80
- }
81
- App::Builder &App::Builder::skipTaskbar(bool skip) {
82
- config.skipTaskbar = skip;
83
- return *this;
84
- }
85
- App::Builder &App::Builder::scrollbars(bool show) {
86
- config.scrollbars = show;
87
- return *this;
88
- }
89
- App::Builder &App::Builder::enableFileDrop(bool enable) {
90
- config.enableFileDrop = enable;
91
- return *this;
92
- }
93
-
94
- Window App::Builder::build() {
95
- WindowConfig winConfig;
96
- winConfig.title = config.title;
97
- winConfig.width = config.width;
98
- winConfig.height = config.height;
99
- winConfig.resizable = config.resizable;
100
- winConfig.alwaysOnTop = config.alwaysOnTop;
101
- winConfig.center = config.centered;
102
- winConfig.transparent = config.transparent;
103
- winConfig.decorations = config.decorations;
104
- winConfig.skipTaskbar = config.skipTaskbar;
105
- winConfig.enableFileDrop = config.enableFileDrop;
106
-
107
- // WebView configuration (now part of WindowConfig)
108
- winConfig.devtools = config.devtools;
109
- winConfig.scrollbars = config.scrollbars;
110
- winConfig.disableWebviewDragDrop =
111
- config.enableFileDrop; // Auto-disable webview drag when FileDrop enabled
80
+ }
81
+ App::Builder &App::Builder::skipTaskbar(bool skip) {
82
+ config.skipTaskbar = skip;
83
+ return *this;
84
+ }
85
+ App::Builder &App::Builder::scrollbars(bool show) {
86
+ config.scrollbars = show;
87
+ return *this;
88
+ }
89
+ App::Builder &App::Builder::fileDrop(bool enable) {
90
+ config.fileDrop = enable;
91
+ return *this;
92
+ }
93
+
94
+ Window App::Builder::build() {
95
+ WindowConfig winConfig;
96
+ winConfig.title = config.title;
97
+ winConfig.width = config.width;
98
+ winConfig.height = config.height;
99
+ winConfig.resizable = config.resizable;
100
+ winConfig.alwaysOnTop = config.alwaysOnTop;
101
+ winConfig.center = config.centered;
102
+ winConfig.transparent = config.transparent;
103
+ winConfig.decorations = config.decorations;
104
+ winConfig.skipTaskbar = config.skipTaskbar;
105
+ winConfig.fileDrop = config.fileDrop;
106
+
107
+ // WebView configuration (now part of WindowConfig)
108
+ winConfig.devtools = config.devtools;
109
+ winConfig.scrollbars = config.scrollbars;
110
+ winConfig.disableWebviewDragDrop =
111
+ config.fileDrop; // Auto-disable browser drag-drop when OS file drop enabled
112
112
 
113
113
  // Create native window
114
114
  auto nativeWinPtr = std::make_shared<Window>(Window::create(winConfig));
@@ -1,3 +1,24 @@
1
+ /**
2
+ * PlusUI FileDrop API
3
+ *
4
+ * Simple API for handling OS file drops in your app.
5
+ * Enable in config: .fileDrop(true) - enabled by default
6
+ *
7
+ * Usage:
8
+ * import plusui from 'plusui';
9
+ * const { fileDrop } = plusui;
10
+ *
11
+ * // Create a drop zone
12
+ * const zone = fileDrop.createDropZone('myZone', element);
13
+ * zone.onFiles((files) => {
14
+ * console.log('Dropped:', files.map(f => f.name));
15
+ * });
16
+ *
17
+ * // Or use data-dropzone attribute
18
+ * // <div data-dropzone="upload"></div>
19
+ * fileDrop.onFiles('upload', (files) => { ... });
20
+ */
21
+
1
22
  export interface FileInfo {
2
23
  path: string;
3
24
  name: string;
@@ -5,9 +26,22 @@ export interface FileInfo {
5
26
  size: number;
6
27
  }
7
28
 
29
+ export interface DropZone {
30
+ name: string;
31
+ onFiles(callback: (files: FileInfo[]) => void): () => void;
32
+ onDragEnter(callback: () => void): () => void;
33
+ onDragLeave(callback: () => void): () => void;
34
+ element: HTMLElement | null;
35
+ }
36
+
37
+ interface PendingCall {
38
+ resolve: (v: any) => void;
39
+ reject: (e: any) => void;
40
+ }
41
+
8
42
  let callId = 0;
9
- const pending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void }>();
10
- const zoneCallbacks = new Map<string, { onFiles: (files: FileInfo[]) => void }>();
43
+ const pending = new Map<string, PendingCall>();
44
+ const zones = new Map<string, DropZone>();
11
45
 
12
46
  function getGlobal(): any {
13
47
  if (typeof window !== 'undefined') return window;
@@ -25,18 +59,18 @@ g.__response__ = function(id: string, result: any, error?: any) {
25
59
  }
26
60
  };
27
61
 
28
- // Called by C++ when files are dropped on a named zone
29
62
  g.__plusui_fileDrop__ = function(zoneName: string, files: FileInfo[]) {
30
- const zone = zoneCallbacks.get(zoneName);
31
- if (zone && zone.onFiles) zone.onFiles(files);
63
+ const zone = zones.get(zoneName);
64
+ if (zone) {
65
+ const cb = (zone as any).__onFiles;
66
+ if (cb) cb(files);
67
+ }
32
68
  };
33
69
 
34
- // Called by C++ when the drop point doesn't hit any named zone — deliver to
35
- // all registered zones so a single-zone app always works regardless of DPI
36
- // scaling or hit-test accuracy.
37
70
  g.__plusui_fileDrop_default__ = function(files: FileInfo[]) {
38
- for (const zone of zoneCallbacks.values()) {
39
- if (zone && zone.onFiles) zone.onFiles(files);
71
+ for (const zone of zones.values()) {
72
+ const cb = (zone as any).__onFiles;
73
+ if (cb) cb(files);
40
74
  }
41
75
  };
42
76
 
@@ -52,77 +86,132 @@ async function invoke<T>(method: string, params: any[] = []): Promise<T> {
52
86
  g.chrome.webview.postMessage(payload);
53
87
  } else {
54
88
  pending.delete(id);
55
- console.warn('[PlusUI] ' + method + ' - native bridge not ready');
89
+ console.warn('[PlusUI] Native bridge not ready');
56
90
  resolve(null as T);
57
91
  }
58
92
  });
59
93
  }
60
94
 
61
- export interface DropZone {
62
- onFiles: (callback: (files: FileInfo[]) => void) => () => void;
63
- element: HTMLElement | null;
64
- }
65
-
66
- export function createDropZone(name: string, el?: HTMLElement | null): DropZone {
67
- const element = el || document.querySelector(`[data-dropzone="${name}"]`) as HTMLElement;
68
-
69
- if (element) {
70
- element.setAttribute('data-dropzone', name);
71
-
72
- // Prevent browser default on drop (file navigation).
73
- // Actual file delivery comes from C++ via WM_DROPFILES → __plusui_fileDrop__.
74
- element.addEventListener('drop', (e: DragEvent) => {
95
+ export function createDropZone(name: string, element?: HTMLElement | null): DropZone {
96
+ const el = element || document.querySelector(`[data-dropzone="${name}"]`) as HTMLElement;
97
+
98
+ if (el) {
99
+ el.setAttribute('data-dropzone', name);
100
+ el.classList.add('plusui-dropzone');
101
+
102
+ el.addEventListener('drop', (e: DragEvent) => {
75
103
  e.preventDefault();
76
- e.stopPropagation();
77
- element.classList.remove('dropzone-active');
104
+ el.classList.remove('dropzone-active');
105
+ el.classList.remove('filedrop-active');
78
106
  });
79
107
 
80
- element.addEventListener('dragleave', (e: DragEvent) => {
108
+ el.addEventListener('dragleave', (e: DragEvent) => {
81
109
  const related = e.relatedTarget as Node | null;
82
- if (!related || !element.contains(related)) {
83
- element.classList.remove('dropzone-active');
110
+ if (!related || !el.contains(related)) {
111
+ el.classList.remove('dropzone-active');
112
+ el.classList.remove('filedrop-active');
84
113
  }
85
114
  });
86
115
 
87
- element.addEventListener('dragenter', (e: DragEvent) => {
116
+ el.addEventListener('dragenter', (e: DragEvent) => {
88
117
  e.preventDefault();
89
- element.classList.add('dropzone-active');
118
+ el.classList.add('dropzone-active');
119
+ el.classList.add('filedrop-active');
90
120
  if (e.dataTransfer) {
91
- // 'move' matches the native OS dropzone cursor (arrow + file icon)
92
- // rather than the browser's '+ Copy' cursor
93
- try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
121
+ try { e.dataTransfer.dropEffect = 'copy'; } catch (_) {}
94
122
  }
95
123
  });
96
124
 
97
- element.addEventListener('dragover', (e: DragEvent) => {
125
+ el.addEventListener('dragover', (e: DragEvent) => {
98
126
  e.preventDefault();
99
127
  if (e.dataTransfer) {
100
- try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
128
+ try { e.dataTransfer.dropEffect = 'copy'; } catch (_) {}
101
129
  }
102
130
  });
103
131
  }
104
132
 
105
- return {
106
- element,
133
+ const zone: DropZone = {
134
+ name,
135
+ element: el,
107
136
  onFiles: (callback: (files: FileInfo[]) => void) => {
108
- zoneCallbacks.set(name, { onFiles: callback });
109
- return () => zoneCallbacks.delete(name);
137
+ (zone as any).__onFiles = callback;
138
+ zones.set(name, zone);
139
+ return () => {
140
+ (zone as any).__onFiles = undefined;
141
+ zones.delete(name);
142
+ };
143
+ },
144
+ onDragEnter: (callback: () => void) => {
145
+ (zone as any).__onDragEnter = callback;
146
+ return () => { (zone as any).__onDragEnter = undefined; };
147
+ },
148
+ onDragLeave: (callback: () => void) => {
149
+ (zone as any).__onDragLeave = callback;
150
+ return () => { (zone as any).__onDragLeave = undefined; };
110
151
  }
111
152
  };
153
+
154
+ zones.set(name, zone);
155
+ return zone;
112
156
  }
113
157
 
114
158
  export const fileDrop = {
115
- setEnabled: (enabled: boolean) => invoke<void>('fileDrop.setEnabled', [enabled]),
116
- isEnabled: () => invoke<boolean>('fileDrop.isEnabled'),
117
159
  createDropZone,
160
+
161
+ onFiles: (name: string, callback: (files: FileInfo[]) => void): (() => void) => {
162
+ const zone = zones.get(name);
163
+ if (zone) {
164
+ return zone.onFiles(callback);
165
+ }
166
+ const newZone = createDropZone(name);
167
+ return newZone.onFiles(callback);
168
+ },
169
+
170
+ setEnabled: (enabled: boolean): Promise<void> =>
171
+ invoke<void>('fileDrop.setEnabled', [enabled]),
172
+
173
+ isEnabled: (): Promise<boolean> =>
174
+ invoke<boolean>('fileDrop.isEnabled'),
175
+
176
+ startDrag: (filePaths: string[]): Promise<void> =>
177
+ invoke<void>('fileDrop.startDrag', [filePaths]),
118
178
  };
119
179
 
120
180
  export function formatFileSize(bytes: number): string {
121
181
  if (bytes === 0) return '0 B';
122
182
  const k = 1024;
123
- const sizes = ['B', 'KB', 'MB', 'GB'];
183
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
124
184
  const i = Math.floor(Math.log(bytes) / Math.log(k));
125
185
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
126
186
  }
127
187
 
188
+ export function filterFilesByExtension(files: FileInfo[], extensions: string[]): FileInfo[] {
189
+ const exts = extensions.map(e => e.toLowerCase().replace(/^\./, ''));
190
+ return files.filter(f => {
191
+ const ext = f.name.split('.').pop()?.toLowerCase() || '';
192
+ return exts.includes(ext);
193
+ });
194
+ }
195
+
196
+ export function filterFilesByMimeType(files: FileInfo[], mimeTypes: string[]): FileInfo[] {
197
+ return files.filter(f => mimeTypes.includes(f.type));
198
+ }
199
+
200
+ export function isImageFile(file: FileInfo): boolean {
201
+ return file.type.startsWith('image/');
202
+ }
203
+
204
+ export function isVideoFile(file: FileInfo): boolean {
205
+ return file.type.startsWith('video/');
206
+ }
207
+
208
+ export function isAudioFile(file: FileInfo): boolean {
209
+ return file.type.startsWith('audio/');
210
+ }
211
+
212
+ export function isTextFile(file: FileInfo): boolean {
213
+ return file.type.startsWith('text/') || file.name.endsWith('.txt') ||
214
+ file.name.endsWith('.md') || file.name.endsWith('.json');
215
+ }
216
+
128
217
  export default fileDrop;
@@ -167,13 +167,14 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
167
167
  // while still showing visual feedback on drop zones.
168
168
  // File delivery is handled by WM_DROPFILES in wndProc.
169
169
  if (pImpl->config.disableWebviewDragDrop ||
170
- pImpl->config.enableFileDrop) {
170
+ pImpl->config.fileDrop) {
171
171
  std::string disableDragDropScript = R"(
172
172
  (function() {
173
173
  if (window.__plusui_dropzone_init) return;
174
174
  window.__plusui_dropzone_init = true;
175
175
 
176
176
  var activeZone = null;
177
+ var dragDepth = 0;
177
178
 
178
179
  var findDropZone = function(e) {
179
180
  var target = null;
@@ -187,13 +188,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
187
188
 
188
189
  var updateActiveZone = function(zone) {
189
190
  if (activeZone === zone) return;
190
- if (activeZone) activeZone.classList.remove('dropzone-active');
191
+ if (activeZone) {
192
+ activeZone.classList.remove('dropzone-active');
193
+ activeZone.classList.remove('filedrop-active');
194
+ }
191
195
  activeZone = zone;
192
- if (activeZone) activeZone.classList.add('dropzone-active');
196
+ if (activeZone) {
197
+ activeZone.classList.add('dropzone-active');
198
+ activeZone.classList.add('filedrop-active');
199
+ }
193
200
  };
194
201
 
195
202
  document.addEventListener('dragenter', function(e) {
196
203
  e.preventDefault();
204
+ dragDepth++;
197
205
  var zone = findDropZone(e);
198
206
  updateActiveZone(zone);
199
207
  if (e.dataTransfer) {
@@ -212,13 +220,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
212
220
 
213
221
  document.addEventListener('dragleave', function(e) {
214
222
  e.preventDefault();
215
- var zone = findDropZone(e);
216
- updateActiveZone(zone);
223
+ dragDepth--;
224
+ if (dragDepth <= 0) {
225
+ dragDepth = 0;
226
+ updateActiveZone(null);
227
+ } else {
228
+ var zone = findDropZone(e);
229
+ updateActiveZone(zone);
230
+ }
217
231
  }, true);
218
232
 
219
233
  document.addEventListener('drop', function(e) {
220
234
  e.preventDefault();
221
235
  updateActiveZone(null);
236
+ dragDepth = 0;
222
237
  }, true);
223
238
 
224
239
  window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
@@ -679,7 +694,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
679
694
  script = "(function() { delete window.__plusui_dropzone_init; })();";
680
695
  } else {
681
696
  // Re-install the dropzone blocker script
682
- script = "(function() { if (window.__plusui_dropzone_init) return; window.__plusui_dropzone_init = true; var activeZone = null; var findDropZone = function(e) { var target = null; if (e && typeof e.clientX === 'number' && document.elementFromPoint) { target = document.elementFromPoint(e.clientX, e.clientY); } if (!target && e && e.target) target = e.target; if (!target || !target.closest) return null; return target.closest('[data-dropzone]'); }; var updateActiveZone = function(zone) { if (activeZone === zone) return; if (activeZone) activeZone.classList.remove('dropzone-active'); activeZone = zone; if (activeZone) activeZone.classList.add('dropzone-active'); }; document.addEventListener('dragenter', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragover', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragleave', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); }, true); document.addEventListener('drop', function(e) { e.preventDefault(); updateActiveZone(null); }, true); window.addEventListener('dragover', function(e) { e.preventDefault(); }, true); window.addEventListener('drop', function(e) { e.preventDefault(); }, true); })();";
697
+ script = "(function() { if (window.__plusui_dropzone_init) return; window.__plusui_dropzone_init = true; var activeZone = null; var findDropZone = function(e) { var target = null; if (e && typeof e.clientX === 'number' && document.elementFromPoint) { target = document.elementFromPoint(e.clientX, e.clientY); } if (!target && e && e.target) target = e.target; if (!target || !target.closest) return null; return target.closest('[data-dropzone]'); }; var updateActiveZone = function(zone) { if (activeZone === zone) return; if (activeZone) activeZone.classList.remove('dropzone-active'); activeZone = zone; if (activeZone) activeZone.classList.add('dropzone-active'); }; document.addEventListener('dragenter', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragover', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragleave', function(e) { e.preventDefault(); var zone = findDropZone(e); if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) { updateActiveZone(null); } }, true); document.addEventListener('drop', function(e) { e.preventDefault(); updateActiveZone(null); }, true); window.addEventListener('dragover', function(e) { e.preventDefault(); }, true); window.addEventListener('drop', function(e) { e.preventDefault(); }, true); })();";
683
698
  }
684
699
  pImpl->webview->ExecuteScript(
685
700
  std::wstring(script.begin(), script.end()).c_str(),
@@ -806,7 +821,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
806
821
  // Block browser default drag-drop while allowing drop zone visual feedback.
807
822
  // File delivery is handled natively by macOS drag APIs, not browser events.
808
823
  if (win.pImpl->config.disableWebviewDragDrop ||
809
- win.pImpl->config.enableFileDrop) {
824
+ win.pImpl->config.fileDrop) {
810
825
  NSString *disableDragDropScript = @"(function() {"
811
826
  "if (window.__plusui_dropzone_init) return;"
812
827
  "window.__plusui_dropzone_init = true;"
@@ -841,7 +856,9 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
841
856
  "document.addEventListener('dragleave', function(e) {"
842
857
  "e.preventDefault();"
843
858
  "var zone = findDropZone(e);"
844
- "updateActiveZone(zone);"
859
+ "if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) {"
860
+ "updateActiveZone(null);"
861
+ "}"
845
862
  "}, true);"
846
863
  "document.addEventListener('drop', function(e) {"
847
864
  "e.preventDefault();"
@@ -20,7 +20,7 @@ export interface WebViewConfig {
20
20
  allowRemoteContent?: boolean;
21
21
  dataPath?: string;
22
22
  cacheSize?: number; // MB
23
- disableWebviewDragDrop?: boolean; // Disable webview drag-drop (auto-set by WindowConfig.enableFileDrop)
23
+ disableWebviewDragDrop?: boolean; // Disable browser HTML drag-drop (auto-set by WindowConfig.fileDrop)
24
24
  }
25
25
 
26
26
  export type WindowId = number;
@@ -270,7 +270,7 @@ struct Window::Impl {
270
270
  }
271
271
  }
272
272
 
273
- if (!targetImpl || !targetImpl->config.enableFileDrop ||
273
+ if (!targetImpl || !targetImpl->config.fileDrop ||
274
274
  !targetImpl->webview) {
275
275
  DragFinish(hDrop);
276
276
  return 0;
@@ -346,6 +346,10 @@ struct Window::Impl {
346
346
  "var files=" +
347
347
  filesJson +
348
348
  ";"
349
+ // Clear any leftover visual feedback from the drag
350
+ "document.querySelectorAll('.dropzone-active,.filedrop-active')"
351
+ ".forEach(function(z){z.classList.remove('dropzone-active');"
352
+ "z.classList.remove('filedrop-active');});"
349
353
  // Global event — always fires
350
354
  "window.dispatchEvent(new "
351
355
  "CustomEvent('plusui:fileDrop.filesDropped',"
@@ -504,7 +508,7 @@ Window Window::create(const WindowConfig &config) {
504
508
  w.pImpl->state.width = config.width;
505
509
  w.pImpl->state.height = config.height;
506
510
 
507
- DragAcceptFiles(w.pImpl->hwnd, config.enableFileDrop ? TRUE : FALSE);
511
+ DragAcceptFiles(w.pImpl->hwnd, config.fileDrop ? TRUE : FALSE);
508
512
 
509
513
  if (config.center) {
510
514
  RECT screen;
@@ -1069,7 +1073,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1069
1073
 
1070
1074
  // Pure native file-drop mode: when native FileDrop is enabled,
1071
1075
  // fully disable browser/WebView drag-drop handling.
1072
- if (win.pImpl->config.enableFileDrop) {
1076
+ if (win.pImpl->config.fileDrop) {
1073
1077
  win.pImpl->config.disableWebviewDragDrop = true;
1074
1078
  }
1075
1079
 
@@ -1104,18 +1108,20 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1104
1108
  Window::Impl::embeddedWebviewByParent[parentHwnd] =
1105
1109
  pImpl.get();
1106
1110
 
1107
- // Disable WebView2's internal drop handling so the OS
1108
- // drop message (WM_DROPFILES) propagates to our WndProc.
1109
- // When AllowExternalDrop is TRUE, WebView2 consumes the
1110
- // drop event itself and WM_DROPFILES never fires.
1111
- // Visual drag feedback (dropzone-active CSS class) is
1112
- // still handled by the injected JS via dragenter/dragover.
1111
+ // AllowExternalDrop must be TRUE so that drag-
1112
+ // related DOM events (dragenter, dragover, dragleave,
1113
+ // drop) fire inside the webview. Our injected JS
1114
+ // calls preventDefault() on all of them to stop the
1115
+ // browser from navigating to the dropped file.
1116
+ // The parent HWND still receives WM_DROPFILES via
1117
+ // DragAcceptFiles, which is where we extract file
1118
+ // metadata and push it into JavaScript.
1113
1119
  ComPtr<ICoreWebView2Controller4> controller4;
1114
1120
  if (controller &&
1115
1121
  SUCCEEDED(controller->QueryInterface(
1116
1122
  IID_PPV_ARGS(&controller4))) &&
1117
1123
  controller4) {
1118
- controller4->put_AllowExternalDrop(FALSE);
1124
+ controller4->put_AllowExternalDrop(TRUE);
1119
1125
  }
1120
1126
 
1121
1127
  pImpl->nativeWebView = pImpl->webview.Get();
@@ -1157,6 +1163,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1157
1163
  window.__plusui_dropzone_init = true;
1158
1164
 
1159
1165
  var activeZone = null;
1166
+ var dragDepth = 0;
1160
1167
 
1161
1168
  var findDropZone = function(e) {
1162
1169
  var target = null;
@@ -1170,19 +1177,28 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1170
1177
 
1171
1178
  var updateActiveZone = function(zone) {
1172
1179
  if (activeZone === zone) return;
1173
- if (activeZone) activeZone.classList.remove('dropzone-active');
1180
+ if (activeZone) {
1181
+ activeZone.classList.remove('dropzone-active');
1182
+ activeZone.classList.remove('filedrop-active');
1183
+ }
1174
1184
  activeZone = zone;
1175
- if (activeZone) activeZone.classList.add('dropzone-active');
1185
+ if (activeZone) {
1186
+ activeZone.classList.add('dropzone-active');
1187
+ activeZone.classList.add('filedrop-active');
1188
+ }
1176
1189
  };
1177
1190
 
1178
1191
  // Always preventDefault to stop browser from navigating to file,
1179
- // but show visual feedback when over a drop zone
1192
+ // but show visual feedback when over a drop zone.
1193
+ // dragDepth tracks nested dragenter/dragleave pairs so we know
1194
+ // when the drag truly leaves the window (depth returns to 0).
1180
1195
  document.addEventListener('dragenter', function(e) {
1181
1196
  e.preventDefault();
1197
+ dragDepth++;
1182
1198
  var zone = findDropZone(e);
1183
1199
  updateActiveZone(zone);
1184
1200
  if (e.dataTransfer) {
1185
- try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1201
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1186
1202
  }
1187
1203
  }, true);
1188
1204
 
@@ -1191,18 +1207,25 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1191
1207
  var zone = findDropZone(e);
1192
1208
  updateActiveZone(zone);
1193
1209
  if (e.dataTransfer) {
1194
- try { e.dataTransfer.dropEffect = zone ? 'move' : 'none'; } catch(_) {}
1210
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1195
1211
  }
1196
1212
  }, true);
1197
1213
 
1198
1214
  document.addEventListener('dragleave', function(e) {
1199
1215
  e.preventDefault();
1200
- var zone = findDropZone(e);
1201
- updateActiveZone(zone);
1216
+ dragDepth--;
1217
+ if (dragDepth <= 0) {
1218
+ dragDepth = 0;
1219
+ updateActiveZone(null);
1220
+ } else {
1221
+ var zone = findDropZone(e);
1222
+ updateActiveZone(zone);
1223
+ }
1202
1224
  }, true);
1203
1225
 
1204
1226
  document.addEventListener('drop', function(e) {
1205
1227
  e.preventDefault();
1228
+ dragDepth = 0;
1206
1229
  updateActiveZone(null);
1207
1230
  }, true);
1208
1231
 
@@ -1619,7 +1642,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1619
1642
  msg.substr(p1 + 1, p2 - p1 - 1);
1620
1643
  bool enabled = params.find("true") !=
1621
1644
  std::string::npos;
1622
- pImpl->config.enableFileDrop = enabled;
1645
+ pImpl->config.fileDrop = enabled;
1623
1646
 
1624
1647
  HWND targetHwnd = nullptr;
1625
1648
  if (pImpl->window) {
@@ -1652,7 +1675,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1652
1675
  }
1653
1676
  success = true;
1654
1677
  } else if (fileDropMethod == "isEnabled") {
1655
- result = pImpl->config.enableFileDrop
1678
+ result = pImpl->config.fileDrop
1656
1679
  ? "true"
1657
1680
  : "false";
1658
1681
  success = true;
@@ -1758,7 +1781,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1758
1781
  // Block browser default drag-drop while allowing drop zone visual feedback.
1759
1782
  // File delivery is handled natively by macOS drag APIs, not browser events.
1760
1783
  if (win.pImpl->config.disableWebviewDragDrop ||
1761
- win.pImpl->config.enableFileDrop) {
1784
+ win.pImpl->config.fileDrop) {
1762
1785
  NSString *disableDragDropScript =
1763
1786
  @"(function() {"
1764
1787
  "if (window.__plusui_dropzone_init) return;"
@@ -1794,7 +1817,9 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1794
1817
  "document.addEventListener('dragleave', function(e) {"
1795
1818
  "e.preventDefault();"
1796
1819
  "var zone = findDropZone(e);"
1797
- "updateActiveZone(zone);"
1820
+ "if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) {"
1821
+ "updateActiveZone(null);"
1822
+ "}"
1798
1823
  "}, true);"
1799
1824
  "document.addEventListener('drop', function(e) {"
1800
1825
  "e.preventDefault();"
@@ -74,7 +74,7 @@
74
74
  * .width(1200) .width(1200)
75
75
  * .height(800) .height(800)
76
76
  * .devtools(true) .devtools(true)
77
- * .enableFileDrop(true); .enableFileDrop(true)
77
+ * .fileDrop(true); .fileDrop(true)
78
78
  * .build() ← returns Window
79
79
  * TS: app.quit() C++: app.quit()
80
80
  * TS: app.onReady(cb) C++: (start after build())
@@ -201,7 +201,7 @@
201
201
  * .title("My App")
202
202
  * .width(1200)
203
203
  * .height(800)
204
- * .enableFileDrop(true)
204
+ * .fileDrop(true)
205
205
  * .build();
206
206
  *
207
207
  * MyApp connect;
@@ -25,47 +25,47 @@ private:
25
25
  std::unique_ptr<Impl> pImpl;
26
26
  };
27
27
 
28
- class App::Builder {
29
- public:
30
- struct Config {
31
- std::string title = "PlusUI App";
32
- int width = 1200;
33
- int height = 800;
34
- bool resizable = true;
35
- bool devtools = true;
36
- std::string trayIconPath;
37
- std::string trayTooltip;
38
- bool alwaysOnTop = false;
39
- bool centered = true;
40
- bool transparent = false;
41
- bool decorations = true;
42
- bool skipTaskbar = false;
43
- bool scrollbars = true;
44
- bool enableFileDrop = true;
45
- };
46
-
47
- Builder();
48
-
49
- Builder &title(const std::string &t);
50
- Builder &width(int w);
51
- Builder &height(int h);
52
- Builder &resizable(bool r);
53
- Builder &devtools(bool d);
54
- Builder &trayIcon(const std::string &icon);
55
- Builder &trayTooltip(const std::string &tooltip);
56
- Builder &alwaysOnTop(bool top);
57
- Builder &centered(bool center);
58
- Builder &transparent(bool transparent);
59
- Builder &decorations(bool decorations);
60
- Builder &skipTaskbar(bool skip);
61
- Builder &scrollbars(bool show);
62
- Builder &enableFileDrop(bool enable);
63
-
64
- Window build();
65
-
66
- private:
67
- Config config;
68
- };
28
+ class App::Builder {
29
+ public:
30
+ struct Config {
31
+ std::string title = "PlusUI App";
32
+ int width = 1200;
33
+ int height = 800;
34
+ bool resizable = true;
35
+ bool devtools = true;
36
+ std::string trayIconPath;
37
+ std::string trayTooltip;
38
+ bool alwaysOnTop = false;
39
+ bool centered = true;
40
+ bool transparent = false;
41
+ bool decorations = true;
42
+ bool skipTaskbar = false;
43
+ bool scrollbars = true;
44
+ bool fileDrop = true; // Enable OS file drop
45
+ };
46
+
47
+ Builder();
48
+
49
+ Builder &title(const std::string &t);
50
+ Builder &width(int w);
51
+ Builder &height(int h);
52
+ Builder &resizable(bool r);
53
+ Builder &devtools(bool d);
54
+ Builder &trayIcon(const std::string &icon);
55
+ Builder &trayTooltip(const std::string &tooltip);
56
+ Builder &alwaysOnTop(bool top);
57
+ Builder &centered(bool center);
58
+ Builder &transparent(bool transparent);
59
+ Builder &decorations(bool decorations);
60
+ Builder &skipTaskbar(bool skip);
61
+ Builder &scrollbars(bool show);
62
+ Builder &fileDrop(bool enable);
63
+
64
+ Window build();
65
+
66
+ private:
67
+ Config config;
68
+ };
69
69
 
70
70
  App::Builder createApp();
71
71
 
@@ -11,46 +11,44 @@ namespace plusui {
11
11
 
12
12
  class TrayManager;
13
13
 
14
- struct WindowConfig {
15
- // Window properties
16
- std::string title = "PlusUI Window";
17
- int x = -1;
18
- int y = -1;
19
- int width = 800;
20
- int height = 600;
21
- int minWidth = 100;
22
- int minHeight = 100;
23
- int maxWidth = -1;
24
- int maxHeight = -1;
25
- bool resizable = true;
26
- bool minimizable = true;
27
- bool maximizable = true;
28
- bool closable = true;
29
- bool alwaysOnTop = false;
30
- bool center = true;
31
- bool frame = true;
32
- bool transparent = false;
33
- bool decorations = true;
34
- bool skipTaskbar = false;
35
- double opacity = 1.0;
36
- bool fullscreen = false;
37
- bool enableFileDrop =
38
- true; // Enable native FileDrop API (auto-disables webview drag-drop)
39
-
40
- // WebView properties
41
- std::string userAgent = "";
42
- bool devtools = true;
43
- bool contextMenu = true;
44
- bool javascript = true;
45
- bool webSecurity = true;
46
- bool allowFileAccess = false;
47
- bool allowRemoteContent = true;
48
- std::string dataPath = "";
49
- int cacheSize = 100; // MB
50
- bool scrollbars = true; // Show/hide scrollbars
51
- bool disableWebviewDragDrop =
52
- true; // Disable webview drag-drop (usually set by enableFileDrop)
53
- };
14
+ struct WindowConfig {
15
+ // Window properties
16
+ std::string title = "PlusUI Window";
17
+ int x = -1;
18
+ int y = -1;
19
+ int width = 800;
20
+ int height = 600;
21
+ int minWidth = 100;
22
+ int minHeight = 100;
23
+ int maxWidth = -1;
24
+ int maxHeight = -1;
25
+ bool resizable = true;
26
+ bool minimizable = true;
27
+ bool maximizable = true;
28
+ bool closable = true;
29
+ bool alwaysOnTop = false;
30
+ bool center = true;
31
+ bool frame = true;
32
+ bool transparent = false;
33
+ bool decorations = true;
34
+ bool skipTaskbar = false;
35
+ double opacity = 1.0;
36
+ bool fullscreen = false;
37
+ bool fileDrop = true; // Enable OS file drop (drag files from Explorer/Finder into window)
38
+ bool disableWebviewDragDrop = true; // Disable browser HTML drag-drop (auto-set by fileDrop)
39
+
40
+ // WebView properties
41
+ std::string userAgent = "";
42
+ bool devtools = true;
43
+ bool contextMenu = true;
44
+ bool javascript = true;
45
+ bool webSecurity = true;
46
+ bool allowFileAccess = false;
47
+ bool allowRemoteContent = true;
48
+ std::string dataPath = "";
49
+ int cacheSize = 100; // MB
50
+ bool scrollbars = true; // Show/hide scrollbars
51
+ };
54
52
 
55
53
  struct WindowState {
56
54
  int x = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "main": "./Core/Features/API/index.ts",