uplofile 2.2.0 → 2.2.2
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 +10 -2
- package/dist/index.mjs +65 -25
- 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
|
@@ -29,10 +29,12 @@ type UploadResult<TMeta = any> = {
|
|
|
29
29
|
type UplofileRootRef<TMeta = any> = {
|
|
30
30
|
setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
|
|
31
31
|
getItems: () => UploadFileItem<TMeta>[];
|
|
32
|
+
isLoading: boolean;
|
|
32
33
|
onDrop: (e: DragEvent) => void;
|
|
33
34
|
onDragOver: (e: DragEvent) => void;
|
|
34
35
|
openFileDialog: () => void;
|
|
35
36
|
actions: ItemActions;
|
|
37
|
+
onLoadingChange?: (isLoading: boolean) => void;
|
|
36
38
|
};
|
|
37
39
|
type BeforeUploadResult<TMeta = any> = boolean | Array<{
|
|
38
40
|
valid: boolean;
|
|
@@ -55,6 +57,7 @@ type RootProps<TMeta = any> = PropsWithChildren<{
|
|
|
55
57
|
accept?: string;
|
|
56
58
|
beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
|
|
57
59
|
onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
|
|
60
|
+
onLoadingChange?: (isLoading: boolean) => void;
|
|
58
61
|
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<TMeta>>;
|
|
59
62
|
onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
|
|
60
63
|
}>;
|
|
@@ -66,6 +69,7 @@ type ItemActions = {
|
|
|
66
69
|
type ImageUploaderContextValue<TMeta = any> = {
|
|
67
70
|
items: UploadFileItem<TMeta>[];
|
|
68
71
|
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
72
|
+
isLoading: boolean;
|
|
69
73
|
disabled?: boolean;
|
|
70
74
|
multiple: boolean;
|
|
71
75
|
accept: string;
|
|
@@ -92,6 +96,7 @@ type ImageUploaderContextValue<TMeta = any> = {
|
|
|
92
96
|
};
|
|
93
97
|
type TriggerRenderProps<TMeta = any> = {
|
|
94
98
|
items: UploadFileItem<TMeta>[];
|
|
99
|
+
isLoading: boolean;
|
|
95
100
|
isUploading: boolean;
|
|
96
101
|
uploadingCount: number;
|
|
97
102
|
doneCount: number;
|
|
@@ -101,14 +106,16 @@ type TriggerRenderProps<TMeta = any> = {
|
|
|
101
106
|
};
|
|
102
107
|
type PreviewRenderProps<TMeta = any> = {
|
|
103
108
|
items: UploadFileItem<TMeta>[];
|
|
109
|
+
isLoading: boolean;
|
|
104
110
|
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
105
111
|
actions: ItemActions;
|
|
106
112
|
};
|
|
107
113
|
|
|
108
114
|
type Props<TMeta = any> = {
|
|
109
115
|
render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
|
|
116
|
+
className?: string;
|
|
110
117
|
};
|
|
111
|
-
declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
|
|
118
|
+
declare const Preview: <TMeta = any>({ render, className, }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
|
|
112
119
|
declare const HiddenInput: ({ name }: {
|
|
113
120
|
name?: string;
|
|
114
121
|
}) => react_jsx_runtime.JSX.Element;
|
|
@@ -131,13 +138,14 @@ declare const Root: React.ForwardRefExoticComponent<{
|
|
|
131
138
|
accept?: string;
|
|
132
139
|
beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
|
|
133
140
|
onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
|
|
141
|
+
onLoadingChange?: (isLoading: boolean) => void;
|
|
134
142
|
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<unknown>>;
|
|
135
143
|
onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
|
|
136
144
|
} & {
|
|
137
145
|
children?: React.ReactNode | undefined;
|
|
138
146
|
} & React.RefAttributes<UplofileRootRef<unknown>>>;
|
|
139
147
|
|
|
140
|
-
declare const Trigger: <TMeta = any>({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
148
|
+
declare const Trigger: <TMeta = any>({ asChild, children, render, onClick, ...rest }: PropsWithChildren<{
|
|
141
149
|
asChild?: boolean;
|
|
142
150
|
render?: (api: TriggerRenderProps<TMeta>) => React.ReactNode;
|
|
143
151
|
children?: React.ReactNode | ((api: TriggerRenderProps<TMeta>) => React.ReactNode);
|
package/dist/index.mjs
CHANGED
|
@@ -115,31 +115,56 @@ const acceptsFile = (file, accept)=>{
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
const UploaderCtx = /*#__PURE__*/ createContext(null);
|
|
118
|
-
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", beforeUpload, name = "image", maxCount, disabled, children }, ref)=>{
|
|
118
|
+
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, onLoadingChange, 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);
|
|
123
124
|
const hasHydratedInitialRef = useRef(false);
|
|
125
|
+
const onLoadingChangeRef = useRef(onLoadingChange);
|
|
126
|
+
useEffect(()=>{
|
|
127
|
+
onLoadingChangeRef.current = onLoadingChange;
|
|
128
|
+
}, [
|
|
129
|
+
onLoadingChange
|
|
130
|
+
]);
|
|
131
|
+
useEffect(()=>{
|
|
132
|
+
onLoadingChangeRef.current?.(isLoading);
|
|
133
|
+
}, [
|
|
134
|
+
isLoading
|
|
135
|
+
]);
|
|
124
136
|
// Hydrate initial items from the server and keep them marked as done
|
|
125
137
|
useEffect(()=>{
|
|
126
138
|
if (hasHydratedInitialRef.current) return;
|
|
127
139
|
const hydrate = async ()=>{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
try {
|
|
141
|
+
const arr = await initial;
|
|
142
|
+
if (!Array.isArray(arr) || arr.length === 0) return;
|
|
143
|
+
const mapped = arr.map((it)=>{
|
|
144
|
+
return {
|
|
145
|
+
uid: it.uid || it.id,
|
|
146
|
+
id: it.id,
|
|
147
|
+
name: it.name,
|
|
148
|
+
url: it.url,
|
|
149
|
+
status: "done",
|
|
150
|
+
meta: it.meta
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
// Append server items if user already added files while loading; avoid replacing
|
|
154
|
+
setItems((prev)=>{
|
|
155
|
+
if (prev.length === 0) return mapped;
|
|
156
|
+
const existing = new Set(prev.map((i)=>i.uid));
|
|
157
|
+
const toAppend = mapped.filter((m)=>!existing.has(m.uid));
|
|
158
|
+
if (toAppend.length === 0) return prev;
|
|
159
|
+
return [
|
|
160
|
+
...prev,
|
|
161
|
+
...toAppend
|
|
162
|
+
];
|
|
163
|
+
});
|
|
164
|
+
hasHydratedInitialRef.current = true;
|
|
165
|
+
} finally{
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
}
|
|
143
168
|
};
|
|
144
169
|
void hydrate();
|
|
145
170
|
}, [
|
|
@@ -377,6 +402,7 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
377
402
|
}, []);
|
|
378
403
|
const ctx = {
|
|
379
404
|
items,
|
|
405
|
+
isLoading,
|
|
380
406
|
disabled,
|
|
381
407
|
multiple,
|
|
382
408
|
accept,
|
|
@@ -409,13 +435,21 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
409
435
|
useImperativeHandle(ref, ()=>({
|
|
410
436
|
setItems: emitChange,
|
|
411
437
|
getItems: ()=>items,
|
|
438
|
+
isLoading,
|
|
412
439
|
onDrop,
|
|
413
440
|
onDragOver,
|
|
414
441
|
openFileDialog: ()=>inputRef.current?.click(),
|
|
415
|
-
actions
|
|
442
|
+
actions,
|
|
443
|
+
get onLoadingChange () {
|
|
444
|
+
return onLoadingChangeRef.current;
|
|
445
|
+
},
|
|
446
|
+
set onLoadingChange (callback){
|
|
447
|
+
onLoadingChangeRef.current = callback;
|
|
448
|
+
}
|
|
416
449
|
}), [
|
|
417
450
|
emitChange,
|
|
418
451
|
items,
|
|
452
|
+
isLoading,
|
|
419
453
|
onDrop,
|
|
420
454
|
onDragOver,
|
|
421
455
|
actions
|
|
@@ -452,19 +486,23 @@ const Dropzone = ({ asChild, ...rest })=>{
|
|
|
452
486
|
});
|
|
453
487
|
};
|
|
454
488
|
|
|
455
|
-
const Preview = ({ render })=>{
|
|
456
|
-
const { items, actions, setItems } = useUplofile();
|
|
489
|
+
const Preview = ({ render, className = "" })=>{
|
|
490
|
+
const { items, actions, setItems, isLoading } = useUplofile();
|
|
457
491
|
if (render && typeof render === "function") return render({
|
|
458
492
|
items,
|
|
459
493
|
setItems,
|
|
460
|
-
actions
|
|
494
|
+
actions,
|
|
495
|
+
isLoading
|
|
461
496
|
});
|
|
462
497
|
if (items.length === 0) return null;
|
|
463
498
|
return /*#__PURE__*/ jsx("div", {
|
|
464
499
|
"data-part": "preview",
|
|
465
500
|
className: "uplofile-preview",
|
|
466
501
|
children: /*#__PURE__*/ jsx("div", {
|
|
467
|
-
className:
|
|
502
|
+
className: [
|
|
503
|
+
"uplofile-preview__wrapper grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5",
|
|
504
|
+
className
|
|
505
|
+
].join(" ").trim(),
|
|
468
506
|
children: items.map((item)=>/*#__PURE__*/ jsxs("div", {
|
|
469
507
|
onClick: (e)=>e.stopPropagation(),
|
|
470
508
|
className: `uplofile-preview__item group relative aspect-square overflow-hidden rounded-xl border bg-muted/5 transition-all ${item.status === "error" ? "border-red-200 bg-red-50/30 hover:shadow-md" : "hover:shadow-md hover:ring-2 hover:ring-primary/20"}`,
|
|
@@ -764,8 +802,8 @@ const Remove = ({ uid, asChild, ...rest })=>{
|
|
|
764
802
|
});
|
|
765
803
|
};
|
|
766
804
|
|
|
767
|
-
const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
768
|
-
const { openFileDialog, disabled, items } = useUplofile();
|
|
805
|
+
const Trigger = ({ asChild, children, render, onClick, ...rest })=>{
|
|
806
|
+
const { openFileDialog, disabled, items, isLoading } = useUplofile();
|
|
769
807
|
const Comp = asChild ? Slot : "button";
|
|
770
808
|
const uploading = items.filter((i)=>i.status === "uploading");
|
|
771
809
|
const uploadingCount = uploading.length;
|
|
@@ -774,6 +812,7 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
|
774
812
|
const totalProgress = uploadingCount ? Math.round(uploading.reduce((acc, it)=>acc + (typeof it.progress === "number" ? it.progress : 0), 0) / uploadingCount) : undefined;
|
|
775
813
|
const api = {
|
|
776
814
|
items,
|
|
815
|
+
isLoading,
|
|
777
816
|
isUploading: uploadingCount > 0,
|
|
778
817
|
uploadingCount,
|
|
779
818
|
doneCount,
|
|
@@ -785,12 +824,13 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
|
785
824
|
type: asChild ? undefined : "button",
|
|
786
825
|
"aria-disabled": disabled,
|
|
787
826
|
"data-part": "trigger",
|
|
827
|
+
...rest,
|
|
788
828
|
onClick: (e)=>{
|
|
789
829
|
if (disabled) return;
|
|
790
|
-
|
|
830
|
+
onClick?.(e);
|
|
831
|
+
if (e.defaultPrevented) return;
|
|
791
832
|
openFileDialog();
|
|
792
833
|
},
|
|
793
|
-
...rest,
|
|
794
834
|
children: render ? render(api) : children
|
|
795
835
|
});
|
|
796
836
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplofile",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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"
|