jattac.libs.web.zest-file-upload 0.2.0 → 0.2.1
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 +545 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# ZestFileUpload
|
|
2
|
+
|
|
3
|
+
A production-ready React file upload component with drag-and-drop, camera capture, image crop, compression, progress tracking, **multi-file parallel uploads**, and a mobile-optimised UI. CSS is auto-injected — no separate stylesheet import required.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install jattac.libs.web.zest-file-upload
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install react react-dom react-icons
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
|
|
23
|
+
|
|
24
|
+
function App() {
|
|
25
|
+
return (
|
|
26
|
+
<ZestFileUpload
|
|
27
|
+
label="Upload a file"
|
|
28
|
+
onFileSelect={async (file) => {
|
|
29
|
+
if (!file) return;
|
|
30
|
+
const formData = new FormData();
|
|
31
|
+
formData.append("file", file);
|
|
32
|
+
await fetch("/api/upload", { method: "POST", body: formData });
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Drag and drop** — full drop-zone with visual feedback
|
|
44
|
+
- **File picker** — click to browse
|
|
45
|
+
- **Camera capture** — take a photo directly (mobile and desktop)
|
|
46
|
+
- **Image crop** — built-in crop UI after camera capture
|
|
47
|
+
- **Image compression** — auto-compress camera photos before upload
|
|
48
|
+
- **Progress bar** — caller-controlled upload progress
|
|
49
|
+
- **Multi-file mode** — parallel real-time uploads with per-file status
|
|
50
|
+
- **Validation** — file type and size enforcement
|
|
51
|
+
- **Error handling** — inline validation errors and upload errors with retry
|
|
52
|
+
- **Accessibility** — keyboard navigation, ARIA roles, live regions
|
|
53
|
+
- **i18n ready** — every label is overridable
|
|
54
|
+
- **Custom icons** — swap any icon with your own
|
|
55
|
+
- **SSR safe** — works with Next.js and other SSR frameworks
|
|
56
|
+
- **Zero CSS import** — styles are injected automatically
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
### Core props
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default | Description |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `label` | `string` | — | **Required.** Label shown above the upload zone |
|
|
67
|
+
| `onFileSelect` | `(file: File \| null) => Promise<void>` | — | **Required.** Called when a file is selected (single mode) or per-file fallback in multi mode |
|
|
68
|
+
| `disabled` | `boolean` | `false` | Disables the component |
|
|
69
|
+
| `accept` | `string` | — | Accepted file types (e.g. `"image/*"`, `".pdf,.docx"`, `"image/png,image/jpeg"`) |
|
|
70
|
+
| `maxFileSize` | `number` | — | Max file size in bytes |
|
|
71
|
+
|
|
72
|
+
### Upload feedback
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `progressPercentage` | `number` | `0` | Upload progress 0–100 (single mode) |
|
|
77
|
+
| `hideCompletionMessage` | `boolean` | `false` | Hide the "Upload complete" message |
|
|
78
|
+
| `successTimeout` | `number` | `5000` | Ms before success state resets to idle |
|
|
79
|
+
| `onError` | `(error: string) => void` | — | Called on validation errors |
|
|
80
|
+
|
|
81
|
+
### Camera
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default | Description |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `capture` | `"user" \| "environment"` | — | Preferred camera facing mode |
|
|
86
|
+
|
|
87
|
+
### Multi-file mode
|
|
88
|
+
|
|
89
|
+
| Prop | Type | Default | Description |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `multiple` | `boolean` | `false` | Enable multi-file mode |
|
|
92
|
+
| `onFilesSelect` | `(files: File[]) => Promise<void>` | — | Batch callback — called once with all valid files. If omitted, `onFileSelect` is called concurrently per file |
|
|
93
|
+
| `filesProgress` | `Record<FileEntryId, number>` | — | Per-file progress 0–100, keyed by `FileEntryId` |
|
|
94
|
+
| `maxFiles` | `number` | — | Maximum number of files in the queue |
|
|
95
|
+
| `multiLabels` | `Partial<MultiFileUploadLabels>` | — | Override multi-mode UI labels |
|
|
96
|
+
|
|
97
|
+
### Customisation
|
|
98
|
+
|
|
99
|
+
| Prop | Type | Default | Description |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `labels` | `Partial<FileUploadLabels>` | — | Override any UI label string |
|
|
102
|
+
| `icons` | `Partial<FileUploadIcons>` | — | Override any icon with a custom `ReactNode` |
|
|
103
|
+
| `errorBoundary` | `boolean` | `false` | Wrap with a React error boundary |
|
|
104
|
+
|
|
105
|
+
### Ref methods
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const ref = useRef<FileUploadRef>(null);
|
|
109
|
+
|
|
110
|
+
ref.current.clearFile(); // Reset single-mode state + call onFileSelect(null)
|
|
111
|
+
ref.current.clearAll(); // Clear the multi-file queue (no-op in single mode)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### Single file upload with progress
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { useState } from "react";
|
|
122
|
+
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
|
|
123
|
+
|
|
124
|
+
function UploadWithProgress() {
|
|
125
|
+
const [progress, setProgress] = useState(0);
|
|
126
|
+
|
|
127
|
+
const handleUpload = async (file: File | null) => {
|
|
128
|
+
if (!file) return;
|
|
129
|
+
|
|
130
|
+
const formData = new FormData();
|
|
131
|
+
formData.append("file", file);
|
|
132
|
+
|
|
133
|
+
await new Promise<void>((resolve, reject) => {
|
|
134
|
+
const xhr = new XMLHttpRequest();
|
|
135
|
+
xhr.upload.onprogress = (e) => {
|
|
136
|
+
if (e.lengthComputable) {
|
|
137
|
+
setProgress(Math.round((e.loaded / e.total) * 100));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject());
|
|
141
|
+
xhr.onerror = reject;
|
|
142
|
+
xhr.open("POST", "/api/upload");
|
|
143
|
+
xhr.send(formData);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
setProgress(0);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<ZestFileUpload
|
|
151
|
+
label="Upload document"
|
|
152
|
+
onFileSelect={handleUpload}
|
|
153
|
+
progressPercentage={progress}
|
|
154
|
+
accept=".pdf,.docx,.xlsx"
|
|
155
|
+
maxFileSize={10 * 1024 * 1024} // 10 MB
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Validate file type and size
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<ZestFileUpload
|
|
167
|
+
label="Upload image"
|
|
168
|
+
onFileSelect={async (file) => {
|
|
169
|
+
if (!file) return;
|
|
170
|
+
await uploadToServer(file);
|
|
171
|
+
}}
|
|
172
|
+
accept="image/png,image/jpeg,image/webp"
|
|
173
|
+
maxFileSize={5 * 1024 * 1024} // 5 MB
|
|
174
|
+
onError={(message) => console.error("Validation failed:", message)}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### Camera capture with image crop
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<ZestFileUpload
|
|
184
|
+
label="Take a photo"
|
|
185
|
+
onFileSelect={async (file) => {
|
|
186
|
+
if (!file) return;
|
|
187
|
+
// file is already compressed (JPEG, max 1920×1080 by default)
|
|
188
|
+
await uploadPhoto(file);
|
|
189
|
+
}}
|
|
190
|
+
accept="image/*"
|
|
191
|
+
capture="environment" // prefer rear camera
|
|
192
|
+
/>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Programmatic clear via ref
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { useRef } from "react";
|
|
201
|
+
import { ZestFileUpload, FileUploadRef } from "jattac.libs.web.zest-file-upload";
|
|
202
|
+
|
|
203
|
+
function FormWithUpload() {
|
|
204
|
+
const uploadRef = useRef<FileUploadRef>(null);
|
|
205
|
+
|
|
206
|
+
const handleFormReset = () => {
|
|
207
|
+
uploadRef.current?.clearFile();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<form onReset={handleFormReset}>
|
|
212
|
+
<ZestFileUpload
|
|
213
|
+
ref={uploadRef}
|
|
214
|
+
label="Attachment"
|
|
215
|
+
onFileSelect={async (file) => { /* ... */ }}
|
|
216
|
+
/>
|
|
217
|
+
<button type="reset">Clear</button>
|
|
218
|
+
</form>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Multi-file upload — batch callback
|
|
226
|
+
|
|
227
|
+
All files are uploaded in a single call. Ideal when your API accepts an array or `multipart/form-data` with multiple fields.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
|
|
231
|
+
|
|
232
|
+
function BatchUpload() {
|
|
233
|
+
const handleFiles = async (files: File[]) => {
|
|
234
|
+
const formData = new FormData();
|
|
235
|
+
files.forEach((file) => formData.append("files", file));
|
|
236
|
+
|
|
237
|
+
const res = await fetch("/api/upload/batch", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: formData,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!res.ok) throw new Error("Upload failed");
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<ZestFileUpload
|
|
247
|
+
label="Upload documents"
|
|
248
|
+
multiple
|
|
249
|
+
onFileSelect={async () => {}} // required prop — unused when onFilesSelect is provided
|
|
250
|
+
onFilesSelect={handleFiles}
|
|
251
|
+
accept=".pdf,.docx"
|
|
252
|
+
maxFiles={10}
|
|
253
|
+
maxFileSize={20 * 1024 * 1024} // 20 MB per file
|
|
254
|
+
/>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Multi-file upload — concurrent per-file with real-time progress
|
|
262
|
+
|
|
263
|
+
Each file uploads in parallel. Progress is tracked per-file via `FileEntryId`.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { useState } from "react";
|
|
267
|
+
import { ZestFileUpload, FileEntryId } from "jattac.libs.web.zest-file-upload";
|
|
268
|
+
|
|
269
|
+
function ParallelUpload() {
|
|
270
|
+
const [progress, setProgress] = useState<Record<FileEntryId, number>>({});
|
|
271
|
+
|
|
272
|
+
// Called concurrently for each file when onFilesSelect is not provided
|
|
273
|
+
const handleFile = async (file: File) => {
|
|
274
|
+
// Note: in multi mode without onFilesSelect, onFileSelect receives individual files
|
|
275
|
+
// You won't have the FileEntryId here — use onFilesSelect for per-file progress
|
|
276
|
+
await uploadFile(file);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<ZestFileUpload
|
|
281
|
+
label="Upload files"
|
|
282
|
+
multiple
|
|
283
|
+
onFileSelect={handleFile}
|
|
284
|
+
accept="image/*"
|
|
285
|
+
maxFiles={20}
|
|
286
|
+
/>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
> **Tip:** For per-file progress bars, use `onFilesSelect` + `filesProgress` together (see next example).
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Multi-file upload — per-file progress bars
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { useState } from "react";
|
|
299
|
+
import {
|
|
300
|
+
ZestFileUpload,
|
|
301
|
+
FileEntryId,
|
|
302
|
+
} from "jattac.libs.web.zest-file-upload";
|
|
303
|
+
|
|
304
|
+
function UploadWithPerFileProgress() {
|
|
305
|
+
const [progress, setProgress] = useState<Record<FileEntryId, number>>({});
|
|
306
|
+
|
|
307
|
+
const handleFiles = async (files: File[]) => {
|
|
308
|
+
// Upload each file independently with XHR for progress events
|
|
309
|
+
await Promise.allSettled(
|
|
310
|
+
files.map(async (file, i) => {
|
|
311
|
+
// In a real app you'd receive FileEntryId from the queue —
|
|
312
|
+
// here we use a simplified index-based key for illustration.
|
|
313
|
+
// Wire real IDs by using a custom upload manager.
|
|
314
|
+
const formData = new FormData();
|
|
315
|
+
formData.append("file", file);
|
|
316
|
+
await fetch("/api/upload", { method: "POST", body: formData });
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<ZestFileUpload
|
|
323
|
+
label="Upload images"
|
|
324
|
+
multiple
|
|
325
|
+
onFileSelect={async () => {}}
|
|
326
|
+
onFilesSelect={handleFiles}
|
|
327
|
+
filesProgress={progress}
|
|
328
|
+
accept="image/*"
|
|
329
|
+
maxFiles={5}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### Multi-file with ref — clear all programmatically
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { useRef } from "react";
|
|
341
|
+
import { ZestFileUpload, FileUploadRef } from "jattac.libs.web.zest-file-upload";
|
|
342
|
+
|
|
343
|
+
function ClearableMultiUpload() {
|
|
344
|
+
const ref = useRef<FileUploadRef>(null);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<>
|
|
348
|
+
<ZestFileUpload
|
|
349
|
+
ref={ref}
|
|
350
|
+
label="Upload files"
|
|
351
|
+
multiple
|
|
352
|
+
onFileSelect={async (file) => { await upload(file!); }}
|
|
353
|
+
maxFiles={8}
|
|
354
|
+
/>
|
|
355
|
+
<button onClick={() => ref.current?.clearAll()}>
|
|
356
|
+
Reset queue
|
|
357
|
+
</button>
|
|
358
|
+
</>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### Custom labels (i18n)
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
<ZestFileUpload
|
|
369
|
+
label="Télécharger un fichier"
|
|
370
|
+
onFileSelect={async (file) => { /* ... */ }}
|
|
371
|
+
labels={{
|
|
372
|
+
browseFiles: "Parcourir",
|
|
373
|
+
takePhoto: "Prendre une photo",
|
|
374
|
+
uploadComplete: "Téléchargement terminé !",
|
|
375
|
+
uploadFailed: "Échec du téléchargement. Veuillez réessayer.",
|
|
376
|
+
uploading: "Téléchargement en cours...",
|
|
377
|
+
noFileSelected: "Aucun fichier. Cliquez ou déposez un fichier ici.",
|
|
378
|
+
invalidFileType: "Type de fichier non valide",
|
|
379
|
+
}}
|
|
380
|
+
multiLabels={{
|
|
381
|
+
filesQueued: "fichiers en attente",
|
|
382
|
+
clearAll: "Tout effacer",
|
|
383
|
+
pending: "En attente",
|
|
384
|
+
retryFile: "Réessayer",
|
|
385
|
+
removeFile: "Supprimer",
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
### Custom icons
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import { Upload, Camera, X } from "lucide-react"; // any icon library
|
|
396
|
+
|
|
397
|
+
<ZestFileUpload
|
|
398
|
+
label="Upload"
|
|
399
|
+
onFileSelect={async (file) => { /* ... */ }}
|
|
400
|
+
icons={{
|
|
401
|
+
browse: <Upload size={18} />,
|
|
402
|
+
camera: <Camera size={18} />,
|
|
403
|
+
close: <X size={16} />,
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
### Disabled state
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
<ZestFileUpload
|
|
414
|
+
label="Upload (locked)"
|
|
415
|
+
onFileSelect={async () => {}}
|
|
416
|
+
disabled={isSubmitting}
|
|
417
|
+
/>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
### With error boundary
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
<ZestFileUpload
|
|
426
|
+
label="Upload"
|
|
427
|
+
onFileSelect={async (file) => { /* ... */ }}
|
|
428
|
+
errorBoundary
|
|
429
|
+
/>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### Next.js (App Router)
|
|
435
|
+
|
|
436
|
+
Mark the parent as a Client Component — the file picker and camera APIs are browser-only.
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
"use client";
|
|
440
|
+
|
|
441
|
+
import { ZestFileUpload } from "jattac.libs.web.zest-file-upload";
|
|
442
|
+
|
|
443
|
+
export default function UploadPage() {
|
|
444
|
+
return (
|
|
445
|
+
<ZestFileUpload
|
|
446
|
+
label="Upload file"
|
|
447
|
+
onFileSelect={async (file) => {
|
|
448
|
+
if (!file) return;
|
|
449
|
+
// call a server action or fetch route handler
|
|
450
|
+
const formData = new FormData();
|
|
451
|
+
formData.append("file", file);
|
|
452
|
+
await fetch("/api/upload", { method: "POST", body: formData });
|
|
453
|
+
}}
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Type Reference
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// Per-file entry in multi mode
|
|
465
|
+
interface FileEntry {
|
|
466
|
+
id: FileEntryId; // stable UUID
|
|
467
|
+
file: File;
|
|
468
|
+
status: FileEntryStatus; // "pending" | "uploading" | "complete" | "error"
|
|
469
|
+
error: string | null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
type FileEntryId = string;
|
|
473
|
+
type FileEntryStatus = "pending" | "uploading" | "complete" | "error";
|
|
474
|
+
|
|
475
|
+
// Multi-mode label overrides
|
|
476
|
+
interface MultiFileUploadLabels {
|
|
477
|
+
addMoreFiles: string;
|
|
478
|
+
removeFile: string;
|
|
479
|
+
filesQueued: string;
|
|
480
|
+
clearAll: string;
|
|
481
|
+
pending: string;
|
|
482
|
+
retryFile: string;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// All label overrides
|
|
486
|
+
interface FileUploadLabels {
|
|
487
|
+
browseFiles: string;
|
|
488
|
+
takePhoto: string;
|
|
489
|
+
uploadComplete: string;
|
|
490
|
+
uploadFailed: string;
|
|
491
|
+
uploading: string;
|
|
492
|
+
noFileSelected: string;
|
|
493
|
+
invalidFileType: string;
|
|
494
|
+
cameraError: string;
|
|
495
|
+
retake: string;
|
|
496
|
+
usePhoto: string;
|
|
497
|
+
cancel: string;
|
|
498
|
+
switchCamera: string;
|
|
499
|
+
toggleFlash: string;
|
|
500
|
+
cropTitle: string;
|
|
501
|
+
cropSave: string;
|
|
502
|
+
cropCancel: string;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Icon overrides
|
|
506
|
+
interface FileUploadIcons {
|
|
507
|
+
camera: React.ReactNode;
|
|
508
|
+
browse: React.ReactNode;
|
|
509
|
+
close: React.ReactNode;
|
|
510
|
+
flashOn: React.ReactNode;
|
|
511
|
+
flashOff: React.ReactNode;
|
|
512
|
+
switchCamera: React.ReactNode;
|
|
513
|
+
retake: React.ReactNode;
|
|
514
|
+
confirm: React.ReactNode;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Ref handle
|
|
518
|
+
interface FileUploadRef {
|
|
519
|
+
clearFile: () => void; // resets single-mode state + calls onFileSelect(null)
|
|
520
|
+
clearAll: () => void; // clears multi-mode queue; no-op in single mode
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Changelog
|
|
527
|
+
|
|
528
|
+
### 0.2.0
|
|
529
|
+
- **Multi-file mode** (`multiple` prop) with parallel real-time uploads via `Promise.allSettled`
|
|
530
|
+
- Per-file status queue with animated rows (slide-in, shimmer progress, green pop on complete, shake on error)
|
|
531
|
+
- `onFilesSelect` batch callback
|
|
532
|
+
- `filesProgress` for per-file progress bars
|
|
533
|
+
- `maxFiles` cap
|
|
534
|
+
- `multiLabels` for multi-mode i18n
|
|
535
|
+
- `clearAll()` ref method
|
|
536
|
+
- Camera photos in multi mode enqueue and upload independently
|
|
537
|
+
|
|
538
|
+
### 0.1.0
|
|
539
|
+
- Initial release: single-file upload, drag-drop, camera capture, image crop, compression, progress bar, validation, custom labels/icons, SSR support
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## License
|
|
544
|
+
|
|
545
|
+
MIT © Jattac
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jattac.libs.web.zest-file-upload",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A production-ready React file upload component with drag-drop, camera capture, image crop, compression, progress tracking, and mobile-optimised UI. CSS is auto-injected — no separate import required.",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|