plusui-native 0.2.7 → 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.7",
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.7",
31
- "plusui-native-bindgen": "^0.1.7"
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.7"
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,11 +198,62 @@ 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
 
206
+ function findLikelyProjectDirs(baseDir) {
207
+ try {
208
+ const entries = execSync('npm pkg get name', { cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
209
+ if (entries) {
210
+ // noop; just to ensure cwd is a Node project when possible
211
+ }
212
+ } catch {}
213
+
214
+ const candidates = [];
215
+ try {
216
+ const dirs = execSync(process.platform === 'win32' ? 'dir /b /ad' : 'ls -1 -d */', {
217
+ cwd: baseDir,
218
+ encoding: 'utf8',
219
+ stdio: ['ignore', 'pipe', 'ignore'],
220
+ shell: true,
221
+ })
222
+ .split(/\r?\n/)
223
+ .map(s => s.trim().replace(/[\\/]$/, ''))
224
+ .filter(Boolean);
225
+
226
+ for (const dirName of dirs) {
227
+ const fullDir = join(baseDir, dirName);
228
+ if (existsSync(join(fullDir, 'CMakeLists.txt')) && existsSync(join(fullDir, 'package.json'))) {
229
+ candidates.push(dirName);
230
+ }
231
+ }
232
+ } catch {}
233
+
234
+ return candidates;
235
+ }
236
+
237
+ function ensureProjectRoot(commandName) {
238
+ const hasCMake = existsSync(join(process.cwd(), 'CMakeLists.txt'));
239
+ const hasPackage = existsSync(join(process.cwd(), 'package.json'));
240
+
241
+ if (hasCMake && hasPackage) {
242
+ return;
243
+ }
244
+
245
+ const likelyDirs = findLikelyProjectDirs(process.cwd());
246
+ let hint = '';
247
+
248
+ if (likelyDirs.length === 1) {
249
+ hint = `\n\nHint: you may be in a parent folder. Try:\n cd ${likelyDirs[0]}\n plusui ${commandName}`;
250
+ } else if (likelyDirs.length > 1) {
251
+ hint = `\n\nHint: run this command from your app folder (one containing CMakeLists.txt and package.json).`;
252
+ }
253
+
254
+ error(`This command must be run from a PlusUI project root (missing CMakeLists.txt and/or package.json in ${process.cwd()}).${hint}`);
255
+ }
256
+
206
257
  function ensureBuildLayout() {
207
258
  const buildRoot = join(process.cwd(), 'build');
208
259
  for (const platform of Object.values(PLATFORMS)) {
@@ -210,11 +261,20 @@ function ensureBuildLayout() {
210
261
  }
211
262
  }
212
263
 
264
+ function getDevBuildDir() {
265
+ const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
266
+ return join('.plusui', 'dev', platformFolder);
267
+ }
268
+
213
269
  function resolveBindgenScriptPath() {
214
270
  const candidates = [
271
+ resolve(__dirname, '../../plusui-bindgen/src/advanced-bindgen.js'),
215
272
  resolve(__dirname, '../../plusui-bindgen/src/index.js'),
273
+ resolve(__dirname, '../../plusui-native-bindgen/src/advanced-bindgen.js'),
216
274
  resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
275
+ resolve(__dirname, '../../../plusui-native-bindgen/src/advanced-bindgen.js'),
217
276
  resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
277
+ resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'advanced-bindgen.js'),
218
278
  resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
219
279
  ];
220
280
 
@@ -275,6 +335,7 @@ async function createProject(name, options = {}) {
275
335
  // ============================================================
276
336
 
277
337
  function buildFrontend() {
338
+ ensureProjectRoot('build:frontend');
278
339
  logSection('Building Frontend');
279
340
 
280
341
  if (existsSync('frontend')) {
@@ -287,6 +348,7 @@ function buildFrontend() {
287
348
  }
288
349
 
289
350
  function buildBackend(platform = null, devMode = false) {
351
+ ensureProjectRoot(devMode ? 'dev:backend' : 'build:backend');
290
352
  const targetPlatform = platform || process.platform;
291
353
  const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
292
354
 
@@ -329,6 +391,7 @@ function buildBackend(platform = null, devMode = false) {
329
391
  }
330
392
 
331
393
  async function generateIcons(inputPath = null) {
394
+ ensureProjectRoot('icons');
332
395
  logSection('Generating Platform Icons');
333
396
 
334
397
  const { IconGenerator } = await import('./assets/icon-generator.js');
@@ -347,6 +410,7 @@ async function generateIcons(inputPath = null) {
347
410
  }
348
411
 
349
412
  async function embedResources(platform = null) {
413
+ ensureProjectRoot('embed');
350
414
  const targetPlatform = platform || process.platform;
351
415
  logSection(`Embedding Resources (${targetPlatform})`);
352
416
 
@@ -369,6 +433,7 @@ async function embedResources(platform = null) {
369
433
  }
370
434
 
371
435
  async function embedAssets() {
436
+ ensureProjectRoot('build');
372
437
  const assetsDir = join(process.cwd(), 'assets');
373
438
  if (!existsSync(assetsDir)) {
374
439
  try {
@@ -417,6 +482,7 @@ async function embedAssets() {
417
482
 
418
483
 
419
484
  async function build(production = true) {
485
+ ensureProjectRoot('build');
420
486
  logSection('Building PlusUI Application');
421
487
 
422
488
  // Embed assets
@@ -435,6 +501,7 @@ async function build(production = true) {
435
501
  }
436
502
 
437
503
  function buildAll() {
504
+ ensureProjectRoot('build:all');
438
505
  logSection('Building for All Platforms');
439
506
 
440
507
  ensureBuildLayout();
@@ -474,6 +541,7 @@ let viteServer = null;
474
541
  let cppProcess = null;
475
542
 
476
543
  async function startViteServer() {
544
+ ensureProjectRoot('dev:frontend');
477
545
  log('Starting Vite dev server...', 'blue');
478
546
 
479
547
  viteServer = await createViteServer({
@@ -491,13 +559,13 @@ async function startViteServer() {
491
559
  }
492
560
 
493
561
  async function startBackend() {
562
+ ensureProjectRoot('dev');
494
563
  logSection('Building C++ Backend (Dev Mode)');
495
564
 
496
565
  const projectName = getProjectName();
497
566
  killProcessByName(projectName);
498
567
 
499
- const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
500
- const buildDir = join('build', platformFolder, 'dev');
568
+ const buildDir = getDevBuildDir();
501
569
 
502
570
  // Configure with dev mode if not configured
503
571
  if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
@@ -598,6 +666,7 @@ function killProcessByName(name) {
598
666
  }
599
667
 
600
668
  async function dev() {
669
+ ensureProjectRoot('dev');
601
670
  logSection('PlusUI Development Mode');
602
671
 
603
672
  const toolCheck = checkTools();
@@ -643,6 +712,7 @@ async function dev() {
643
712
  }
644
713
 
645
714
  async function devFrontend() {
715
+ ensureProjectRoot('dev:frontend');
646
716
  logSection('Frontend Development Mode');
647
717
 
648
718
  // specific port cleaning
@@ -665,13 +735,13 @@ async function devFrontend() {
665
735
  }
666
736
 
667
737
  function devBackend() {
738
+ ensureProjectRoot('dev:backend');
668
739
  logSection('Backend Development Mode');
669
740
 
670
741
  const projectName = getProjectName();
671
742
  killProcessByName(projectName);
672
743
 
673
- const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
674
- const buildDir = join('build', platformFolder, 'dev');
744
+ const buildDir = getDevBuildDir();
675
745
 
676
746
  if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
677
747
  log('Configuring CMake...', 'blue');
@@ -742,6 +812,7 @@ function devBackend() {
742
812
  // ============================================================
743
813
 
744
814
  function run() {
815
+ ensureProjectRoot('run');
745
816
  const projectName = getProjectName();
746
817
  const platform = PLATFORMS[process.platform];
747
818
  const buildDir = `build/${platform?.folder || 'Windows'}`;
@@ -774,9 +845,10 @@ function run() {
774
845
  // ============================================================
775
846
 
776
847
  async function clean() {
848
+ ensureProjectRoot('clean');
777
849
  logSection('Cleaning Build Artifacts');
778
850
 
779
- const dirs = ['build', 'frontend/dist'];
851
+ const dirs = ['build', '.plusui', 'frontend/dist'];
780
852
 
781
853
  for (const dir of dirs) {
782
854
  if (existsSync(dir)) {
@@ -793,6 +865,7 @@ async function clean() {
793
865
  // ============================================================
794
866
 
795
867
  async function runBindgen(providedArgs = null, options = {}) {
868
+ ensureProjectRoot('bindgen');
796
869
  logSection('Running Binding Generator');
797
870
 
798
871
  const { skipIfNoInput = false, source = 'manual' } = options;
@@ -815,22 +888,12 @@ async function runBindgen(providedArgs = null, options = {}) {
815
888
  let defaultFrontendOutputDir = null;
816
889
 
817
890
  if (bindgenArgs.length === 0) {
818
- const { featuresDir: appFeaturesDir, outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
819
-
820
- if (existsSync(appFeaturesDir)) {
821
- bindgenArgs = [appFeaturesDir, appOutputDir];
822
- usedDefaultAppMode = true;
823
- defaultOutputDir = appOutputDir;
824
- defaultFrontendOutputDir = frontendOutputDir;
825
- log(`App mode: ${appFeaturesDir} -> ${appOutputDir}`, 'dim');
826
- } else {
827
- if (skipIfNoInput) {
828
- log(`No src/features folder found; skipping binding refresh for ${source}.`, 'dim');
829
- return;
830
- } else {
831
- error('No bindgen input found. Create src/features in your app or pass paths: plusui bindgen <featuresDir> <outputDir>');
832
- }
833
- }
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');
834
897
  }
835
898
 
836
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