plusui-native-core 0.1.7 → 0.1.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.
@@ -0,0 +1,421 @@
1
+ /**
2
+ * FileDrop Styles - Drop zone styling for drag & drop functionality
3
+ *
4
+ * Usage:
5
+ * <div class="filedrop-zone">
6
+ * Drop files here
7
+ * </div>
8
+ *
9
+ * States automatically applied via JavaScript:
10
+ * - .filedrop-active: When drag enters the zone
11
+ * - .filedrop-disabled: When file drop is disabled
12
+ */
13
+
14
+ /* ============================================================================
15
+ * BASE DROP ZONE STYLES
16
+ * ============================================================================ */
17
+
18
+ .filedrop-zone {
19
+ position: relative;
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: center;
23
+ justify-content: center;
24
+ min-height: 200px;
25
+ padding: 2rem;
26
+ border: 2px dashed #cbd5e1;
27
+ border-radius: 0.75rem;
28
+ background-color: #f8fafc;
29
+ transition: all 0.2s ease-in-out;
30
+ cursor: pointer;
31
+ user-select: none;
32
+ }
33
+
34
+ .filedrop-zone:hover {
35
+ border-color: #94a3b8;
36
+ background-color: #f1f5f9;
37
+ }
38
+
39
+ /* ============================================================================
40
+ * ACTIVE STATE (Drag Over)
41
+ * ============================================================================ */
42
+
43
+ .filedrop-zone.filedrop-active {
44
+ border-color: #3b82f6;
45
+ background-color: #eff6ff;
46
+ border-width: 3px;
47
+ transform: scale(1.02);
48
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
49
+ }
50
+
51
+ .filedrop-zone.filedrop-active::before {
52
+ content: '';
53
+ position: absolute;
54
+ inset: 0;
55
+ border-radius: 0.5rem;
56
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(147, 51, 234, 0.05));
57
+ pointer-events: none;
58
+ }
59
+
60
+ /* ============================================================================
61
+ * DISABLED STATE
62
+ * ============================================================================ */
63
+
64
+ .filedrop-zone.filedrop-disabled {
65
+ opacity: 0.5;
66
+ cursor: not-allowed;
67
+ border-color: #e2e8f0;
68
+ background-color: #f8fafc;
69
+ }
70
+
71
+ .filedrop-zone.filedrop-disabled:hover {
72
+ border-color: #e2e8f0;
73
+ background-color: #f8fafc;
74
+ transform: none;
75
+ }
76
+
77
+ /* ============================================================================
78
+ * DROP ZONE CONTENT
79
+ * ============================================================================ */
80
+
81
+ .filedrop-content {
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ gap: 1rem;
86
+ text-align: center;
87
+ pointer-events: none;
88
+ }
89
+
90
+ .filedrop-icon {
91
+ width: 3rem;
92
+ height: 3rem;
93
+ color: #94a3b8;
94
+ transition: all 0.2s ease-in-out;
95
+ }
96
+
97
+ .filedrop-zone.filedrop-active .filedrop-icon {
98
+ color: #3b82f6;
99
+ transform: scale(1.2);
100
+ }
101
+
102
+ .filedrop-zone.filedrop-disabled .filedrop-icon {
103
+ color: #cbd5e1;
104
+ }
105
+
106
+ .filedrop-text {
107
+ font-size: 1rem;
108
+ font-weight: 500;
109
+ color: #475569;
110
+ transition: color 0.2s ease-in-out;
111
+ }
112
+
113
+ .filedrop-zone.filedrop-active .filedrop-text {
114
+ color: #3b82f6;
115
+ font-weight: 600;
116
+ }
117
+
118
+ .filedrop-zone.filedrop-disabled .filedrop-text {
119
+ color: #cbd5e1;
120
+ }
121
+
122
+ .filedrop-hint {
123
+ font-size: 0.875rem;
124
+ color: #94a3b8;
125
+ transition: color 0.2s ease-in-out;
126
+ }
127
+
128
+ .filedrop-zone.filedrop-active .filedrop-hint {
129
+ color: #60a5fa;
130
+ }
131
+
132
+ .filedrop-zone.filedrop-disabled .filedrop-hint {
133
+ color: #cbd5e1;
134
+ }
135
+
136
+ /* ============================================================================
137
+ * COMPACT VARIANT
138
+ * ============================================================================ */
139
+
140
+ .filedrop-zone.filedrop-compact {
141
+ min-height: 120px;
142
+ padding: 1.5rem;
143
+ }
144
+
145
+ .filedrop-zone.filedrop-compact .filedrop-icon {
146
+ width: 2rem;
147
+ height: 2rem;
148
+ }
149
+
150
+ .filedrop-zone.filedrop-compact .filedrop-text {
151
+ font-size: 0.875rem;
152
+ }
153
+
154
+ .filedrop-zone.filedrop-compact .filedrop-hint {
155
+ font-size: 0.75rem;
156
+ }
157
+
158
+ /* ============================================================================
159
+ * INLINE VARIANT (Full Width)
160
+ * ============================================================================ */
161
+
162
+ .filedrop-zone.filedrop-inline {
163
+ min-height: 80px;
164
+ padding: 1rem;
165
+ flex-direction: row;
166
+ justify-content: flex-start;
167
+ gap: 1rem;
168
+ }
169
+
170
+ .filedrop-zone.filedrop-inline .filedrop-content {
171
+ flex-direction: row;
172
+ align-items: center;
173
+ text-align: left;
174
+ gap: 0.75rem;
175
+ }
176
+
177
+ .filedrop-zone.filedrop-inline .filedrop-icon {
178
+ width: 2rem;
179
+ height: 2rem;
180
+ }
181
+
182
+ /* ============================================================================
183
+ * FILE LIST DISPLAY
184
+ * ============================================================================ */
185
+
186
+ .filedrop-files {
187
+ margin-top: 1rem;
188
+ width: 100%;
189
+ }
190
+
191
+ .filedrop-file-item {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 0.75rem;
195
+ padding: 0.75rem;
196
+ background-color: white;
197
+ border: 1px solid #e2e8f0;
198
+ border-radius: 0.5rem;
199
+ margin-bottom: 0.5rem;
200
+ transition: all 0.2s ease-in-out;
201
+ }
202
+
203
+ .filedrop-file-item:hover {
204
+ border-color: #cbd5e1;
205
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
206
+ }
207
+
208
+ .filedrop-file-icon {
209
+ width: 2rem;
210
+ height: 2rem;
211
+ flex-shrink: 0;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ background-color: #f1f5f9;
216
+ border-radius: 0.375rem;
217
+ color: #64748b;
218
+ }
219
+
220
+ .filedrop-file-info {
221
+ flex: 1;
222
+ min-width: 0;
223
+ }
224
+
225
+ .filedrop-file-name {
226
+ font-size: 0.875rem;
227
+ font-weight: 500;
228
+ color: #334155;
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ }
233
+
234
+ .filedrop-file-meta {
235
+ font-size: 0.75rem;
236
+ color: #94a3b8;
237
+ margin-top: 0.125rem;
238
+ }
239
+
240
+ .filedrop-file-remove {
241
+ flex-shrink: 0;
242
+ width: 1.5rem;
243
+ height: 1.5rem;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ border-radius: 0.25rem;
248
+ color: #94a3b8;
249
+ cursor: pointer;
250
+ transition: all 0.2s ease-in-out;
251
+ pointer-events: auto;
252
+ }
253
+
254
+ .filedrop-file-remove:hover {
255
+ background-color: #fee2e2;
256
+ color: #ef4444;
257
+ }
258
+
259
+ /* ============================================================================
260
+ * DARK MODE SUPPORT
261
+ * ============================================================================ */
262
+
263
+ @media (prefers-color-scheme: dark) {
264
+ .filedrop-zone {
265
+ border-color: #475569;
266
+ background-color: #1e293b;
267
+ }
268
+
269
+ .filedrop-zone:hover {
270
+ border-color: #64748b;
271
+ background-color: #334155;
272
+ }
273
+
274
+ .filedrop-zone.filedrop-active {
275
+ border-color: #60a5fa;
276
+ background-color: #1e3a8a;
277
+ box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.1);
278
+ }
279
+
280
+ .filedrop-zone.filedrop-active::before {
281
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.1), rgba(168, 85, 247, 0.1));
282
+ }
283
+
284
+ .filedrop-zone.filedrop-disabled {
285
+ border-color: #334155;
286
+ background-color: #1e293b;
287
+ }
288
+
289
+ .filedrop-icon {
290
+ color: #64748b;
291
+ }
292
+
293
+ .filedrop-zone.filedrop-active .filedrop-icon {
294
+ color: #60a5fa;
295
+ }
296
+
297
+ .filedrop-zone.filedrop-disabled .filedrop-icon {
298
+ color: #475569;
299
+ }
300
+
301
+ .filedrop-text {
302
+ color: #cbd5e1;
303
+ }
304
+
305
+ .filedrop-zone.filedrop-active .filedrop-text {
306
+ color: #93c5fd;
307
+ }
308
+
309
+ .filedrop-zone.filedrop-disabled .filedrop-text {
310
+ color: #64748b;
311
+ }
312
+
313
+ .filedrop-hint {
314
+ color: #64748b;
315
+ }
316
+
317
+ .filedrop-zone.filedrop-active .filedrop-hint {
318
+ color: #93c5fd;
319
+ }
320
+
321
+ .filedrop-file-item {
322
+ background-color: #334155;
323
+ border-color: #475569;
324
+ }
325
+
326
+ .filedrop-file-item:hover {
327
+ border-color: #64748b;
328
+ }
329
+
330
+ .filedrop-file-icon {
331
+ background-color: #1e293b;
332
+ color: #94a3b8;
333
+ }
334
+
335
+ .filedrop-file-name {
336
+ color: #e2e8f0;
337
+ }
338
+
339
+ .filedrop-file-meta {
340
+ color: #64748b;
341
+ }
342
+
343
+ .filedrop-file-remove:hover {
344
+ background-color: #7f1d1d;
345
+ color: #fca5a5;
346
+ }
347
+ }
348
+
349
+ /* ============================================================================
350
+ * ANIMATION UTILITIES
351
+ * ============================================================================ */
352
+
353
+ @keyframes filedrop-pulse {
354
+ 0%, 100% {
355
+ opacity: 1;
356
+ }
357
+ 50% {
358
+ opacity: 0.7;
359
+ }
360
+ }
361
+
362
+ .filedrop-zone.filedrop-processing {
363
+ animation: filedrop-pulse 1.5s ease-in-out infinite;
364
+ pointer-events: none;
365
+ }
366
+
367
+ @keyframes filedrop-shake {
368
+ 0%, 100% {
369
+ transform: translateX(0);
370
+ }
371
+ 25% {
372
+ transform: translateX(-4px);
373
+ }
374
+ 75% {
375
+ transform: translateX(4px);
376
+ }
377
+ }
378
+
379
+ .filedrop-zone.filedrop-error {
380
+ border-color: #ef4444;
381
+ background-color: #fee2e2;
382
+ animation: filedrop-shake 0.3s ease-in-out;
383
+ }
384
+
385
+ @media (prefers-color-scheme: dark) {
386
+ .filedrop-zone.filedrop-error {
387
+ border-color: #f87171;
388
+ background-color: #7f1d1d;
389
+ }
390
+ }
391
+
392
+ /* ============================================================================
393
+ * CUSTOM VARIANTS
394
+ * ============================================================================ */
395
+
396
+ /* Minimal variant - subtle styling */
397
+ .filedrop-zone.filedrop-minimal {
398
+ border-style: solid;
399
+ border-width: 1px;
400
+ background-color: transparent;
401
+ }
402
+
403
+ .filedrop-zone.filedrop-minimal:hover {
404
+ background-color: rgba(0, 0, 0, 0.02);
405
+ }
406
+
407
+ @media (prefers-color-scheme: dark) {
408
+ .filedrop-zone.filedrop-minimal:hover {
409
+ background-color: rgba(255, 255, 255, 0.02);
410
+ }
411
+ }
412
+
413
+ /* Bold variant - prominent styling */
414
+ .filedrop-zone.filedrop-bold {
415
+ border-width: 3px;
416
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
417
+ }
418
+
419
+ .filedrop-zone.filedrop-bold.filedrop-active {
420
+ box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
421
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * FileDrop API - Cross-platform drag & drop file handling
3
+ *
4
+ * Enables dragging files into the webview window and from the window to the desktop.
5
+ * Supports all file types cross-platform (Windows, macOS, Linux).
6
+ */
7
+
8
+ /**
9
+ * Information about a dropped file
10
+ */
11
+ export interface FileInfo {
12
+ /** Full path to the file */
13
+ path: string;
14
+ /** File name with extension */
15
+ name: string;
16
+ /** MIME type (e.g., "image/png", "text/plain") */
17
+ type: string;
18
+ /** File size in bytes */
19
+ size: number;
20
+ }
21
+
22
+ /**
23
+ * Drag event data
24
+ */
25
+ export interface DragEvent {
26
+ /** X coordinate of drag position */
27
+ x?: number;
28
+ /** Y coordinate of drag position */
29
+ y?: number;
30
+ }
31
+
32
+ export interface FileDropAPI {
33
+ /**
34
+ * Enable or disable file drop into window
35
+ * @param enabled - true to allow files to be dropped into the window
36
+ */
37
+ setEnabled(enabled: boolean): Promise<void>;
38
+
39
+ /**
40
+ * Check if file drop is currently enabled
41
+ * @returns true if file drop is enabled
42
+ */
43
+ isEnabled(): Promise<boolean>;
44
+
45
+ /**
46
+ * Start a drag operation to drag files out of the window
47
+ * @param filePaths - Paths to files to be dragged
48
+ * @returns true if drag operation started successfully
49
+ */
50
+ startDrag(filePaths: string[]): Promise<boolean>;
51
+
52
+ /**
53
+ * Clear all callbacks
54
+ */
55
+ clearCallbacks(): Promise<void>;
56
+
57
+ /**
58
+ * Listen for files dropped into the window
59
+ * @param callback - Function called with array of dropped files
60
+ * @returns Unsubscribe function
61
+ */
62
+ onFilesDropped(callback: (files: FileInfo[]) => void): () => void;
63
+
64
+ /**
65
+ * Listen for drag enter events
66
+ * @param callback - Function called when drag enters the window
67
+ * @returns Unsubscribe function
68
+ */
69
+ onDragEnter(callback: (event: DragEvent) => void): () => void;
70
+
71
+ /**
72
+ * Listen for drag leave events
73
+ * @param callback - Function called when drag leaves the window
74
+ * @returns Unsubscribe function
75
+ */
76
+ onDragLeave(callback: (event: DragEvent) => void): () => void;
77
+ }
78
+
79
+ export class FileDrop implements FileDropAPI {
80
+ private invokeFn: (name: string, args?: unknown[]) => Promise<unknown>;
81
+ private onFn: (event: string, callback: (data: unknown) => void) => () => void;
82
+
83
+ constructor(
84
+ invokeFn: (name: string, args?: unknown[]) => Promise<unknown>,
85
+ onFn: (event: string, callback: (data: unknown) => void) => () => void
86
+ ) {
87
+ this.invokeFn = invokeFn;
88
+ this.onFn = onFn;
89
+ }
90
+
91
+ /**
92
+ * Enable or disable file drop into window
93
+ * @param enabled - true to allow files to be dropped into the window
94
+ */
95
+ async setEnabled(enabled: boolean): Promise<void> {
96
+ await this.invokeFn('fileDrop.setEnabled', [enabled]);
97
+ }
98
+
99
+ /**
100
+ * Check if file drop is currently enabled
101
+ * @returns true if file drop is enabled
102
+ */
103
+ async isEnabled(): Promise<boolean> {
104
+ return (await this.invokeFn('fileDrop.isEnabled')) as boolean;
105
+ }
106
+
107
+ /**
108
+ * Start a drag operation to drag files out of the window
109
+ * @param filePaths - Paths to files to be dragged
110
+ * @returns true if drag operation started successfully
111
+ */
112
+ async startDrag(filePaths: string[]): Promise<boolean> {
113
+ if (!filePaths || filePaths.length === 0) {
114
+ throw new Error('filePaths must be a non-empty array');
115
+ }
116
+ return (await this.invokeFn('fileDrop.startDrag', [filePaths])) as boolean;
117
+ }
118
+
119
+ /**
120
+ * Clear all callbacks
121
+ */
122
+ async clearCallbacks(): Promise<void> {
123
+ await this.invokeFn('fileDrop.clearCallbacks');
124
+ }
125
+
126
+ /**
127
+ * Listen for files dropped into the window
128
+ * @param callback - Function called with array of dropped files
129
+ * @returns Unsubscribe function
130
+ */
131
+ onFilesDropped(callback: (files: FileInfo[]) => void): () => void {
132
+ return this.onFn('fileDrop.filesDropped', (data) => {
133
+ callback(data as FileInfo[]);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Listen for drag enter events
139
+ * @param callback - Function called when drag enters the window
140
+ * @returns Unsubscribe function
141
+ */
142
+ onDragEnter(callback: (event: DragEvent) => void): () => void {
143
+ return this.onFn('fileDrop.dragEnter', (data) => {
144
+ callback((data as DragEvent) || {});
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Listen for drag leave events
150
+ * @param callback - Function called when drag leaves the window
151
+ * @returns Unsubscribe function
152
+ */
153
+ onDragLeave(callback: (event: DragEvent) => void): () => void {
154
+ return this.onFn('fileDrop.dragLeave', (data) => {
155
+ callback((data as DragEvent) || {});
156
+ });
157
+ }
158
+ }
159
+
160
+ // Helper functions for common use cases
161
+
162
+ /**
163
+ * Read dropped file as text
164
+ * @param filePath - Path to the file
165
+ * @returns File contents as text
166
+ */
167
+ export async function readFileAsText(filePath: string): Promise<string> {
168
+ const response = await fetch(`file://${filePath}`);
169
+ return await response.text();
170
+ }
171
+
172
+ /**
173
+ * Read dropped file as data URL (useful for images)
174
+ * @param filePath - Path to the file
175
+ * @returns File contents as data URL
176
+ */
177
+ export async function readFileAsDataUrl(filePath: string): Promise<string> {
178
+ const response = await fetch(`file://${filePath}`);
179
+ const blob = await response.blob();
180
+ return new Promise((resolve, reject) => {
181
+ const reader = new FileReader();
182
+ reader.onload = () => resolve(reader.result as string);
183
+ reader.onerror = reject;
184
+ reader.readAsDataURL(blob);
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Filter files by extension
190
+ * @param files - Array of file info
191
+ * @param extensions - Array of extensions to filter (e.g., ['.png', '.jpg'])
192
+ * @returns Filtered array of files
193
+ */
194
+ export function filterFilesByExtension(
195
+ files: FileInfo[],
196
+ extensions: string[]
197
+ ): FileInfo[] {
198
+ const lowerExtensions = extensions.map((ext) => ext.toLowerCase());
199
+ return files.filter((file) => {
200
+ const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
201
+ return lowerExtensions.includes(ext);
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Filter files by MIME type
207
+ * @param files - Array of file info
208
+ * @param mimeTypes - Array of MIME types to filter (e.g., ['image/png', 'image/jpeg'])
209
+ * @returns Filtered array of files
210
+ */
211
+ export function filterFilesByMimeType(
212
+ files: FileInfo[],
213
+ mimeTypes: string[]
214
+ ): FileInfo[] {
215
+ return files.filter((file) => mimeTypes.includes(file.type));
216
+ }
217
+
218
+ /**
219
+ * Check if file is an image
220
+ * @param file - File info
221
+ * @returns true if file is an image
222
+ */
223
+ export function isImageFile(file: FileInfo): boolean {
224
+ return file.type.startsWith('image/');
225
+ }
226
+
227
+ /**
228
+ * Check if file is a video
229
+ * @param file - File info
230
+ * @returns true if file is a video
231
+ */
232
+ export function isVideoFile(file: FileInfo): boolean {
233
+ return file.type.startsWith('video/');
234
+ }
235
+
236
+ /**
237
+ * Check if file is audio
238
+ * @param file - File info
239
+ * @returns true if file is audio
240
+ */
241
+ export function isAudioFile(file: FileInfo): boolean {
242
+ return file.type.startsWith('audio/');
243
+ }
244
+
245
+ /**
246
+ * Check if file is text
247
+ * @param file - File info
248
+ * @returns true if file is text
249
+ */
250
+ export function isTextFile(file: FileInfo): boolean {
251
+ return file.type.startsWith('text/') ||
252
+ file.type === 'application/json' ||
253
+ file.type === 'application/xml';
254
+ }
255
+
256
+ /**
257
+ * Format file size for display
258
+ * @param bytes - File size in bytes
259
+ * @returns Formatted string (e.g., "1.5 MB")
260
+ */
261
+ export function formatFileSize(bytes: number): string {
262
+ if (bytes === 0) return '0 Bytes';
263
+
264
+ const k = 1024;
265
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
266
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
267
+
268
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
269
+ }