@shipstatic/drop 0.1.6 → 0.1.8
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/README.md +146 -27
- package/dist/index.cjs +136 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -9
- package/dist/index.d.ts +23 -9
- package/dist/index.js +137 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
A focused React hook for preparing files for deployment with [@shipstatic/ship](https://github.com/shipstatic/ship). Handles ZIP extraction, path normalization, and validation - everything needed before calling `ship.deploy()`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Built-in drag & drop support with prop getters! No need to manually implement drag & drop handlers - just spread `{...drop.getDropzoneProps()}` on your container and `{...drop.getInputProps()}` on the input element. Folder structure preservation is handled automatically.
|
|
8
8
|
|
|
9
9
|
**Note:** MD5 calculation is handled by Ship SDK during deployment. Drop focuses on file processing and UI state management.
|
|
10
10
|
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Why Headless?](#why-headless)
|
|
14
|
+
- [Features](#features)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Requirements](#requirements)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Configuration Architecture](#️-configuration-architecture)
|
|
19
|
+
- [Advanced Usage](#advanced-programmatic-file-picker)
|
|
20
|
+
- [API Reference](#api)
|
|
21
|
+
- [State Machine](#state-machine)
|
|
22
|
+
- [Error Handling](#error-handling)
|
|
23
|
+
- [Types](#types)
|
|
24
|
+
- [Ship SDK Integration](#direct-ship-sdk-integration)
|
|
25
|
+
- [Architecture Decisions](#architecture-decisions)
|
|
26
|
+
|
|
11
27
|
## Why Headless?
|
|
12
28
|
|
|
13
29
|
This package provides **zero UI components** - just a React hook with built-in drag & drop functionality. You bring your own styling.
|
|
@@ -16,8 +32,7 @@ This package provides **zero UI components** - just a React hook with built-in d
|
|
|
16
32
|
1. **Built-in drag & drop** - Proper folder support with `webkitGetAsEntry` API, all handled internally
|
|
17
33
|
2. **Prop getters API** - Similar to `react-dropzone`, just spread props on your elements
|
|
18
34
|
3. **Full styling control** - No imposed CSS, design system, or theming
|
|
19
|
-
4. **
|
|
20
|
-
5. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
|
|
35
|
+
4. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
|
|
21
36
|
|
|
22
37
|
**What's different from other libraries:**
|
|
23
38
|
- Generic dropzone libraries don't preserve folder structure properly
|
|
@@ -34,6 +49,7 @@ This package provides **zero UI components** - just a React hook with built-in d
|
|
|
34
49
|
- 🔒 **Path Sanitization** - Defense-in-depth protection against directory traversal attacks
|
|
35
50
|
- 📁 **Folder Structure Preservation** - Proper folder paths via `webkitRelativePath`
|
|
36
51
|
- 🎨 **Headless UI** - No visual components, just logic and state management
|
|
52
|
+
- 📘 **Full TypeScript Support** - Complete type definitions with discriminated unions for state machine
|
|
37
53
|
- 🚀 **Focused Scope** - File processing and UI state only. MD5 calculation and deployment handled by Ship SDK
|
|
38
54
|
|
|
39
55
|
## Installation
|
|
@@ -44,6 +60,17 @@ npm install @shipstatic/drop
|
|
|
44
60
|
pnpm add @shipstatic/drop
|
|
45
61
|
```
|
|
46
62
|
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- **React**: ^18.0.0 or ^19.0.0
|
|
66
|
+
- **TypeScript**: Full TypeScript support with exported types
|
|
67
|
+
- **Browsers**: Modern browsers with support for:
|
|
68
|
+
- File API (universal support)
|
|
69
|
+
- DataTransfer API for drag & drop (universal support)
|
|
70
|
+
- `webkitGetAsEntry` for folder uploads (Chrome, Edge, Safari 11.1+, Firefox 50+)
|
|
71
|
+
|
|
72
|
+
**Note on folder uploads**: The folder drag & drop feature uses the `webkitGetAsEntry` API. While widely supported, older browsers may only support file-by-file selection. ZIP extraction works universally as a fallback.
|
|
73
|
+
|
|
47
74
|
## Quick Start
|
|
48
75
|
|
|
49
76
|
```tsx
|
|
@@ -80,11 +107,15 @@ function MyUploader() {
|
|
|
80
107
|
{drop.isDragging ? '📂 Drop here' : '📁 Click or drag files/folders'}
|
|
81
108
|
</div>
|
|
82
109
|
|
|
83
|
-
{/* Status */}
|
|
84
|
-
|
|
110
|
+
{/* Status - using state machine */}
|
|
111
|
+
{drop.state.status && (
|
|
112
|
+
<p>
|
|
113
|
+
<strong>{drop.state.status.title}:</strong> {drop.state.status.details}
|
|
114
|
+
</p>
|
|
115
|
+
)}
|
|
85
116
|
|
|
86
117
|
{/* File list */}
|
|
87
|
-
{drop.files.map(file => (
|
|
118
|
+
{drop.state.files.map(file => (
|
|
88
119
|
<div key={file.id}>
|
|
89
120
|
{file.name} - {file.status}
|
|
90
121
|
</div>
|
|
@@ -93,7 +124,7 @@ function MyUploader() {
|
|
|
93
124
|
{/* Upload button */}
|
|
94
125
|
<button
|
|
95
126
|
onClick={handleUpload}
|
|
96
|
-
disabled={drop.
|
|
127
|
+
disabled={drop.state.value !== 'ready'}
|
|
97
128
|
>
|
|
98
129
|
Upload {drop.getValidFiles().length} files
|
|
99
130
|
</button>
|
|
@@ -203,19 +234,15 @@ interface DropOptions {
|
|
|
203
234
|
|
|
204
235
|
```typescript
|
|
205
236
|
interface DropReturn {
|
|
206
|
-
// State
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** Current status text */
|
|
212
|
-
statusText: string;
|
|
237
|
+
// State machine
|
|
238
|
+
/** Current state of the drop hook */
|
|
239
|
+
state: DropState;
|
|
240
|
+
|
|
241
|
+
// Convenience getters (computed from state)
|
|
213
242
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
214
243
|
isProcessing: boolean;
|
|
215
244
|
/** Whether user is currently dragging over the dropzone */
|
|
216
245
|
isDragging: boolean;
|
|
217
|
-
/** Last validation error if any */
|
|
218
|
-
validationError: ClientError | null;
|
|
219
246
|
|
|
220
247
|
// Primary API: Prop getters for easy integration
|
|
221
248
|
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
@@ -231,7 +258,7 @@ interface DropReturn {
|
|
|
231
258
|
type: 'file';
|
|
232
259
|
style: { display: string };
|
|
233
260
|
multiple: boolean;
|
|
234
|
-
webkitdirectory: string;
|
|
261
|
+
webkitdirectory: string; // Note: React expects string ('') for boolean attributes
|
|
235
262
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
236
263
|
};
|
|
237
264
|
|
|
@@ -253,19 +280,111 @@ interface DropReturn {
|
|
|
253
280
|
progress?: number;
|
|
254
281
|
}) => void;
|
|
255
282
|
}
|
|
283
|
+
|
|
284
|
+
// State machine types
|
|
285
|
+
type DropStateValue =
|
|
286
|
+
| 'idle' // The hook is ready for files
|
|
287
|
+
| 'dragging' // The user is dragging files over the dropzone
|
|
288
|
+
| 'processing' // Files are being validated and processed
|
|
289
|
+
| 'ready' // Files are valid and ready for deployment
|
|
290
|
+
| 'error'; // An error occurred during processing
|
|
291
|
+
|
|
292
|
+
interface DropState {
|
|
293
|
+
value: DropStateValue;
|
|
294
|
+
files: ProcessedFile[];
|
|
295
|
+
sourceName: string;
|
|
296
|
+
status: DropStatus | null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface DropStatus {
|
|
300
|
+
title: string;
|
|
301
|
+
details: string;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## State Machine
|
|
306
|
+
|
|
307
|
+
The drop hook uses a state machine for predictable, clear state management. Instead of multiple boolean flags, you have a single `state.value` that represents exactly what's happening.
|
|
308
|
+
|
|
309
|
+
### State Flow
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
idle → dragging → idle (drag leave without drop)
|
|
313
|
+
idle → dragging → processing → ready (successful)
|
|
314
|
+
idle → dragging → processing → error (failed)
|
|
315
|
+
ready → dragging → processing → ... (new drop)
|
|
316
|
+
error → dragging → processing → ... (retry)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Using the State Machine
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
function StatusIndicator({ drop }) {
|
|
323
|
+
const { state } = drop;
|
|
324
|
+
|
|
325
|
+
switch (state.value) {
|
|
326
|
+
case 'idle':
|
|
327
|
+
return <p>Drop files here or click to select</p>;
|
|
328
|
+
|
|
329
|
+
case 'dragging':
|
|
330
|
+
return <p>Drop your files now!</p>;
|
|
331
|
+
|
|
332
|
+
case 'processing':
|
|
333
|
+
return <p>{state.status?.details || 'Processing...'}</p>;
|
|
334
|
+
|
|
335
|
+
case 'ready':
|
|
336
|
+
return (
|
|
337
|
+
<div>
|
|
338
|
+
<p>✓ {state.files.length} files ready</p>
|
|
339
|
+
<button>Upload to Ship</button>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
case 'error':
|
|
344
|
+
return (
|
|
345
|
+
<div>
|
|
346
|
+
<p>✗ {state.status?.title}</p>
|
|
347
|
+
<p>{state.status?.details}</p>
|
|
348
|
+
<button onClick={drop.clearAll}>Try Again</button>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Convenience Getters
|
|
356
|
+
|
|
357
|
+
For simpler use cases, boolean convenience getters are provided:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// These are computed from state.value (read-only projections)
|
|
361
|
+
drop.isProcessing // true when state.value === 'processing'
|
|
362
|
+
drop.isDragging // true when state.value === 'dragging'
|
|
363
|
+
|
|
364
|
+
// For error information, use the state object
|
|
365
|
+
drop.state.value === 'error' // Check if in error state
|
|
366
|
+
drop.state.status?.title // Error title
|
|
367
|
+
drop.state.status?.details // Error details
|
|
256
368
|
```
|
|
257
369
|
|
|
370
|
+
### Benefits
|
|
371
|
+
|
|
372
|
+
- **No impossible states** - Can't be `isProcessing=true` AND `isDragging=true`
|
|
373
|
+
- **Clear transitions** - State flow is explicit and predictable
|
|
374
|
+
- **Better TypeScript** - Discriminated unions provide type safety
|
|
375
|
+
- **Easier debugging** - Single source of truth for what's happening
|
|
376
|
+
|
|
258
377
|
## Error Handling
|
|
259
378
|
|
|
260
379
|
### Per-File Error Display
|
|
261
380
|
|
|
262
|
-
Each file in the `files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
381
|
+
Each file in the `state.files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
263
382
|
|
|
264
383
|
```tsx
|
|
265
384
|
function FileList({ drop }) {
|
|
266
385
|
return (
|
|
267
386
|
<div>
|
|
268
|
-
{drop.files.map(file => (
|
|
387
|
+
{drop.state.files.map(file => (
|
|
269
388
|
<div key={file.id}>
|
|
270
389
|
<span>{file.path}</span>
|
|
271
390
|
|
|
@@ -282,7 +401,7 @@ function FileList({ drop }) {
|
|
|
282
401
|
))}
|
|
283
402
|
|
|
284
403
|
{/* If validation fails, allow user to clear all and try again */}
|
|
285
|
-
{drop.
|
|
404
|
+
{drop.state.value === 'error' && (
|
|
286
405
|
<button onClick={drop.clearAll}>
|
|
287
406
|
Clear All & Try Again
|
|
288
407
|
</button>
|
|
@@ -298,15 +417,15 @@ function FileList({ drop }) {
|
|
|
298
417
|
- `empty_file` - File is 0 bytes
|
|
299
418
|
- `ready` - File passed all validation and is ready for upload
|
|
300
419
|
|
|
301
|
-
###
|
|
420
|
+
### Error State Summary
|
|
302
421
|
|
|
303
|
-
|
|
422
|
+
When files fail validation or processing, check the error state:
|
|
304
423
|
|
|
305
424
|
```tsx
|
|
306
|
-
{drop.
|
|
425
|
+
{drop.state.value === 'error' && drop.state.status && (
|
|
307
426
|
<div>
|
|
308
|
-
<p>{drop.
|
|
309
|
-
<p>{drop.
|
|
427
|
+
<p>{drop.state.status.title}</p>
|
|
428
|
+
<p>{drop.state.status.details}</p>
|
|
310
429
|
</div>
|
|
311
430
|
)}
|
|
312
431
|
```
|
|
@@ -336,10 +455,10 @@ Use `clearAll()` to reset and try again:
|
|
|
336
455
|
|
|
337
456
|
```tsx
|
|
338
457
|
// If validation fails, show user which files failed
|
|
339
|
-
{drop.
|
|
458
|
+
{drop.state.value === 'error' && (
|
|
340
459
|
<div>
|
|
341
460
|
<p>Validation failed. Please fix the issues and try again:</p>
|
|
342
|
-
{drop.files.map(file => (
|
|
461
|
+
{drop.state.files.map(file => (
|
|
343
462
|
<div key={file.id}>
|
|
344
463
|
{file.path}: {file.statusMessage}
|
|
345
464
|
</div>
|
package/dist/index.cjs
CHANGED
|
@@ -37,9 +37,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
37
37
|
|
|
38
38
|
// node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js
|
|
39
39
|
var require_jszip_min = __commonJS({
|
|
40
|
-
"node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js"(exports, module) {
|
|
40
|
+
"node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js"(exports$1, module) {
|
|
41
41
|
!(function(e) {
|
|
42
|
-
if ("object" == typeof exports && "undefined" != typeof module) module.exports = e();
|
|
42
|
+
if ("object" == typeof exports$1 && "undefined" != typeof module) module.exports = e();
|
|
43
43
|
else if ("function" == typeof define && define.amd) define([], e);
|
|
44
44
|
else {
|
|
45
45
|
("undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : this).JSZip = e();
|
|
@@ -2349,7 +2349,7 @@ var require_jszip_min = __commonJS({
|
|
|
2349
2349
|
|
|
2350
2350
|
// node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/db.json
|
|
2351
2351
|
var require_db = __commonJS({
|
|
2352
|
-
"node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/db.json"(exports, module) {
|
|
2352
|
+
"node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/db.json"(exports$1, module) {
|
|
2353
2353
|
module.exports = {
|
|
2354
2354
|
"application/1d-interleaved-parityfec": {
|
|
2355
2355
|
source: "iana"
|
|
@@ -11697,7 +11697,7 @@ var require_db = __commonJS({
|
|
|
11697
11697
|
|
|
11698
11698
|
// node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/index.js
|
|
11699
11699
|
var require_mime_db = __commonJS({
|
|
11700
|
-
"node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/index.js"(exports, module) {
|
|
11700
|
+
"node_modules/.pnpm/mime-db@1.54.0/node_modules/mime-db/index.js"(exports$1, module) {
|
|
11701
11701
|
module.exports = require_db();
|
|
11702
11702
|
}
|
|
11703
11703
|
});
|
|
@@ -11835,35 +11835,39 @@ function stripCommonPrefix(files) {
|
|
|
11835
11835
|
}));
|
|
11836
11836
|
}
|
|
11837
11837
|
async function traverseFileTree(entry, files, currentPath = "") {
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
const
|
|
11853
|
-
|
|
11854
|
-
|
|
11838
|
+
try {
|
|
11839
|
+
if (entry.isFile) {
|
|
11840
|
+
const file = await new Promise((resolve, reject) => {
|
|
11841
|
+
entry.file(resolve, reject);
|
|
11842
|
+
});
|
|
11843
|
+
const relativePath = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
11844
|
+
Object.defineProperty(file, "webkitRelativePath", {
|
|
11845
|
+
value: relativePath,
|
|
11846
|
+
writable: false
|
|
11847
|
+
});
|
|
11848
|
+
files.push(file);
|
|
11849
|
+
} else if (entry.isDirectory) {
|
|
11850
|
+
const dirReader = entry.createReader();
|
|
11851
|
+
let allEntries = [];
|
|
11852
|
+
const readEntriesBatch = async () => {
|
|
11853
|
+
const batch = await new Promise(
|
|
11854
|
+
(resolve, reject) => {
|
|
11855
|
+
dirReader.readEntries(resolve, reject);
|
|
11856
|
+
}
|
|
11857
|
+
);
|
|
11858
|
+
if (batch.length > 0) {
|
|
11859
|
+
allEntries = allEntries.concat(batch);
|
|
11860
|
+
await readEntriesBatch();
|
|
11855
11861
|
}
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11862
|
+
};
|
|
11863
|
+
await readEntriesBatch();
|
|
11864
|
+
for (const childEntry of allEntries) {
|
|
11865
|
+
const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
|
|
11866
|
+
await traverseFileTree(childEntry, files, entryPath);
|
|
11860
11867
|
}
|
|
11861
|
-
};
|
|
11862
|
-
await readEntriesBatch();
|
|
11863
|
-
for (const childEntry of allEntries) {
|
|
11864
|
-
const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
|
|
11865
|
-
await traverseFileTree(childEntry, files, entryPath);
|
|
11866
11868
|
}
|
|
11869
|
+
} catch (error) {
|
|
11870
|
+
console.warn(`Error traversing file tree for entry ${entry.name}:`, error);
|
|
11867
11871
|
}
|
|
11868
11872
|
}
|
|
11869
11873
|
function useDrop(options) {
|
|
@@ -11873,28 +11877,32 @@ function useDrop(options) {
|
|
|
11873
11877
|
onFilesReady,
|
|
11874
11878
|
stripPrefix = true
|
|
11875
11879
|
} = options;
|
|
11876
|
-
const
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11880
|
+
const initialState = {
|
|
11881
|
+
value: "idle",
|
|
11882
|
+
files: [],
|
|
11883
|
+
sourceName: "",
|
|
11884
|
+
status: null
|
|
11885
|
+
};
|
|
11886
|
+
const [state, setState] = react.useState(initialState);
|
|
11882
11887
|
const isProcessingRef = react.useRef(false);
|
|
11883
11888
|
const inputRef = react.useRef(null);
|
|
11889
|
+
const isProcessing = react.useMemo(() => state.value === "processing", [state.value]);
|
|
11890
|
+
const isDragging = react.useMemo(() => state.value === "dragging", [state.value]);
|
|
11884
11891
|
const processFiles = react.useCallback(async (newFiles) => {
|
|
11885
11892
|
if (isProcessingRef.current) {
|
|
11886
11893
|
console.warn("File processing already in progress. Ignoring duplicate call.");
|
|
11887
11894
|
return;
|
|
11888
11895
|
}
|
|
11889
11896
|
if (!newFiles || newFiles.length === 0) {
|
|
11890
|
-
setStatusText("No files selected.");
|
|
11891
11897
|
return;
|
|
11892
11898
|
}
|
|
11893
11899
|
isProcessingRef.current = true;
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11900
|
+
setState({
|
|
11901
|
+
value: "processing",
|
|
11902
|
+
files: [],
|
|
11903
|
+
sourceName: "",
|
|
11904
|
+
status: { title: "Processing...", details: "Validating and preparing files." }
|
|
11905
|
+
});
|
|
11898
11906
|
try {
|
|
11899
11907
|
let detectedSourceName = "";
|
|
11900
11908
|
if (newFiles.length === 1 && isZipFile(newFiles[0])) {
|
|
@@ -11907,12 +11915,14 @@ function useDrop(options) {
|
|
|
11907
11915
|
detectedSourceName = newFiles[0].name;
|
|
11908
11916
|
}
|
|
11909
11917
|
}
|
|
11910
|
-
setSourceName(detectedSourceName);
|
|
11911
11918
|
const allFiles = [];
|
|
11912
11919
|
const shouldExtractZip = newFiles.length === 1 && isZipFile(newFiles[0]);
|
|
11913
11920
|
if (shouldExtractZip) {
|
|
11914
11921
|
const zipFile = newFiles[0];
|
|
11915
|
-
|
|
11922
|
+
setState((prev) => ({
|
|
11923
|
+
...prev,
|
|
11924
|
+
status: { title: "Extracting...", details: `Extracting ${zipFile.name}...` }
|
|
11925
|
+
}));
|
|
11916
11926
|
const { files: extractedFiles, errors } = await extractZipToFiles(zipFile);
|
|
11917
11927
|
if (errors.length > 0) {
|
|
11918
11928
|
console.warn("ZIP extraction errors:", errors);
|
|
@@ -11928,29 +11938,44 @@ function useDrop(options) {
|
|
|
11928
11938
|
const filePaths = allFiles.map(getFilePath);
|
|
11929
11939
|
const validPaths = new Set(ship.filterJunk(filePaths));
|
|
11930
11940
|
const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
|
|
11931
|
-
|
|
11941
|
+
setState((prev) => ({
|
|
11942
|
+
...prev,
|
|
11943
|
+
status: { title: "Processing...", details: "Processing files..." }
|
|
11944
|
+
}));
|
|
11932
11945
|
const processedFiles = await Promise.all(
|
|
11933
11946
|
cleanFiles.map((file) => createProcessedFile(file))
|
|
11934
11947
|
);
|
|
11935
11948
|
const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
|
|
11936
11949
|
const config = await ship$1.getConfig();
|
|
11937
11950
|
const validation = ship.validateFiles(finalFiles, config);
|
|
11938
|
-
setFiles(validation.files);
|
|
11939
|
-
setValidationError(validation.error);
|
|
11940
11951
|
if (validation.error) {
|
|
11941
|
-
|
|
11952
|
+
setState({
|
|
11953
|
+
value: "error",
|
|
11954
|
+
files: validation.files,
|
|
11955
|
+
sourceName: detectedSourceName,
|
|
11956
|
+
status: { title: validation.error.error, details: validation.error.details }
|
|
11957
|
+
});
|
|
11942
11958
|
onValidationError?.(validation.error);
|
|
11943
11959
|
} else if (validation.validFiles.length > 0) {
|
|
11944
|
-
|
|
11960
|
+
setState({
|
|
11961
|
+
value: "ready",
|
|
11962
|
+
files: validation.files,
|
|
11963
|
+
sourceName: detectedSourceName,
|
|
11964
|
+
status: { title: "Ready", details: `${validation.validFiles.length} file(s) are ready.` }
|
|
11965
|
+
});
|
|
11945
11966
|
onFilesReady?.(validation.validFiles);
|
|
11946
11967
|
} else {
|
|
11947
11968
|
const noValidError = {
|
|
11948
11969
|
error: "No Valid Files",
|
|
11949
|
-
details: "
|
|
11970
|
+
details: "None of the provided files could be processed.",
|
|
11950
11971
|
isClientError: true
|
|
11951
11972
|
};
|
|
11952
|
-
|
|
11953
|
-
|
|
11973
|
+
setState({
|
|
11974
|
+
value: "error",
|
|
11975
|
+
files: validation.files,
|
|
11976
|
+
sourceName: detectedSourceName,
|
|
11977
|
+
status: { title: noValidError.error, details: noValidError.details }
|
|
11978
|
+
});
|
|
11954
11979
|
onValidationError?.(noValidError);
|
|
11955
11980
|
}
|
|
11956
11981
|
} catch (error) {
|
|
@@ -11959,69 +11984,92 @@ function useDrop(options) {
|
|
|
11959
11984
|
details: `Failed to process files: ${error instanceof Error ? error.message : String(error)}`,
|
|
11960
11985
|
isClientError: true
|
|
11961
11986
|
};
|
|
11962
|
-
|
|
11963
|
-
|
|
11987
|
+
setState((prev) => ({
|
|
11988
|
+
...prev,
|
|
11989
|
+
value: "error",
|
|
11990
|
+
status: { title: processingError.error, details: processingError.details }
|
|
11991
|
+
}));
|
|
11964
11992
|
onValidationError?.(processingError);
|
|
11965
11993
|
} finally {
|
|
11966
11994
|
isProcessingRef.current = false;
|
|
11967
|
-
setIsProcessing(false);
|
|
11968
11995
|
}
|
|
11969
11996
|
}, [ship$1, onValidationError, onFilesReady, stripPrefix]);
|
|
11970
11997
|
const clearAll = react.useCallback(() => {
|
|
11971
|
-
|
|
11972
|
-
setSourceName("");
|
|
11973
|
-
setStatusText("");
|
|
11974
|
-
setValidationError(null);
|
|
11975
|
-
setIsDragging(false);
|
|
11998
|
+
setState(initialState);
|
|
11976
11999
|
isProcessingRef.current = false;
|
|
11977
|
-
setIsProcessing(false);
|
|
11978
12000
|
}, []);
|
|
11979
12001
|
const getValidFilesCallback = react.useCallback(() => {
|
|
11980
|
-
return getValidFiles(files);
|
|
11981
|
-
}, [files]);
|
|
11982
|
-
const updateFileStatus = react.useCallback((fileId,
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
12002
|
+
return getValidFiles(state.files);
|
|
12003
|
+
}, [state.files]);
|
|
12004
|
+
const updateFileStatus = react.useCallback((fileId, fileState) => {
|
|
12005
|
+
setState((prev) => ({
|
|
12006
|
+
...prev,
|
|
12007
|
+
files: prev.files.map(
|
|
12008
|
+
(file) => file.id === fileId ? { ...file, ...fileState } : file
|
|
12009
|
+
)
|
|
12010
|
+
}));
|
|
11986
12011
|
}, []);
|
|
11987
12012
|
const handleDragOver = react.useCallback((e) => {
|
|
11988
12013
|
e.preventDefault();
|
|
11989
|
-
|
|
12014
|
+
setState((prev) => {
|
|
12015
|
+
if (prev.value === "idle" || prev.value === "ready" || prev.value === "error") {
|
|
12016
|
+
return { ...prev, value: "dragging" };
|
|
12017
|
+
}
|
|
12018
|
+
return prev;
|
|
12019
|
+
});
|
|
11990
12020
|
}, []);
|
|
11991
12021
|
const handleDragLeave = react.useCallback((e) => {
|
|
11992
12022
|
e.preventDefault();
|
|
11993
|
-
|
|
12023
|
+
setState((prev) => {
|
|
12024
|
+
if (prev.value !== "dragging") return prev;
|
|
12025
|
+
const nextValue = prev.files.length > 0 ? prev.status?.title === "Ready" ? "ready" : "error" : "idle";
|
|
12026
|
+
return { ...prev, value: nextValue };
|
|
12027
|
+
});
|
|
11994
12028
|
}, []);
|
|
11995
12029
|
const handleDrop = react.useCallback(async (e) => {
|
|
11996
12030
|
e.preventDefault();
|
|
11997
|
-
setIsDragging(false);
|
|
11998
12031
|
const items = Array.from(e.dataTransfer.items);
|
|
11999
|
-
const
|
|
12032
|
+
const files = [];
|
|
12000
12033
|
let hasEntries = false;
|
|
12001
12034
|
for (const item of items) {
|
|
12002
12035
|
if (item.kind === "file") {
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12036
|
+
try {
|
|
12037
|
+
const entry = item.webkitGetAsEntry?.();
|
|
12038
|
+
if (entry) {
|
|
12039
|
+
hasEntries = true;
|
|
12040
|
+
await traverseFileTree(
|
|
12041
|
+
entry,
|
|
12042
|
+
files,
|
|
12043
|
+
entry.isDirectory ? entry.name : ""
|
|
12044
|
+
);
|
|
12045
|
+
} else {
|
|
12046
|
+
const file = item.getAsFile();
|
|
12047
|
+
if (file) {
|
|
12048
|
+
files.push(file);
|
|
12049
|
+
}
|
|
12050
|
+
}
|
|
12051
|
+
} catch (error) {
|
|
12052
|
+
console.warn("Error processing drop item:", error);
|
|
12053
|
+
const file = item.getAsFile();
|
|
12054
|
+
if (file) {
|
|
12055
|
+
files.push(file);
|
|
12056
|
+
}
|
|
12011
12057
|
}
|
|
12012
12058
|
}
|
|
12013
12059
|
}
|
|
12014
12060
|
if (!hasEntries && e.dataTransfer.files.length > 0) {
|
|
12015
|
-
|
|
12061
|
+
files.push(...Array.from(e.dataTransfer.files));
|
|
12016
12062
|
}
|
|
12017
|
-
if (
|
|
12018
|
-
await processFiles(
|
|
12063
|
+
if (files.length > 0) {
|
|
12064
|
+
await processFiles(files);
|
|
12065
|
+
} else if (state.value === "dragging") {
|
|
12066
|
+
setState((prev) => ({ ...prev, value: "idle" }));
|
|
12019
12067
|
}
|
|
12020
|
-
}, [processFiles]);
|
|
12068
|
+
}, [processFiles, state.value]);
|
|
12021
12069
|
const handleInputChange = react.useCallback((e) => {
|
|
12022
|
-
const
|
|
12023
|
-
if (
|
|
12024
|
-
processFiles(
|
|
12070
|
+
const files = Array.from(e.target.files || []);
|
|
12071
|
+
if (files.length > 0) {
|
|
12072
|
+
processFiles(files);
|
|
12025
12073
|
}
|
|
12026
12074
|
}, [processFiles]);
|
|
12027
12075
|
const open = react.useCallback(() => {
|
|
@@ -12042,13 +12090,11 @@ function useDrop(options) {
|
|
|
12042
12090
|
onChange: handleInputChange
|
|
12043
12091
|
}), [handleInputChange]);
|
|
12044
12092
|
return {
|
|
12045
|
-
// State
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
statusText,
|
|
12093
|
+
// State machine
|
|
12094
|
+
state,
|
|
12095
|
+
// Convenience getters (computed from state)
|
|
12049
12096
|
isProcessing,
|
|
12050
12097
|
isDragging,
|
|
12051
|
-
validationError,
|
|
12052
12098
|
// Primary API: Prop getters
|
|
12053
12099
|
getDropzoneProps,
|
|
12054
12100
|
getInputProps,
|