plusui-native 0.2.8 → 0.2.9

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.8",
3
+ "version": "0.2.9",
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.8",
31
- "plusui-native-bindgen": "^0.1.8"
30
+ "plusui-native-builder": "^0.1.9",
31
+ "plusui-native-bindgen": "^0.1.9"
32
32
  },
33
33
  "peerDependencies": {
34
- "plusui-native-bindgen": "^0.1.8"
34
+ "plusui-native-bindgen": "^0.1.9"
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';
@@ -198,8 +198,8 @@ function runCMake(args, options = {}) {
198
198
  function getAppBindgenPaths() {
199
199
  return {
200
200
  featuresDir: join(process.cwd(), 'src', 'features'),
201
- outputDir: join(process.cwd(), 'src', 'Bindings_Generated'),
202
- frontendOutputDir: join(process.cwd(), 'frontend', 'src', 'Bindings_Generated'),
201
+ outputDir: join(process.cwd(), 'src', 'Bindings'),
202
+ frontendOutputDir: join(process.cwd(), 'frontend', 'src', 'Bindings'),
203
203
  };
204
204
  }
205
205
 
@@ -261,11 +261,20 @@ function ensureBuildLayout() {
261
261
  }
262
262
  }
263
263
 
264
+ function getDevBuildDir() {
265
+ const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
266
+ return join('.plusui', 'dev', platformFolder);
267
+ }
268
+
264
269
  function resolveBindgenScriptPath() {
265
270
  const candidates = [
271
+ resolve(__dirname, '../../plusui-bindgen/src/advanced-bindgen.js'),
266
272
  resolve(__dirname, '../../plusui-bindgen/src/index.js'),
273
+ resolve(__dirname, '../../plusui-native-bindgen/src/advanced-bindgen.js'),
267
274
  resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
275
+ resolve(__dirname, '../../../plusui-native-bindgen/src/advanced-bindgen.js'),
268
276
  resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
277
+ resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'advanced-bindgen.js'),
269
278
  resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
270
279
  ];
271
280
 
@@ -556,8 +565,7 @@ async function startBackend() {
556
565
  const projectName = getProjectName();
557
566
  killProcessByName(projectName);
558
567
 
559
- const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
560
- const buildDir = join('build', platformFolder, 'dev');
568
+ const buildDir = getDevBuildDir();
561
569
 
562
570
  // Configure with dev mode if not configured
563
571
  if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
@@ -733,8 +741,7 @@ function devBackend() {
733
741
  const projectName = getProjectName();
734
742
  killProcessByName(projectName);
735
743
 
736
- const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
737
- const buildDir = join('build', platformFolder, 'dev');
744
+ const buildDir = getDevBuildDir();
738
745
 
739
746
  if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
740
747
  log('Configuring CMake...', 'blue');
@@ -841,7 +848,7 @@ async function clean() {
841
848
  ensureProjectRoot('clean');
842
849
  logSection('Cleaning Build Artifacts');
843
850
 
844
- const dirs = ['build', 'frontend/dist'];
851
+ const dirs = ['build', '.plusui', 'frontend/dist'];
845
852
 
846
853
  for (const dir of dirs) {
847
854
  if (existsSync(dir)) {
@@ -881,22 +888,12 @@ async function runBindgen(providedArgs = null, options = {}) {
881
888
  let defaultFrontendOutputDir = null;
882
889
 
883
890
  if (bindgenArgs.length === 0) {
884
- const { featuresDir: appFeaturesDir, outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
885
-
886
- if (existsSync(appFeaturesDir)) {
887
- bindgenArgs = [appFeaturesDir, appOutputDir];
888
- usedDefaultAppMode = true;
889
- defaultOutputDir = appOutputDir;
890
- defaultFrontendOutputDir = frontendOutputDir;
891
- log(`App mode: ${appFeaturesDir} -> ${appOutputDir}`, 'dim');
892
- } else {
893
- if (skipIfNoInput) {
894
- log(`No src/features folder found; skipping binding refresh for ${source}.`, 'dim');
895
- return;
896
- } else {
897
- error('No bindgen input found. Create src/features in your app or pass paths: plusui bindgen <featuresDir> <outputDir>');
898
- }
899
- }
891
+ const { outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
892
+ bindgenArgs = [process.cwd(), appOutputDir];
893
+ usedDefaultAppMode = true;
894
+ defaultOutputDir = appOutputDir;
895
+ defaultFrontendOutputDir = frontendOutputDir;
896
+ log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
900
897
  }
901
898
 
902
899
  // Spawn node process
@@ -27,22 +27,44 @@ This will:
27
27
  - Changes to frontend code reflect instantly
28
28
  - Auto-refresh app bindings when `src/features` exists
29
29
 
30
+ Dev build intermediates are stored in `.plusui/dev/...` so your `build/` folder stays focused on release/platform outputs.
31
+
30
32
  Note: You can still run `npm run bind` manually anytime.
31
33
 
32
34
  ## Bindings (App-level)
33
35
 
34
- Generate bindings for your app feature headers:
36
+ Generate bidirectional bindings for your app:
35
37
  ```bash
36
38
  npm run bind
37
39
  ```
38
40
 
39
41
  Default bindgen paths:
40
- - Input headers: `src/features`
41
- - Output: `src/Bindings_Generated`
42
+ - Input: project root scan (frontend + backend files)
43
+ - Output: `src/Bindings`
44
+
45
+ Generated structure:
46
+ - `src/Bindings/NativeBindings/CPP_IO`
47
+ - `src/Bindings/NativeBindings/WEB_IO`
48
+ - `src/Bindings/CustomBindings/CPP_IO`
49
+ - `src/Bindings/CustomBindings/WEB_IO`
50
+ - `include/Bindings/NativeBindings/CPP_IO` (generated `.hpp` headers)
51
+ - `include/Bindings/CustomBindings/CPP_IO` (generated `.hpp` headers)
52
+
53
+ Scan extensions:
54
+ - `WEB_IO`: `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.html`
55
+ - `CPP_IO`: `.h`, `.hpp`, `.hh`, `.hxx`, `.cpp`, `.cc`, `.cxx`
56
+
57
+ Custom binding kinds detected:
58
+ - `method`
59
+ - `service`
60
+ - `stream`
61
+ - `event`
62
+
63
+ `plusui bind` scans your whole project structure and does not require a specific feature folder.
42
64
 
43
65
  You can also pass custom paths:
44
66
  ```bash
45
- plusui bindgen <featuresDir> <outputDir>
67
+ plusui bindgen <projectRoot> <outputDir>
46
68
  ```
47
69
 
48
70
  ## Assets & Icons
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
- import { win, browser, router, app } from './plusui';
2
+ import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
3
3
 
4
4
  // Define routes for your app (optional - for SPA routing)
5
5
  const routes = {
@@ -14,6 +14,11 @@ function App() {
14
14
  const [currentUrl, setCurrentUrl] = useState('');
15
15
  const [canGoBack, setCanGoBack] = useState(false);
16
16
  const [canGoForward, setCanGoForward] = useState(false);
17
+
18
+ // FileDrop state
19
+ const [files, setFiles] = useState<FileInfo[]>([]);
20
+ const [isDragging, setIsDragging] = useState(false);
21
+ const [dropZoneStyle, setDropZoneStyle] = useState('');
17
22
 
18
23
  useEffect(() => {
19
24
  // Setup routes
@@ -26,7 +31,27 @@ function App() {
26
31
  browser.canGoForward().then(setCanGoForward);
27
32
  });
28
33
 
29
- // Get initial state
34
+ // Setup FileDrop listeners
35
+ const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
36
+ console.log('Files dropped:', droppedFiles);
37
+ setFiles(prev => [...prev, ...droppedFiles]);
38
+ setIsDragging(false);
39
+ });
40
+
41
+ const unsubEnter = fileDrop.onDragEnter(() => {
42
+ setIsDragging(true);
43
+ });
44
+
45
+ const unsubLeave = fileDrop.onDragLeave(() => {
46
+ setIsDragging(false);
47
+ });
48
+
49
+ return () => {
50
+ unsub();
51
+ unsubDrop();
52
+ unsubEnter();
53
+ unsubLeave();
54
+ }al state
30
55
  browser.getUrl().then(setCurrentUrl);
31
56
  browser.canGoBack().then(setCanGoBack);
32
57
  browser.canGoForward().then(setCanGoForward);
@@ -101,7 +126,68 @@ function App() {
101
126
  <div className="button-group">
102
127
  <button onClick={handleGoBack} className="button" disabled={!canGoBack}>Back</button>
103
128
  <button onClick={handleGoForward} className="button" disabled={!canGoForward}>Forward</button>
104
- <button onClick={handleReload} className="button">Reload</button>
129
+ <button onClcard">
130
+ <h2>FileDrop - Drag & Drop Files</h2>
131
+
132
+ <div style={{ marginBottom: '1rem' }}>
133
+ <label style={{ marginRight: '0.5rem', fontSize: '0.9em' }}>Style:</label>
134
+ <select
135
+ value={dropZoneStyle}
136
+ onChange={(e) => setDropZoneStyle(e.target.value)}
137
+ style={{
138
+ padding: '0.5rem',
139
+ borderRadius: '0.25rem',
140
+ border: '1px solid rgba(255,255,255,0.3)',
141
+ background: 'rgba(255,255,255,0.1)',
142
+ color: '#fff',
143
+ fontSize: '0.9em'
144
+ }}
145
+ >
146
+ <option value="">Default</option>
147
+ <option value="filedrop-compact">Compact</option>
148
+ <option value="filedrop-inline">Inline</option>
149
+ <option value="filedrop-minimal">Minimal</option>
150
+ <option value="filedrop-bold">Bold</option>
151
+ </select>
152
+ </div>
153
+
154
+ <div className={`filedrop-zone ${dropZoneStyle} ${isDragging ? 'filedrop-active' : ''}`}>
155
+ <div className="filedrop-content">
156
+ <svg className="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
157
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
158
+ 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" />
159
+ </svg>
160
+ <div className="filedrop-text">
161
+ {isDragging ? 'Drop files here' : 'Drag & drop files'}
162
+ </div>
163
+ <div className="filedrop-hint">All file types supported</div>
164
+ </div>
165
+ </div>
166
+
167
+ {files.length > 0 && (
168
+ <div className="filedrop-files">
169
+ {files.map((file, i) => (
170
+ <div key={i} className="filedrop-file-item">
171
+ <div className="filedrop-file-icon">📄</div>
172
+ <div className="filedrop-file-info">
173
+ <div className="filedrop-file-name">{file.name}</div>
174
+ <div className="filedrop-file-meta">
175
+ {formatFileSize(file.size)} • {file.type}
176
+ </div>
177
+ </div>
178
+ <button
179
+ className="filedrop-file-remove"
180
+ onClick={() => setFiles(files.filter((_, idx) => idx !== i))}
181
+ >
182
+
183
+ </button>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ )}
188
+ </div>
189
+
190
+ <div className="ick={handleReload} className="button">Reload</button>
105
191
  </div>
106
192
  </div>
107
193
 
@@ -115,3 +115,50 @@ export const router = {
115
115
  export const app = {
116
116
  quit: async () => invoke('app.quit', []),
117
117
  };
118
+
119
+ // FileDrop API
120
+ export interface FileInfo {
121
+ path: string;
122
+ name: string;
123
+ type: string;
124
+ size: number;
125
+ }
126
+
127
+ export const fileDrop = {
128
+ setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
129
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
130
+ onFilesDropped: (handler: (files: FileInfo[]) => void) => {
131
+ 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);
138
+ },
139
+ onDragEnter: (handler: () => void) => {
140
+ if (typeof window === 'undefined') return () => {};
141
+ const eventHandler = () => handler();
142
+ window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
143
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
144
+ },
145
+ onDragLeave: (handler: () => void) => {
146
+ if (typeof window === 'undefined') return () => {};
147
+ const eventHandler = () => handler();
148
+ window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
149
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
150
+ },
151
+ };
152
+
153
+ // Helper functions
154
+ export function formatFileSize(bytes: number): string {
155
+ if (bytes === 0) return '0 Bytes';
156
+ const k = 1024;
157
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
158
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
159
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
160
+ }
161
+
162
+ export function isImageFile(file: FileInfo): boolean {
163
+ return file.type.startsWith('image/');
164
+ }
@@ -138,3 +138,191 @@ body {
138
138
  border-radius: 0.25rem;
139
139
  font-family: 'Courier New', monospace;
140
140
  }
141
+
142
+ /* ============================================================================
143
+ * FILEDROP STYLES
144
+ * ============================================================================ */
145
+
146
+ .filedrop-zone {
147
+ position: relative;
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: center;
151
+ justify-content: center;
152
+ min-height: 180px;
153
+ padding: 2rem;
154
+ border: 2px dashed rgba(255, 255, 255, 0.3);
155
+ border-radius: 0.75rem;
156
+ background-color: rgba(255, 255, 255, 0.05);
157
+ transition: all 0.2s ease-in-out;
158
+ cursor: pointer;
159
+ user-select: none;
160
+ }
161
+
162
+ .filedrop-zone:hover {
163
+ border-color: rgba(255, 255, 255, 0.5);
164
+ background-color: rgba(255, 255, 255, 0.1);
165
+ }
166
+
167
+ .filedrop-zone.filedrop-active {
168
+ border-color: #60a5fa;
169
+ background-color: rgba(96, 165, 250, 0.1);
170
+ border-width: 3px;
171
+ transform: scale(1.02);
172
+ box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
173
+ }
174
+
175
+ .filedrop-content {
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ gap: 1rem;
180
+ text-align: center;
181
+ pointer-events: none;
182
+ }
183
+
184
+ .filedrop-icon {
185
+ width: 3rem;
186
+ height: 3rem;
187
+ color: rgba(255, 255, 255, 0.6);
188
+ transition: all 0.2s ease-in-out;
189
+ }
190
+
191
+ .filedrop-zone.filedrop-active .filedrop-icon {
192
+ color: #60a5fa;
193
+ transform: scale(1.2);
194
+ }
195
+
196
+ .filedrop-text {
197
+ font-size: 1rem;
198
+ font-weight: 500;
199
+ color: rgba(255, 255, 255, 0.9);
200
+ transition: color 0.2s ease-in-out;
201
+ }
202
+
203
+ .filedrop-zone.filedrop-active .filedrop-text {
204
+ color: #93c5fd;
205
+ font-weight: 600;
206
+ }
207
+
208
+ .filedrop-hint {
209
+ font-size: 0.875rem;
210
+ color: rgba(255, 255, 255, 0.6);
211
+ transition: color 0.2s ease-in-out;
212
+ }
213
+
214
+ .filedrop-zone.filedrop-active .filedrop-hint {
215
+ color: rgba(255, 255, 255, 0.8);
216
+ }
217
+
218
+ .filedrop-zone.filedrop-compact {
219
+ min-height: 120px;
220
+ padding: 1.5rem;
221
+ }
222
+
223
+ .filedrop-zone.filedrop-compact .filedrop-icon {
224
+ width: 2rem;
225
+ height: 2rem;
226
+ }
227
+
228
+ .filedrop-zone.filedrop-inline {
229
+ min-height: 80px;
230
+ padding: 1rem;
231
+ flex-direction: row;
232
+ justify-content: flex-start;
233
+ gap: 1rem;
234
+ }
235
+
236
+ .filedrop-zone.filedrop-inline .filedrop-content {
237
+ flex-direction: row;
238
+ align-items: center;
239
+ text-align: left;
240
+ gap: 0.75rem;
241
+ }
242
+
243
+ .filedrop-zone.filedrop-minimal {
244
+ border-style: solid;
245
+ border-width: 1px;
246
+ background-color: transparent;
247
+ }
248
+
249
+ .filedrop-zone.filedrop-bold {
250
+ border-width: 3px;
251
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
252
+ }
253
+
254
+ .filedrop-files {
255
+ margin-top: 1rem;
256
+ width: 100%;
257
+ }
258
+
259
+ .filedrop-file-item {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 0.75rem;
263
+ padding: 0.75rem;
264
+ background-color: rgba(255, 255, 255, 0.1);
265
+ border: 1px solid rgba(255, 255, 255, 0.2);
266
+ border-radius: 0.5rem;
267
+ margin-bottom: 0.5rem;
268
+ transition: all 0.2s ease-in-out;
269
+ }
270
+
271
+ .filedrop-file-item:hover {
272
+ background-color: rgba(255, 255, 255, 0.15);
273
+ border-color: rgba(255, 255, 255, 0.3);
274
+ }
275
+
276
+ .filedrop-file-icon {
277
+ width: 2rem;
278
+ height: 2rem;
279
+ flex-shrink: 0;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ background-color: rgba(255, 255, 255, 0.1);
284
+ border-radius: 0.375rem;
285
+ font-size: 1.2rem;
286
+ }
287
+
288
+ .filedrop-file-info {
289
+ flex: 1;
290
+ min-width: 0;
291
+ }
292
+
293
+ .filedrop-file-name {
294
+ font-size: 0.875rem;
295
+ font-weight: 500;
296
+ color: #fff;
297
+ white-space: nowrap;
298
+ overflow: hidden;
299
+ text-overflow: ellipsis;
300
+ }
301
+
302
+ .filedrop-file-meta {
303
+ font-size: 0.75rem;
304
+ color: rgba(255, 255, 255, 0.6);
305
+ margin-top: 0.125rem;
306
+ }
307
+
308
+ .filedrop-file-remove {
309
+ flex-shrink: 0;
310
+ width: 1.5rem;
311
+ height: 1.5rem;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ border-radius: 0.25rem;
316
+ color: rgba(255, 255, 255, 0.6);
317
+ cursor: pointer;
318
+ transition: all 0.2s ease-in-out;
319
+ pointer-events: auto;
320
+ background: transparent;
321
+ border: none;
322
+ font-size: 1rem;
323
+ }
324
+
325
+ .filedrop-file-remove:hover {
326
+ background-color: rgba(239, 68, 68, 0.2);
327
+ color: #fca5a5;
328
+ }
@@ -75,6 +75,7 @@ struct ServicesConfig {
75
75
  bool enableDisplay = true; // Enable multi-display detection
76
76
  bool enableKeyboard = true; // Enable keyboard shortcuts/hotkeys
77
77
  bool enableMenu = true; // Enable custom menu bar
78
+ bool enableFileDrop = true; // Enable drag & drop file handling
78
79
  } servicesConfig;
79
80
 
80
81
  // ============================================================================
@@ -169,6 +170,24 @@ int main() {
169
170
  // });
170
171
  // Call from JS: const version = await app.invoke('getVersion');
171
172
 
173
+ // ========================================
174
+ // FILE DROP EVENTS (Drag & Drop)
175
+ // ========================================
176
+ // Listen for files dropped into the window
177
+ plusui::event::on("fileDrop.filesDropped", [](const std::string& data) {
178
+ std::cout << "Files dropped: " << data << std::endl;
179
+ // Parse the JSON data to get file info
180
+ // You can process files here in C++
181
+ });
182
+
183
+ plusui::event::on("fileDrop.dragEnter", [](const std::string&) {
184
+ std::cout << "Drag entered window" << std::endl;
185
+ });
186
+
187
+ plusui::event::on("fileDrop.dragLeave", [](const std::string&) {
188
+ std::cout << "Drag left window" << std::endl;
189
+ });
190
+
172
191
  // ========================================
173
192
  // RUN APPLICATION
174
193
  // ========================================
@@ -198,4 +217,8 @@ int main() {
198
217
  // DISPLAY: display.getAll(), display.getPrimary(), display.getCurrent()
199
218
  //
200
219
  // CLIPBOARD: clipboard.writeText(str), clipboard.readText(), clipboard.clear()
220
+ //
221
+ // FILEDROP: fileDrop.onFilesDropped(callback), fileDrop.setEnabled(bool),
222
+ // fileDrop.onDragEnter(callback), fileDrop.onDragLeave(callback),
223
+ // fileDrop.startDrag([paths])
201
224
 
@@ -1,5 +1,5 @@
1
- import { createSignal, onMount, Show } from 'solid-js';
2
- import { win, browser, router, app } from './plusui';
1
+ import { createSignal, onMount, onCleanup, Show, For } from 'solid-js';
2
+ import { win, browser, router, app, fileDrop, formatFileSize, type FileInfo } from './plusui';
3
3
 
4
4
  // Define routes for your app (optional - for SPA routing)
5
5
  const routes = {
@@ -14,6 +14,11 @@ function App() {
14
14
  const [currentUrl, setCurrentUrl] = createSignal('');
15
15
  const [canGoBack, setCanGoBack] = createSignal(false);
16
16
  const [canGoForward, setCanGoForward] = createSignal(false);
17
+
18
+ // FileDrop state
19
+ const [files, setFiles] = createSignal<FileInfo[]>([]);
20
+ const [isDragging, setIsDragging] = createSignal(false);
21
+ const [dropZoneStyle, setDropZoneStyle] = createSignal('');
17
22
 
18
23
  onMount(() => {
19
24
  // Setup routes
@@ -30,6 +35,27 @@ function App() {
30
35
  browser.getUrl().then(setCurrentUrl);
31
36
  browser.canGoBack().then(setCanGoBack);
32
37
  browser.canGoForward().then(setCanGoForward);
38
+
39
+ // Setup FileDrop listeners
40
+ const unsubDrop = fileDrop.onFilesDropped((droppedFiles) => {
41
+ console.log('Files dropped:', droppedFiles);
42
+ setFiles(prev => [...prev, ...droppedFiles]);
43
+ setIsDragging(false);
44
+ });
45
+
46
+ const unsubEnter = fileDrop.onDragEnter(() => {
47
+ setIsDragging(true);
48
+ });
49
+
50
+ const unsubLeave = fileDrop.onDragLeave(() => {
51
+ setIsDragging(false);
52
+ });
53
+
54
+ onCleanup(() => {
55
+ unsubDrop();
56
+ unsubEnter();
57
+ unsubLeave();
58
+ });
33
59
  });
34
60
 
35
61
  const handleMinimize = async () => await win.minimize();
@@ -119,6 +145,69 @@ function App() {
119
145
  <button onClick={handleQuit} class="button button-danger">Quit App</button>
120
146
  </div>
121
147
 
148
+ <div class="card">
149
+ <h2>FileDrop - Drag & Drop Files</h2>
150
+
151
+ <div style={{ 'margin-bottom': '1rem' }}>
152
+ <label style={{ 'margin-right': '0.5rem', 'font-size': '0.9em' }}>Style:</label>
153
+ <select
154
+ value={dropZoneStyle()}
155
+ onChange={(e) => setDropZoneStyle(e.target.value)}
156
+ style={{
157
+ padding: '0.5rem',
158
+ 'border-radius': '0.25rem',
159
+ border: '1px solid rgba(255,255,255,0.3)',
160
+ background: 'rgba(255,255,255,0.1)',
161
+ color: '#fff',
162
+ 'font-size': '0.9em'
163
+ }}
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
+ <div class={`filedrop-zone ${dropZoneStyle()} ${isDragging() ? 'filedrop-active' : ''}`}>
174
+ <div class="filedrop-content">
175
+ <svg class="filedrop-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
176
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width={2}
177
+ 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" />
178
+ </svg>
179
+ <div class="filedrop-text">
180
+ {isDragging() ? 'Drop files here' : 'Drag & drop files'}
181
+ </div>
182
+ <div class="filedrop-hint">All file types supported</div>
183
+ </div>
184
+ </div>
185
+
186
+ <Show when={files().length > 0}>
187
+ <div class="filedrop-files">
188
+ <For each={files()}>
189
+ {(file, i) => (
190
+ <div class="filedrop-file-item">
191
+ <div class="filedrop-file-icon">📄</div>
192
+ <div class="filedrop-file-info">
193
+ <div class="filedrop-file-name">{file.name}</div>
194
+ <div class="filedrop-file-meta">
195
+ {formatFileSize(file.size)} • {file.type}
196
+ </div>
197
+ </div>
198
+ <button
199
+ class="filedrop-file-remove"
200
+ onClick={() => setFiles(files().filter((_, idx) => idx !== i()))}
201
+ >
202
+
203
+ </button>
204
+ </div>
205
+ )}
206
+ </For>
207
+ </div>
208
+ </Show>
209
+ </div>
210
+
122
211
  <div class="info">
123
212
  <p>Edit <code>frontend/src/App.tsx</code> to modify the UI.</p>
124
213
  <p>Edit <code>main.cpp</code> to add C++ functionality.</p>
@@ -115,3 +115,50 @@ export const router = {
115
115
  export const app = {
116
116
  quit: async () => invoke('app.quit', []),
117
117
  };
118
+
119
+ // FileDrop API
120
+ export interface FileInfo {
121
+ path: string;
122
+ name: string;
123
+ type: string;
124
+ size: number;
125
+ }
126
+
127
+ export const fileDrop = {
128
+ setEnabled: async (enabled: boolean) => invoke('fileDrop.setEnabled', [enabled]),
129
+ isEnabled: async (): Promise<boolean> => invoke('fileDrop.isEnabled', []) as Promise<boolean>,
130
+ onFilesDropped: (handler: (files: FileInfo[]) => void) => {
131
+ 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);
138
+ },
139
+ onDragEnter: (handler: () => void) => {
140
+ if (typeof window === 'undefined') return () => {};
141
+ const eventHandler = () => handler();
142
+ window.addEventListener('plusui:fileDrop.dragEnter', eventHandler);
143
+ return () => window.removeEventListener('plusui:fileDrop.dragEnter', eventHandler);
144
+ },
145
+ onDragLeave: (handler: () => void) => {
146
+ if (typeof window === 'undefined') return () => {};
147
+ const eventHandler = () => handler();
148
+ window.addEventListener('plusui:fileDrop.dragLeave', eventHandler);
149
+ return () => window.removeEventListener('plusui:fileDrop.dragLeave', eventHandler);
150
+ },
151
+ };
152
+
153
+ // Helper functions
154
+ export function formatFileSize(bytes: number): string {
155
+ if (bytes === 0) return '0 Bytes';
156
+ const k = 1024;
157
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
158
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
159
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
160
+ }
161
+
162
+ export function isImageFile(file: FileInfo): boolean {
163
+ return file.type.startsWith('image/');
164
+ }
@@ -138,3 +138,191 @@ body {
138
138
  border-radius: 0.25rem;
139
139
  font-family: 'Courier New', monospace;
140
140
  }
141
+
142
+ /* ============================================================================
143
+ * FILEDROP STYLES
144
+ * ============================================================================ */
145
+
146
+ .filedrop-zone {
147
+ position: relative;
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: center;
151
+ justify-content: center;
152
+ min-height: 180px;
153
+ padding: 2rem;
154
+ border: 2px dashed rgba(255, 255, 255, 0.3);
155
+ border-radius: 0.75rem;
156
+ background-color: rgba(255, 255, 255, 0.05);
157
+ transition: all 0.2s ease-in-out;
158
+ cursor: pointer;
159
+ user-select: none;
160
+ }
161
+
162
+ .filedrop-zone:hover {
163
+ border-color: rgba(255, 255, 255, 0.5);
164
+ background-color: rgba(255, 255, 255, 0.1);
165
+ }
166
+
167
+ .filedrop-zone.filedrop-active {
168
+ border-color: #60a5fa;
169
+ background-color: rgba(96, 165, 250, 0.1);
170
+ border-width: 3px;
171
+ transform: scale(1.02);
172
+ box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
173
+ }
174
+
175
+ .filedrop-content {
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ gap: 1rem;
180
+ text-align: center;
181
+ pointer-events: none;
182
+ }
183
+
184
+ .filedrop-icon {
185
+ width: 3rem;
186
+ height: 3rem;
187
+ color: rgba(255, 255, 255, 0.6);
188
+ transition: all 0.2s ease-in-out;
189
+ }
190
+
191
+ .filedrop-zone.filedrop-active .filedrop-icon {
192
+ color: #60a5fa;
193
+ transform: scale(1.2);
194
+ }
195
+
196
+ .filedrop-text {
197
+ font-size: 1rem;
198
+ font-weight: 500;
199
+ color: rgba(255, 255, 255, 0.9);
200
+ transition: color 0.2s ease-in-out;
201
+ }
202
+
203
+ .filedrop-zone.filedrop-active .filedrop-text {
204
+ color: #93c5fd;
205
+ font-weight: 600;
206
+ }
207
+
208
+ .filedrop-hint {
209
+ font-size: 0.875rem;
210
+ color: rgba(255, 255, 255, 0.6);
211
+ transition: color 0.2s ease-in-out;
212
+ }
213
+
214
+ .filedrop-zone.filedrop-active .filedrop-hint {
215
+ color: rgba(255, 255, 255, 0.8);
216
+ }
217
+
218
+ .filedrop-zone.filedrop-compact {
219
+ min-height: 120px;
220
+ padding: 1.5rem;
221
+ }
222
+
223
+ .filedrop-zone.filedrop-compact .filedrop-icon {
224
+ width: 2rem;
225
+ height: 2rem;
226
+ }
227
+
228
+ .filedrop-zone.filedrop-inline {
229
+ min-height: 80px;
230
+ padding: 1rem;
231
+ flex-direction: row;
232
+ justify-content: flex-start;
233
+ gap: 1rem;
234
+ }
235
+
236
+ .filedrop-zone.filedrop-inline .filedrop-content {
237
+ flex-direction: row;
238
+ align-items: center;
239
+ text-align: left;
240
+ gap: 0.75rem;
241
+ }
242
+
243
+ .filedrop-zone.filedrop-minimal {
244
+ border-style: solid;
245
+ border-width: 1px;
246
+ background-color: transparent;
247
+ }
248
+
249
+ .filedrop-zone.filedrop-bold {
250
+ border-width: 3px;
251
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
252
+ }
253
+
254
+ .filedrop-files {
255
+ margin-top: 1rem;
256
+ width: 100%;
257
+ }
258
+
259
+ .filedrop-file-item {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 0.75rem;
263
+ padding: 0.75rem;
264
+ background-color: rgba(255, 255, 255, 0.1);
265
+ border: 1px solid rgba(255, 255, 255, 0.2);
266
+ border-radius: 0.5rem;
267
+ margin-bottom: 0.5rem;
268
+ transition: all 0.2s ease-in-out;
269
+ }
270
+
271
+ .filedrop-file-item:hover {
272
+ background-color: rgba(255, 255, 255, 0.15);
273
+ border-color: rgba(255, 255, 255, 0.3);
274
+ }
275
+
276
+ .filedrop-file-icon {
277
+ width: 2rem;
278
+ height: 2rem;
279
+ flex-shrink: 0;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ background-color: rgba(255, 255, 255, 0.1);
284
+ border-radius: 0.375rem;
285
+ font-size: 1.2rem;
286
+ }
287
+
288
+ .filedrop-file-info {
289
+ flex: 1;
290
+ min-width: 0;
291
+ }
292
+
293
+ .filedrop-file-name {
294
+ font-size: 0.875rem;
295
+ font-weight: 500;
296
+ color: #fff;
297
+ white-space: nowrap;
298
+ overflow: hidden;
299
+ text-overflow: ellipsis;
300
+ }
301
+
302
+ .filedrop-file-meta {
303
+ font-size: 0.75rem;
304
+ color: rgba(255, 255, 255, 0.6);
305
+ margin-top: 0.125rem;
306
+ }
307
+
308
+ .filedrop-file-remove {
309
+ flex-shrink: 0;
310
+ width: 1.5rem;
311
+ height: 1.5rem;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ border-radius: 0.25rem;
316
+ color: rgba(255, 255, 255, 0.6);
317
+ cursor: pointer;
318
+ transition: all 0.2s ease-in-out;
319
+ pointer-events: auto;
320
+ background: transparent;
321
+ border: none;
322
+ font-size: 1rem;
323
+ }
324
+
325
+ .filedrop-file-remove:hover {
326
+ background-color: rgba(239, 68, 68, 0.2);
327
+ color: #fca5a5;
328
+ }
@@ -75,6 +75,7 @@ struct ServicesConfig {
75
75
  bool enableDisplay = true; // Enable multi-display detection
76
76
  bool enableKeyboard = true; // Enable keyboard shortcuts/hotkeys
77
77
  bool enableMenu = true; // Enable custom menu bar
78
+ bool enableFileDrop = true; // Enable drag & drop file handling
78
79
  } servicesConfig;
79
80
 
80
81
  // ============================================================================
@@ -160,6 +161,24 @@ int main() {
160
161
  // });
161
162
  // Call from JS: const version = await app.invoke('getVersion');
162
163
 
164
+ // ========================================
165
+ // FILE DROP EVENTS (Drag & Drop)
166
+ // ========================================
167
+ // Listen for files dropped into the window
168
+ plusui::event::on("fileDrop.filesDropped", [](const std::string& data) {
169
+ std::cout << "Files dropped: " << data << std::endl;
170
+ // Parse the JSON data to get file info
171
+ // You can process files here in C++
172
+ });
173
+
174
+ plusui::event::on("fileDrop.dragEnter", [](const std::string&) {
175
+ std::cout << "Drag entered window" << std::endl;
176
+ });
177
+
178
+ plusui::event::on("fileDrop.dragLeave", [](const std::string&) {
179
+ std::cout << "Drag left window" << std::endl;
180
+ });
181
+
163
182
  // ========================================
164
183
  // RUN APPLICATION
165
184
  // ========================================
@@ -189,4 +208,8 @@ int main() {
189
208
  // DISPLAY: display.getAll(), display.getPrimary(), display.getCurrent()
190
209
  //
191
210
  // CLIPBOARD: clipboard.writeText(str), clipboard.readText(), clipboard.clear()
211
+ //
212
+ // FILEDROP: fileDrop.onFilesDropped(callback), fileDrop.setEnabled(bool),
213
+ // fileDrop.onDragEnter(callback), fileDrop.onDragLeave(callback),
214
+ // fileDrop.startDrag([paths])
192
215