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.
Files changed (2) hide show
  1. package/README.md +545 -0
  2. 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.0",
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",