plusui-native-core 0.1.104 → 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
+ };
@@ -102,27 +102,30 @@ export function createDropZone(name: string, element?: HTMLElement | null): Drop
102
102
  el.addEventListener('drop', (e: DragEvent) => {
103
103
  e.preventDefault();
104
104
  el.classList.remove('dropzone-active');
105
+ el.classList.remove('filedrop-active');
105
106
  });
106
107
 
107
108
  el.addEventListener('dragleave', (e: DragEvent) => {
108
109
  const related = e.relatedTarget as Node | null;
109
110
  if (!related || !el.contains(related)) {
110
111
  el.classList.remove('dropzone-active');
112
+ el.classList.remove('filedrop-active');
111
113
  }
112
114
  });
113
115
 
114
116
  el.addEventListener('dragenter', (e: DragEvent) => {
115
117
  e.preventDefault();
116
118
  el.classList.add('dropzone-active');
119
+ el.classList.add('filedrop-active');
117
120
  if (e.dataTransfer) {
118
- try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
121
+ try { e.dataTransfer.dropEffect = 'copy'; } catch (_) {}
119
122
  }
120
123
  });
121
124
 
122
125
  el.addEventListener('dragover', (e: DragEvent) => {
123
126
  e.preventDefault();
124
127
  if (e.dataTransfer) {
125
- try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
128
+ try { e.dataTransfer.dropEffect = 'copy'; } catch (_) {}
126
129
  }
127
130
  });
128
131
  }
@@ -174,6 +174,7 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
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,16 +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
- // Only remove if actually leaving the zone (relatedTarget is outside)
217
- if (e.relatedTarget && zone && !zone.contains(e.relatedTarget)) {
223
+ dragDepth--;
224
+ if (dragDepth <= 0) {
225
+ dragDepth = 0;
218
226
  updateActiveZone(null);
227
+ } else {
228
+ var zone = findDropZone(e);
229
+ updateActiveZone(zone);
219
230
  }
220
231
  }, true);
221
232
 
222
233
  document.addEventListener('drop', function(e) {
223
234
  e.preventDefault();
224
235
  updateActiveZone(null);
236
+ dragDepth = 0;
225
237
  }, true);
226
238
 
227
239
  window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
@@ -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',"
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.104",
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",