uplofile 2.1.0 → 2.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 +19 -19
- package/dist/index.d.ts +13 -6
- package/dist/index.mjs +33 -19
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,11 +7,11 @@ Accessible, unstyled primitives for building your own upload UI — with drag‑
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- React 16+ compatible
|
|
11
|
-
- Drag‑and‑drop or click‑to‑upload
|
|
12
|
-
- Upload progress, plus cancel/retry/remove actions
|
|
10
|
+
- React 16+ compatible
|
|
11
|
+
- Drag‑and‑drop or click‑to‑upload
|
|
12
|
+
- Upload progress, plus cancel/retry/remove actions
|
|
13
13
|
- **Custom Validation:** Use `beforeUpload` to validate files before they start uploading
|
|
14
|
-
- Hidden input for form submissions
|
|
14
|
+
- Hidden input for form submissions
|
|
15
15
|
- Unstyled — bring your own design
|
|
16
16
|
|
|
17
17
|
---
|
|
@@ -36,25 +36,25 @@ Import and use the components in your React component:
|
|
|
36
36
|
"use client";
|
|
37
37
|
|
|
38
38
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
UplofileDropzone,
|
|
40
|
+
UplofilePreview,
|
|
41
|
+
UplofileRoot,
|
|
42
|
+
UplofileTrigger,
|
|
43
43
|
} from "uplofile";
|
|
44
44
|
|
|
45
45
|
export default function Basic() {
|
|
46
46
|
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
<UplofileRoot onRemove={onRemove} upload={upload} removeMode={"strict"}>
|
|
48
|
+
<UplofileDropzone className={"border p-2 rounded"}>
|
|
49
|
+
<span>Drop your files here or</span>{" "}
|
|
50
|
+
<UplofileTrigger className={"underline text-blue-500"}>
|
|
51
|
+
Select file
|
|
52
|
+
</UplofileTrigger>
|
|
53
|
+
<div className={"border-t my-6 py-6"}>
|
|
54
|
+
<UplofilePreview />
|
|
55
|
+
</div>
|
|
56
|
+
</UplofileDropzone>
|
|
57
|
+
</UplofileRoot>
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare const Dropzone: ({ asChild, ...rest }: {
|
|
|
6
6
|
asChild?: boolean;
|
|
7
7
|
} & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
|
|
8
8
|
|
|
9
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
9
10
|
type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
|
|
10
11
|
type UploadFileItem<TMeta = any> = {
|
|
11
12
|
uid: string;
|
|
@@ -19,13 +20,16 @@ type UploadFileItem<TMeta = any> = {
|
|
|
19
20
|
error?: string;
|
|
20
21
|
meta?: TMeta;
|
|
21
22
|
};
|
|
22
|
-
type UploadResult = {
|
|
23
|
+
type UploadResult<TMeta = any> = {
|
|
23
24
|
url: string;
|
|
24
25
|
id?: string;
|
|
26
|
+
meta?: TMeta;
|
|
27
|
+
previewUrl?: string;
|
|
25
28
|
};
|
|
26
29
|
type UplofileRootRef<TMeta = any> = {
|
|
27
30
|
setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
|
|
28
31
|
getItems: () => UploadFileItem<TMeta>[];
|
|
32
|
+
isLoading: boolean;
|
|
29
33
|
onDrop: (e: DragEvent) => void;
|
|
30
34
|
onDragOver: (e: DragEvent) => void;
|
|
31
35
|
openFileDialog: () => void;
|
|
@@ -40,7 +44,7 @@ type BeforeUploadResult<TMeta = any> = boolean | Array<{
|
|
|
40
44
|
}>;
|
|
41
45
|
type RootProps<TMeta = any> = PropsWithChildren<{
|
|
42
46
|
multiple?: boolean;
|
|
43
|
-
initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta"
|
|
47
|
+
initial?: MaybePromise<Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>>;
|
|
44
48
|
/**
|
|
45
49
|
* optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
|
|
46
50
|
* strict: call onRemove first; only remove from UI if it succeeds.
|
|
@@ -52,7 +56,7 @@ type RootProps<TMeta = any> = PropsWithChildren<{
|
|
|
52
56
|
accept?: string;
|
|
53
57
|
beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
|
|
54
58
|
onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
|
|
55
|
-
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult
|
|
59
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<TMeta>>;
|
|
56
60
|
onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
|
|
57
61
|
}>;
|
|
58
62
|
type ItemActions = {
|
|
@@ -63,6 +67,7 @@ type ItemActions = {
|
|
|
63
67
|
type ImageUploaderContextValue<TMeta = any> = {
|
|
64
68
|
items: UploadFileItem<TMeta>[];
|
|
65
69
|
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
70
|
+
isLoading: boolean;
|
|
66
71
|
disabled?: boolean;
|
|
67
72
|
multiple: boolean;
|
|
68
73
|
accept: string;
|
|
@@ -89,6 +94,7 @@ type ImageUploaderContextValue<TMeta = any> = {
|
|
|
89
94
|
};
|
|
90
95
|
type TriggerRenderProps<TMeta = any> = {
|
|
91
96
|
items: UploadFileItem<TMeta>[];
|
|
97
|
+
isLoading: boolean;
|
|
92
98
|
isUploading: boolean;
|
|
93
99
|
uploadingCount: number;
|
|
94
100
|
doneCount: number;
|
|
@@ -98,6 +104,7 @@ type TriggerRenderProps<TMeta = any> = {
|
|
|
98
104
|
};
|
|
99
105
|
type PreviewRenderProps<TMeta = any> = {
|
|
100
106
|
items: UploadFileItem<TMeta>[];
|
|
107
|
+
isLoading: boolean;
|
|
101
108
|
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
102
109
|
actions: ItemActions;
|
|
103
110
|
};
|
|
@@ -120,7 +127,7 @@ declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runt
|
|
|
120
127
|
|
|
121
128
|
declare const Root: React.ForwardRefExoticComponent<{
|
|
122
129
|
multiple?: boolean;
|
|
123
|
-
initial?: Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[] | undefined;
|
|
130
|
+
initial?: MaybePromise<Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[]> | undefined;
|
|
124
131
|
removeMode?: "optimistic" | "strict";
|
|
125
132
|
name?: string;
|
|
126
133
|
maxCount?: number;
|
|
@@ -128,7 +135,7 @@ declare const Root: React.ForwardRefExoticComponent<{
|
|
|
128
135
|
accept?: string;
|
|
129
136
|
beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
|
|
130
137
|
onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
|
|
131
|
-
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult
|
|
138
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<unknown>>;
|
|
132
139
|
onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
|
|
133
140
|
} & {
|
|
134
141
|
children?: React.ReactNode | undefined;
|
|
@@ -147,4 +154,4 @@ declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[
|
|
|
147
154
|
declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
148
155
|
|
|
149
156
|
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|
|
150
|
-
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
|
157
|
+
export type { BeforeUploadResult, ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
package/dist/index.mjs
CHANGED
|
@@ -117,6 +117,7 @@ const acceptsFile = (file, accept)=>{
|
|
|
117
117
|
const UploaderCtx = /*#__PURE__*/ createContext(null);
|
|
118
118
|
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", beforeUpload, name = "image", maxCount, disabled, children }, ref)=>{
|
|
119
119
|
const [items, setItems] = useState([]);
|
|
120
|
+
const [isLoading, setIsLoading] = useState(Array.isArray(initial) ? initial.length > 0 : !!initial);
|
|
120
121
|
const controllers = useRef(new Map());
|
|
121
122
|
const removeControllers = useRef(new Map());
|
|
122
123
|
const inputRef = useRef(null);
|
|
@@ -124,21 +125,28 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
124
125
|
// Hydrate initial items from the server and keep them marked as done
|
|
125
126
|
useEffect(()=>{
|
|
126
127
|
if (hasHydratedInitialRef.current) return;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
const hydrate = async ()=>{
|
|
129
|
+
try {
|
|
130
|
+
const arr = await initial;
|
|
131
|
+
if (!Array.isArray(arr) || arr.length === 0) return;
|
|
132
|
+
const mapped = arr.map((it)=>{
|
|
133
|
+
return {
|
|
134
|
+
uid: it.uid || it.id,
|
|
135
|
+
id: it.id,
|
|
136
|
+
name: it.name,
|
|
137
|
+
url: it.url,
|
|
138
|
+
status: "done",
|
|
139
|
+
meta: it.meta
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
// Only hydrate if the user hasn't already added/modified items locally
|
|
143
|
+
setItems((prev)=>prev.length === 0 ? mapped : prev);
|
|
144
|
+
hasHydratedInitialRef.current = true;
|
|
145
|
+
} finally{
|
|
146
|
+
setIsLoading(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
void hydrate();
|
|
142
150
|
}, [
|
|
143
151
|
initial
|
|
144
152
|
]);
|
|
@@ -183,13 +191,14 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
183
191
|
} catch {
|
|
184
192
|
/*fail silently*/ }
|
|
185
193
|
}
|
|
186
|
-
const serverPreview = result
|
|
194
|
+
const serverPreview = result.previewUrl ?? result.preview ?? result.url;
|
|
187
195
|
return {
|
|
188
196
|
...it,
|
|
189
197
|
status: "done",
|
|
190
198
|
url: result.url,
|
|
191
199
|
id: result.id ?? it.id,
|
|
192
200
|
previewUrl: serverPreview,
|
|
201
|
+
meta: result.meta ?? it.meta,
|
|
193
202
|
progress: 100
|
|
194
203
|
};
|
|
195
204
|
}));
|
|
@@ -373,6 +382,7 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
373
382
|
}, []);
|
|
374
383
|
const ctx = {
|
|
375
384
|
items,
|
|
385
|
+
isLoading,
|
|
376
386
|
disabled,
|
|
377
387
|
multiple,
|
|
378
388
|
accept,
|
|
@@ -405,6 +415,7 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
405
415
|
useImperativeHandle(ref, ()=>({
|
|
406
416
|
setItems: emitChange,
|
|
407
417
|
getItems: ()=>items,
|
|
418
|
+
isLoading,
|
|
408
419
|
onDrop,
|
|
409
420
|
onDragOver,
|
|
410
421
|
openFileDialog: ()=>inputRef.current?.click(),
|
|
@@ -412,6 +423,7 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
412
423
|
}), [
|
|
413
424
|
emitChange,
|
|
414
425
|
items,
|
|
426
|
+
isLoading,
|
|
415
427
|
onDrop,
|
|
416
428
|
onDragOver,
|
|
417
429
|
actions
|
|
@@ -449,11 +461,12 @@ const Dropzone = ({ asChild, ...rest })=>{
|
|
|
449
461
|
};
|
|
450
462
|
|
|
451
463
|
const Preview = ({ render })=>{
|
|
452
|
-
const { items, actions, setItems } = useUplofile();
|
|
464
|
+
const { items, actions, setItems, isLoading } = useUplofile();
|
|
453
465
|
if (render && typeof render === "function") return render({
|
|
454
466
|
items,
|
|
455
467
|
setItems,
|
|
456
|
-
actions
|
|
468
|
+
actions,
|
|
469
|
+
isLoading
|
|
457
470
|
});
|
|
458
471
|
if (items.length === 0) return null;
|
|
459
472
|
return /*#__PURE__*/ jsx("div", {
|
|
@@ -761,7 +774,7 @@ const Remove = ({ uid, asChild, ...rest })=>{
|
|
|
761
774
|
};
|
|
762
775
|
|
|
763
776
|
const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
764
|
-
const { openFileDialog, disabled, items } = useUplofile();
|
|
777
|
+
const { openFileDialog, disabled, items, isLoading } = useUplofile();
|
|
765
778
|
const Comp = asChild ? Slot : "button";
|
|
766
779
|
const uploading = items.filter((i)=>i.status === "uploading");
|
|
767
780
|
const uploadingCount = uploading.length;
|
|
@@ -770,6 +783,7 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
|
770
783
|
const totalProgress = uploadingCount ? Math.round(uploading.reduce((acc, it)=>acc + (typeof it.progress === "number" ? it.progress : 0), 0) / uploadingCount) : undefined;
|
|
771
784
|
const api = {
|
|
772
785
|
items,
|
|
786
|
+
isLoading,
|
|
773
787
|
isUploading: uploadingCount > 0,
|
|
774
788
|
uploadingCount,
|
|
775
789
|
doneCount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplofile",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Composable file‑upload components for React.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chris Josh <KristofaJosh>",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"clean": "rm -rf dist",
|
|
42
42
|
"prepare": "pnpm run build",
|
|
43
43
|
"prepublishOnly": "pnpm run build",
|
|
44
|
-
"test": "vitest"
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"format": "prettier --write ."
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@radix-ui/react-slot": "1.1.0"
|