@shipstatic/drop 0.1.5 → 0.1.7
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 +227 -180
- package/dist/index.cjs +167 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -13
- package/dist/index.d.ts +59 -13
- package/dist/index.js +168 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,27 +4,36 @@
|
|
|
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
|
+
**v0.1.6 Update:** Now includes 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
|
+
|
|
7
9
|
**Note:** MD5 calculation is handled by Ship SDK during deployment. Drop focuses on file processing and UI state management.
|
|
8
10
|
|
|
9
11
|
## Why Headless?
|
|
10
12
|
|
|
11
|
-
This package provides **zero UI components
|
|
13
|
+
This package provides **zero UI components** - just a React hook with built-in drag & drop functionality. You bring your own styling.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
**What you get:**
|
|
16
|
+
1. **Built-in drag & drop** - Proper folder support with `webkitGetAsEntry` API, all handled internally
|
|
17
|
+
2. **Prop getters API** - Similar to `react-dropzone`, just spread props on your elements
|
|
18
|
+
3. **Full styling control** - No imposed CSS, design system, or theming
|
|
19
|
+
4. **Smaller bundle** - No UI components means less bloat
|
|
20
|
+
5. **Ship SDK integration** - Purpose-built for Ship deployments, not a generic file upload library
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
**What's different from other libraries:**
|
|
23
|
+
- Generic dropzone libraries don't preserve folder structure properly
|
|
24
|
+
- We handle the complex parts (ZIP extraction, folder traversal, path normalization)
|
|
25
|
+
- You handle the simple parts (styling, layout, animations)
|
|
19
26
|
|
|
20
27
|
## Features
|
|
21
28
|
|
|
22
|
-
- 🎯 **
|
|
29
|
+
- 🎯 **Prop Getters API** - Just spread props on your elements (like `react-dropzone`)
|
|
30
|
+
- 🖱️ **Built-in Drag & Drop** - Automatic folder support with `webkitGetAsEntry` API
|
|
23
31
|
- 📦 **ZIP Support** - Automatic ZIP file extraction and processing
|
|
24
32
|
- ✅ **Validation** - Client-side file size, count, and total size validation (powered by Ship SDK)
|
|
25
33
|
- 🗑️ **Junk Filtering** - Automatically filters `.DS_Store`, `Thumbs.db`, etc. (powered by Ship SDK)
|
|
26
34
|
- 🔒 **Path Sanitization** - Defense-in-depth protection against directory traversal attacks
|
|
27
|
-
- 📁 **Folder Structure Preservation** -
|
|
35
|
+
- 📁 **Folder Structure Preservation** - Proper folder paths via `webkitRelativePath`
|
|
36
|
+
- 🎨 **Headless UI** - No visual components, just logic and state management
|
|
28
37
|
- 🚀 **Focused Scope** - File processing and UI state only. MD5 calculation and deployment handled by Ship SDK
|
|
29
38
|
|
|
30
39
|
## Installation
|
|
@@ -50,33 +59,45 @@ function MyUploader() {
|
|
|
50
59
|
|
|
51
60
|
const handleUpload = async () => {
|
|
52
61
|
const validFiles = drop.getValidFiles();
|
|
53
|
-
|
|
54
62
|
// ProcessedFile extends StaticFile - no conversion needed!
|
|
55
63
|
await ship.deployments.create(validFiles.map(f => f.file));
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
return (
|
|
59
67
|
<div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
drop.
|
|
68
|
+
{/* Drag & drop zone with built-in folder support */}
|
|
69
|
+
<div
|
|
70
|
+
{...drop.getDropzoneProps()}
|
|
71
|
+
style={{
|
|
72
|
+
border: '2px dashed',
|
|
73
|
+
borderColor: drop.isDragging ? 'blue' : 'gray',
|
|
74
|
+
padding: '40px',
|
|
75
|
+
textAlign: 'center',
|
|
76
|
+
cursor: 'pointer',
|
|
66
77
|
}}
|
|
67
|
-
|
|
78
|
+
>
|
|
79
|
+
<input {...drop.getInputProps()} />
|
|
80
|
+
{drop.isDragging ? '📂 Drop here' : '📁 Click or drag files/folders'}
|
|
81
|
+
</div>
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
{/* Status - using state machine */}
|
|
84
|
+
{drop.state.status && (
|
|
85
|
+
<p>
|
|
86
|
+
<strong>{drop.state.status.title}:</strong> {drop.state.status.details}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
70
89
|
|
|
71
|
-
{
|
|
90
|
+
{/* File list */}
|
|
91
|
+
{drop.state.files.map(file => (
|
|
72
92
|
<div key={file.id}>
|
|
73
93
|
{file.name} - {file.status}
|
|
74
94
|
</div>
|
|
75
95
|
))}
|
|
76
96
|
|
|
97
|
+
{/* Upload button */}
|
|
77
98
|
<button
|
|
78
99
|
onClick={handleUpload}
|
|
79
|
-
disabled={drop.
|
|
100
|
+
disabled={drop.state.value !== 'ready'}
|
|
80
101
|
>
|
|
81
102
|
Upload {drop.getValidFiles().length} files
|
|
82
103
|
</button>
|
|
@@ -110,151 +131,56 @@ const drop = useDrop({ ship });
|
|
|
110
131
|
- Ship SDK is the single source of truth for validation
|
|
111
132
|
- Drop only provides what Ship doesn't have (ZIP, React state, folder structure)
|
|
112
133
|
|
|
113
|
-
##
|
|
134
|
+
## Advanced: Programmatic File Picker
|
|
114
135
|
|
|
115
|
-
|
|
136
|
+
You can programmatically trigger the file picker using the `open()` method:
|
|
116
137
|
|
|
117
138
|
```tsx
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
function MyDeployUI() {
|
|
122
|
-
const drop = useDrop();
|
|
123
|
-
const [isDragActive, setIsDragActive] = useState(false);
|
|
124
|
-
|
|
125
|
-
const handleDrop = async (e: React.DragEvent) => {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
setIsDragActive(false);
|
|
128
|
-
|
|
129
|
-
// Extract files with folder structure preserved
|
|
130
|
-
const files = await extractFilesWithStructure(e.dataTransfer);
|
|
131
|
-
drop.processFiles(files);
|
|
132
|
-
};
|
|
139
|
+
function MyUploader() {
|
|
140
|
+
const drop = useDrop({ ship });
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
{/* Custom trigger button */}
|
|
145
|
+
<button onClick={drop.open}>
|
|
146
|
+
Select Files
|
|
147
|
+
</button>
|
|
139
148
|
|
|
140
|
-
|
|
149
|
+
{/* Hidden input managed by the hook */}
|
|
150
|
+
<input {...drop.getInputProps()} />
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
{/* Or use the dropzone */}
|
|
153
|
+
<div {...drop.getDropzoneProps()}>
|
|
154
|
+
Drop files here
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
};
|
|
161
|
+
## Advanced: Manual File Processing
|
|
151
162
|
|
|
152
|
-
|
|
153
|
-
item: DataTransferItem,
|
|
154
|
-
files: File[]
|
|
155
|
-
): Promise<void> => {
|
|
156
|
-
// Try modern File System Access API first (Chrome 86+)
|
|
157
|
-
if (
|
|
158
|
-
globalThis.isSecureContext &&
|
|
159
|
-
typeof (item as any).getAsFileSystemHandle === 'function'
|
|
160
|
-
) {
|
|
161
|
-
try {
|
|
162
|
-
const handle = await (item as any).getAsFileSystemHandle();
|
|
163
|
-
if (handle) {
|
|
164
|
-
await processFileSystemHandle(handle, files, '');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
// Fall through to webkit API
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Fallback to webkitGetAsEntry (broader browser support)
|
|
173
|
-
const entry = (item as any).webkitGetAsEntry?.();
|
|
174
|
-
if (entry) {
|
|
175
|
-
await processEntry(entry, files, '');
|
|
176
|
-
}
|
|
177
|
-
};
|
|
163
|
+
For advanced use cases, you can manually process files instead of using prop getters:
|
|
178
164
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
basePath: string
|
|
183
|
-
): Promise<void> => {
|
|
184
|
-
if (handle.kind === 'file') {
|
|
185
|
-
const file = await handle.getFile();
|
|
186
|
-
// Set webkitRelativePath for Ship SDK compatibility
|
|
187
|
-
Object.defineProperty(file, 'webkitRelativePath', {
|
|
188
|
-
value: basePath + file.name,
|
|
189
|
-
writable: false,
|
|
190
|
-
enumerable: true,
|
|
191
|
-
configurable: true,
|
|
192
|
-
});
|
|
193
|
-
files.push(file);
|
|
194
|
-
} else if (handle.kind === 'directory') {
|
|
195
|
-
const dirPath = basePath + handle.name + '/';
|
|
196
|
-
for await (const entry of handle.values()) {
|
|
197
|
-
await processFileSystemHandle(entry, files, dirPath);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
165
|
+
```tsx
|
|
166
|
+
function AdvancedUploader() {
|
|
167
|
+
const drop = useDrop({ ship });
|
|
201
168
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
files
|
|
205
|
-
|
|
206
|
-
): Promise<void> => {
|
|
207
|
-
if (entry.isFile) {
|
|
208
|
-
const file = await new Promise<File>((resolve, reject) => {
|
|
209
|
-
entry.file(resolve, reject);
|
|
210
|
-
});
|
|
211
|
-
// Set webkitRelativePath for Ship SDK compatibility
|
|
212
|
-
Object.defineProperty(file, 'webkitRelativePath', {
|
|
213
|
-
value: basePath + entry.name,
|
|
214
|
-
writable: false,
|
|
215
|
-
enumerable: true,
|
|
216
|
-
configurable: true,
|
|
217
|
-
});
|
|
218
|
-
files.push(file);
|
|
219
|
-
} else if (entry.isDirectory) {
|
|
220
|
-
const dirReader = entry.createReader();
|
|
221
|
-
const entries = await new Promise<any[]>((resolve, reject) => {
|
|
222
|
-
dirReader.readEntries(resolve, reject);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
for (const childEntry of entries) {
|
|
226
|
-
await processEntry(childEntry, files, basePath + entry.name + '/');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
169
|
+
const handleManualDrop = async (e: React.DragEvent) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
const files = Array.from(e.dataTransfer.files);
|
|
172
|
+
await drop.processFiles(files);
|
|
229
173
|
};
|
|
230
174
|
|
|
231
175
|
return (
|
|
232
|
-
<div
|
|
233
|
-
|
|
234
|
-
onDragLeave={() => setIsDragActive(false)}
|
|
235
|
-
onDrop={handleDrop}
|
|
236
|
-
className={isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
|
|
237
|
-
>
|
|
238
|
-
{drop.isProcessing ? (
|
|
239
|
-
<p>Processing {drop.files.length} files...</p>
|
|
240
|
-
) : (
|
|
241
|
-
<p>Drag & drop files or folders here</p>
|
|
242
|
-
)}
|
|
243
|
-
|
|
244
|
-
{drop.validationError && (
|
|
245
|
-
<div className="text-red-600">{drop.validationError.details}</div>
|
|
246
|
-
)}
|
|
176
|
+
<div onDrop={handleManualDrop}>
|
|
177
|
+
{/* Custom implementation */}
|
|
247
178
|
</div>
|
|
248
179
|
);
|
|
249
180
|
}
|
|
250
181
|
```
|
|
251
182
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
- ✅ **Preserves folder structure** via `webkitRelativePath`
|
|
255
|
-
- ✅ **Uses modern File System Access API** (no permission prompts in Chrome 86+)
|
|
256
|
-
- ✅ **Fallback to webkit APIs** for broader browser support (Safari, Firefox)
|
|
257
|
-
- ✅ **You control every aspect** of the UI and UX
|
|
183
|
+
**Note:** When using manual processing, you lose automatic folder structure preservation. The built-in `getDropzoneProps()` handles `webkitGetAsEntry` API internally to preserve folder paths.
|
|
258
184
|
|
|
259
185
|
## API
|
|
260
186
|
|
|
@@ -281,19 +207,43 @@ interface DropOptions {
|
|
|
281
207
|
|
|
282
208
|
```typescript
|
|
283
209
|
interface DropReturn {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
210
|
+
// State machine
|
|
211
|
+
/** Current state of the drop hook */
|
|
212
|
+
state: DropState;
|
|
213
|
+
|
|
214
|
+
// Convenience getters (computed from state)
|
|
288
215
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
289
216
|
isProcessing: boolean;
|
|
290
|
-
/**
|
|
291
|
-
|
|
217
|
+
/** Whether user is currently dragging over the dropzone */
|
|
218
|
+
isDragging: boolean;
|
|
219
|
+
|
|
220
|
+
// Primary API: Prop getters for easy integration
|
|
221
|
+
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
222
|
+
getDropzoneProps: () => {
|
|
223
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
224
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
225
|
+
onDrop: (e: React.DragEvent) => void;
|
|
226
|
+
onClick: () => void;
|
|
227
|
+
};
|
|
228
|
+
/** Get props to spread on hidden file input element */
|
|
229
|
+
getInputProps: () => {
|
|
230
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
231
|
+
type: 'file';
|
|
232
|
+
style: { display: string };
|
|
233
|
+
multiple: boolean;
|
|
234
|
+
webkitdirectory: string;
|
|
235
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
236
|
+
};
|
|
292
237
|
|
|
293
|
-
|
|
238
|
+
// Actions
|
|
239
|
+
/** Programmatically trigger file picker */
|
|
240
|
+
open: () => void;
|
|
241
|
+
/** Manually process files (for advanced usage) */
|
|
294
242
|
processFiles: (files: File[]) => Promise<void>;
|
|
295
243
|
/** Clear all files and reset state */
|
|
296
244
|
clearAll: () => void;
|
|
245
|
+
|
|
246
|
+
// Helpers
|
|
297
247
|
/** Get only valid files ready for upload */
|
|
298
248
|
getValidFiles: () => ProcessedFile[];
|
|
299
249
|
/** Update upload state for a specific file (status, progress, message) */
|
|
@@ -303,19 +253,111 @@ interface DropReturn {
|
|
|
303
253
|
progress?: number;
|
|
304
254
|
}) => void;
|
|
305
255
|
}
|
|
256
|
+
|
|
257
|
+
// State machine types
|
|
258
|
+
type DropStateValue =
|
|
259
|
+
| 'idle' // The hook is ready for files
|
|
260
|
+
| 'dragging' // The user is dragging files over the dropzone
|
|
261
|
+
| 'processing' // Files are being validated and processed
|
|
262
|
+
| 'ready' // Files are valid and ready for deployment
|
|
263
|
+
| 'error'; // An error occurred during processing
|
|
264
|
+
|
|
265
|
+
interface DropState {
|
|
266
|
+
value: DropStateValue;
|
|
267
|
+
files: ProcessedFile[];
|
|
268
|
+
sourceName: string;
|
|
269
|
+
status: DropStatus | null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface DropStatus {
|
|
273
|
+
title: string;
|
|
274
|
+
details: string;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## State Machine
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
### State Flow
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
idle → dragging → idle (drag leave without drop)
|
|
286
|
+
idle → dragging → processing → ready (successful)
|
|
287
|
+
idle → dragging → processing → error (failed)
|
|
288
|
+
ready → dragging → processing → ... (new drop)
|
|
289
|
+
error → dragging → processing → ... (retry)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Using the State Machine
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
function StatusIndicator({ drop }) {
|
|
296
|
+
const { state } = drop;
|
|
297
|
+
|
|
298
|
+
switch (state.value) {
|
|
299
|
+
case 'idle':
|
|
300
|
+
return <p>Drop files here or click to select</p>;
|
|
301
|
+
|
|
302
|
+
case 'dragging':
|
|
303
|
+
return <p>Drop your files now!</p>;
|
|
304
|
+
|
|
305
|
+
case 'processing':
|
|
306
|
+
return <p>{state.status?.details || 'Processing...'}</p>;
|
|
307
|
+
|
|
308
|
+
case 'ready':
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
<p>✓ {state.files.length} files ready</p>
|
|
312
|
+
<button>Upload to Ship</button>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
case 'error':
|
|
317
|
+
return (
|
|
318
|
+
<div>
|
|
319
|
+
<p>✗ {state.status?.title}</p>
|
|
320
|
+
<p>{state.status?.details}</p>
|
|
321
|
+
<button onClick={drop.clearAll}>Try Again</button>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
306
326
|
```
|
|
307
327
|
|
|
328
|
+
### Convenience Getters
|
|
329
|
+
|
|
330
|
+
For simpler use cases, boolean convenience getters are provided:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// These are computed from state.value (read-only projections)
|
|
334
|
+
drop.isProcessing // true when state.value === 'processing'
|
|
335
|
+
drop.isDragging // true when state.value === 'dragging'
|
|
336
|
+
|
|
337
|
+
// For error information, use the state object
|
|
338
|
+
drop.state.value === 'error' // Check if in error state
|
|
339
|
+
drop.state.status?.title // Error title
|
|
340
|
+
drop.state.status?.details // Error details
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Benefits
|
|
344
|
+
|
|
345
|
+
- **No impossible states** - Can't be `isProcessing=true` AND `isDragging=true`
|
|
346
|
+
- **Clear transitions** - State flow is explicit and predictable
|
|
347
|
+
- **Better TypeScript** - Discriminated unions provide type safety
|
|
348
|
+
- **Easier debugging** - Single source of truth for what's happening
|
|
349
|
+
|
|
308
350
|
## Error Handling
|
|
309
351
|
|
|
310
352
|
### Per-File Error Display
|
|
311
353
|
|
|
312
|
-
Each file in the `files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
354
|
+
Each file in the `state.files` array contains its own `status` and `statusMessage`, allowing you to display granular errors for individual files:
|
|
313
355
|
|
|
314
356
|
```tsx
|
|
315
357
|
function FileList({ drop }) {
|
|
316
358
|
return (
|
|
317
359
|
<div>
|
|
318
|
-
{drop.files.map(file => (
|
|
360
|
+
{drop.state.files.map(file => (
|
|
319
361
|
<div key={file.id}>
|
|
320
362
|
<span>{file.path}</span>
|
|
321
363
|
|
|
@@ -332,7 +374,7 @@ function FileList({ drop }) {
|
|
|
332
374
|
))}
|
|
333
375
|
|
|
334
376
|
{/* If validation fails, allow user to clear all and try again */}
|
|
335
|
-
{drop.
|
|
377
|
+
{drop.state.value === 'error' && (
|
|
336
378
|
<button onClick={drop.clearAll}>
|
|
337
379
|
Clear All & Try Again
|
|
338
380
|
</button>
|
|
@@ -348,15 +390,15 @@ function FileList({ drop }) {
|
|
|
348
390
|
- `empty_file` - File is 0 bytes
|
|
349
391
|
- `ready` - File passed all validation and is ready for upload
|
|
350
392
|
|
|
351
|
-
###
|
|
393
|
+
### Error State Summary
|
|
352
394
|
|
|
353
|
-
|
|
395
|
+
When files fail validation or processing, check the error state:
|
|
354
396
|
|
|
355
397
|
```tsx
|
|
356
|
-
{drop.
|
|
398
|
+
{drop.state.value === 'error' && drop.state.status && (
|
|
357
399
|
<div>
|
|
358
|
-
<p>{drop.
|
|
359
|
-
<p>{drop.
|
|
400
|
+
<p>{drop.state.status.title}</p>
|
|
401
|
+
<p>{drop.state.status.details}</p>
|
|
360
402
|
</div>
|
|
361
403
|
)}
|
|
362
404
|
```
|
|
@@ -386,10 +428,10 @@ Use `clearAll()` to reset and try again:
|
|
|
386
428
|
|
|
387
429
|
```tsx
|
|
388
430
|
// If validation fails, show user which files failed
|
|
389
|
-
{drop.
|
|
431
|
+
{drop.state.value === 'error' && (
|
|
390
432
|
<div>
|
|
391
433
|
<p>Validation failed. Please fix the issues and try again:</p>
|
|
392
|
-
{drop.files.map(file => (
|
|
434
|
+
{drop.state.files.map(file => (
|
|
393
435
|
<div key={file.id}>
|
|
394
436
|
{file.path}: {file.statusMessage}
|
|
395
437
|
</div>
|
|
@@ -514,23 +556,28 @@ This package was extracted from the `web/drop` application and is purpose-built
|
|
|
514
556
|
**1. Focused on UI Concerns**
|
|
515
557
|
- ZIP extraction for user convenience
|
|
516
558
|
- File list state management for React UIs
|
|
517
|
-
-
|
|
559
|
+
- Drag & drop event handling with folder support
|
|
560
|
+
- Folder structure preservation via `webkitGetAsEntry` API
|
|
518
561
|
- Path normalization for clean URLs
|
|
519
562
|
- These are UI/UX concerns, not deployment logic
|
|
520
563
|
|
|
521
|
-
**2.
|
|
564
|
+
**2. Prop Getters Pattern (Like react-dropzone)**
|
|
565
|
+
We provide event handlers, not visual components:
|
|
566
|
+
- ✅ **`getDropzoneProps()`** - Returns drag & drop event handlers
|
|
567
|
+
- ✅ **`getInputProps()`** - Returns file input configuration
|
|
568
|
+
- ✅ **`isDragging`** - State for visual feedback
|
|
569
|
+
- ✅ **You control the DOM** - Your markup, your styling, your design system
|
|
570
|
+
|
|
571
|
+
This is the same pattern used by popular libraries like `react-dropzone`, `downshift`, and `react-table`.
|
|
572
|
+
|
|
573
|
+
**3. Loosely Coupled Integration**
|
|
522
574
|
Following industry standards (Firebase hooks, Supabase utilities), we chose:
|
|
523
|
-
- ✅ **
|
|
524
|
-
- ✅ **Simple**:
|
|
525
|
-
- ✅ **Testable**:
|
|
575
|
+
- ✅ **Ship instance as dependency**: Validates using `ship.getConfig()`
|
|
576
|
+
- ✅ **Simple output**: ProcessedFile[] can be passed directly to Ship SDK
|
|
577
|
+
- ✅ **Testable**: Easy to mock Ship SDK for testing
|
|
526
578
|
- ✅ **Flexible**: Host app controls WHEN to deploy
|
|
527
579
|
|
|
528
|
-
|
|
529
|
-
- ❌ Passing Ship SDK instance to useDrop
|
|
530
|
-
- ❌ React Context provider pattern
|
|
531
|
-
- ❌ Global configuration singleton
|
|
532
|
-
|
|
533
|
-
**3. Type System Integration**
|
|
580
|
+
**4. Type System Integration**
|
|
534
581
|
|
|
535
582
|
ProcessedFile extends StaticFile from `@shipstatic/types` - the single source of truth for Ship SDK types:
|
|
536
583
|
|
|
@@ -540,13 +587,13 @@ File[] → ProcessedFile[] (which IS StaticFile[]) → ship.deployments.create()
|
|
|
540
587
|
|
|
541
588
|
No conversion needed. ProcessedFile adds UI-specific properties (id, name, status, progress) to StaticFile's base properties (content, path, size, md5).
|
|
542
589
|
|
|
543
|
-
**
|
|
590
|
+
**5. No Visual Components**
|
|
544
591
|
|
|
545
|
-
We deliberately don't provide
|
|
546
|
-
-
|
|
547
|
-
-
|
|
548
|
-
-
|
|
549
|
-
-
|
|
592
|
+
We deliberately don't provide styled components because:
|
|
593
|
+
- Your design system is unique to your application
|
|
594
|
+
- Styling should match your brand, not our opinions
|
|
595
|
+
- Prop getters give you full control over DOM structure and CSS
|
|
596
|
+
- Similar to how `react-dropzone` works - logic without opinions
|
|
550
597
|
|
|
551
598
|
### Error Handling Philosophy
|
|
552
599
|
|