uplofile 1.0.0 → 1.0.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 +0 -4
- package/dist/index.d.ts +2 -1
- package/package.json +6 -15
- package/dist/index.cjs +0 -418
- package/dist/index.d.mts +0 -114
- package/dist/index.d.mts.map +0 -1
- package/dist/index.js +0 -408
- package/dist/output.css +0 -397
package/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# Uplofile
|
|
2
2
|
|
|
3
|
-
[](https://uplofile.kristofajosh.dev)
|
|
4
|
-
|
|
5
3
|
**Composable file‑upload components for React.**
|
|
6
4
|
Accessible, unstyled primitives for building your own upload UI — with drag‑and‑drop, progress indicators, cancel/retry/remove actions, and an optional hidden input for classic form posts.
|
|
7
5
|
|
|
8
|
-
> Heads‑up: Uplofile is currently in Beta. Expect some rough edges and occasional API tweaks. We’d love your feedback — please kick the tires and open issues!
|
|
9
|
-
|
|
10
6
|
---
|
|
11
7
|
|
|
12
8
|
## Features
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="es2015.iterable" />
|
|
1
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
3
|
import React, { HTMLAttributes, PropsWithChildren, RefObject, ChangeEvent, DragEvent, ButtonHTMLAttributes } from 'react';
|
|
3
4
|
|
|
@@ -86,7 +87,7 @@ type PreviewRenderProps = {
|
|
|
86
87
|
type Props = {
|
|
87
88
|
render?: (api: PreviewRenderProps) => React.ReactNode;
|
|
88
89
|
};
|
|
89
|
-
declare const Preview: ({ render }: Props) => string | number | boolean | Iterable<React.ReactNode> |
|
|
90
|
+
declare const Preview: ({ render }: Props) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
|
|
90
91
|
declare const HiddenInput: ({ name }: {
|
|
91
92
|
name?: string;
|
|
92
93
|
}) => react_jsx_runtime.JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplofile",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Composable file‑upload components for React (Beta).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chris Josh <KristofaJosh>",
|
|
@@ -23,28 +23,19 @@
|
|
|
23
23
|
"customizable"
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
26
|
-
"main": "dist/index.
|
|
27
|
-
"
|
|
28
|
-
"types": "dist/index.d.ts",
|
|
26
|
+
"main": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
29
28
|
"exports": {
|
|
30
29
|
".": {
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
"default": "./dist/index.mjs"
|
|
34
|
-
},
|
|
35
|
-
"require": {
|
|
36
|
-
"types": "./dist/index.d.ts",
|
|
37
|
-
"default": "./dist/index.js"
|
|
38
|
-
},
|
|
39
|
-
"default": "./dist/index.js"
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.mjs"
|
|
40
32
|
}
|
|
41
33
|
},
|
|
42
34
|
"files": [
|
|
43
35
|
"dist"
|
|
44
36
|
],
|
|
45
37
|
"scripts": {
|
|
46
|
-
"build": "bunchee src/index.ts --dts --external react,react-dom &&
|
|
47
|
-
"build:css": "npx @tailwindcss/cli -i ./src/index.css -o ./dist/output.css",
|
|
38
|
+
"build": "bunchee src/index.ts --dts --external react,react-dom --format esm && sed -i '' '1i\\\n/// <reference lib=\"es2015.iterable\" />\n' dist/index.d.ts",
|
|
48
39
|
"clean": "rm -rf dist",
|
|
49
40
|
"prepare": "pnpm run build",
|
|
50
41
|
"prepublishOnly": "pnpm run build",
|
package/dist/index.cjs
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
-
|
|
3
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
-
var reactSlot = require('@radix-ui/react-slot');
|
|
5
|
-
var react = require('react');
|
|
6
|
-
|
|
7
|
-
const uid = ()=>Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
|
|
8
|
-
|
|
9
|
-
const UploaderCtx = /*#__PURE__*/ react.createContext(null);
|
|
10
|
-
const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "images", maxCount, disabled, children })=>{
|
|
11
|
-
const [items, setItems] = react.useState([]);
|
|
12
|
-
const controllers = react.useRef(new Map());
|
|
13
|
-
const removeControllers = react.useRef(new Map());
|
|
14
|
-
const inputRef = react.useRef(null);
|
|
15
|
-
const hasHydratedInitialRef = react.useRef(false);
|
|
16
|
-
// Hydrate initial items from the server and keep them marked as done
|
|
17
|
-
react.useEffect(()=>{
|
|
18
|
-
if (hasHydratedInitialRef.current) return;
|
|
19
|
-
const arr = initial ?? [];
|
|
20
|
-
if (!Array.isArray(arr) || arr.length === 0) return;
|
|
21
|
-
const mapped = arr.map((it)=>{
|
|
22
|
-
return {
|
|
23
|
-
uid: it.uid || it.id,
|
|
24
|
-
id: it.id,
|
|
25
|
-
name: it.name,
|
|
26
|
-
url: it.url,
|
|
27
|
-
status: "done"
|
|
28
|
-
};
|
|
29
|
-
});
|
|
30
|
-
// Only hydrate if the user hasn't already added/modified items locally
|
|
31
|
-
setItems((prev)=>prev.length === 0 ? mapped : prev);
|
|
32
|
-
hasHydratedInitialRef.current = true;
|
|
33
|
-
}, [
|
|
34
|
-
initial
|
|
35
|
-
]);
|
|
36
|
-
const hiddenInputValue = react.useMemo(()=>{
|
|
37
|
-
const done = items.filter((i)=>i.status === "done" && i.url);
|
|
38
|
-
return JSON.stringify(done.map(({ uid: _u, previewUrl: _p, file: _f, status: _s, progress: _pr, error: _e, ...rest })=>rest));
|
|
39
|
-
}, [
|
|
40
|
-
items
|
|
41
|
-
]);
|
|
42
|
-
const emitChange = react.useCallback((next)=>{
|
|
43
|
-
setItems((prev)=>{
|
|
44
|
-
const nextState = typeof next === "function" ? next(prev) : next;
|
|
45
|
-
if (onChange) Promise.resolve(onChange(nextState)).catch(()=>{});
|
|
46
|
-
return nextState;
|
|
47
|
-
});
|
|
48
|
-
}, [
|
|
49
|
-
onChange
|
|
50
|
-
]);
|
|
51
|
-
const startUpload = react.useCallback(async (item)=>{
|
|
52
|
-
if (!item.file) return;
|
|
53
|
-
const controller = new AbortController();
|
|
54
|
-
controllers.current.set(item.uid, controller);
|
|
55
|
-
const setProgress = (pct)=>{
|
|
56
|
-
emitChange((items)=>items.map((it)=>it.uid === item.uid ? {
|
|
57
|
-
...it,
|
|
58
|
-
progress: Math.max(0, Math.min(100, pct))
|
|
59
|
-
} : it));
|
|
60
|
-
};
|
|
61
|
-
emitChange((items)=>items.map((it)=>it.uid === item.uid ? {
|
|
62
|
-
...it,
|
|
63
|
-
status: "uploading",
|
|
64
|
-
error: undefined
|
|
65
|
-
} : it));
|
|
66
|
-
try {
|
|
67
|
-
const result = await upload(item.file, controller.signal, setProgress);
|
|
68
|
-
emitChange((items)=>items.map((it)=>{
|
|
69
|
-
if (it.uid !== item.uid) return it;
|
|
70
|
-
// Revoke the local objectURL preview to avoid memory leaks
|
|
71
|
-
if (it.previewUrl && it.previewUrl.startsWith("blob:")) {
|
|
72
|
-
try {
|
|
73
|
-
URL.revokeObjectURL(it.previewUrl);
|
|
74
|
-
} catch {
|
|
75
|
-
/*fail silently*/ }
|
|
76
|
-
}
|
|
77
|
-
const serverPreview = result?.preview || result.url;
|
|
78
|
-
return {
|
|
79
|
-
...it,
|
|
80
|
-
status: "done",
|
|
81
|
-
url: result.url,
|
|
82
|
-
id: result.id,
|
|
83
|
-
previewUrl: serverPreview,
|
|
84
|
-
progress: 100
|
|
85
|
-
};
|
|
86
|
-
}));
|
|
87
|
-
} catch (err) {
|
|
88
|
-
const wasAborted = controller.signal.aborted;
|
|
89
|
-
emitChange((items)=>items.map((it)=>it.uid === item.uid ? {
|
|
90
|
-
...it,
|
|
91
|
-
status: wasAborted ? "canceled" : "error",
|
|
92
|
-
error: wasAborted ? undefined : err?.message || "Upload failed"
|
|
93
|
-
} : it));
|
|
94
|
-
} finally{
|
|
95
|
-
controllers.current.delete(item.uid);
|
|
96
|
-
}
|
|
97
|
-
}, [
|
|
98
|
-
emitChange,
|
|
99
|
-
upload
|
|
100
|
-
]);
|
|
101
|
-
const selectFiles = react.useCallback((files)=>{
|
|
102
|
-
if (!files || files.length === 0) return;
|
|
103
|
-
const selected = Array.from(files);
|
|
104
|
-
const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
|
|
105
|
-
const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
|
|
106
|
-
const newItems = toUse.map((file)=>({
|
|
107
|
-
uid: uid(),
|
|
108
|
-
name: file.name,
|
|
109
|
-
file,
|
|
110
|
-
previewUrl: URL.createObjectURL(file),
|
|
111
|
-
status: "idle",
|
|
112
|
-
progress: 0
|
|
113
|
-
}));
|
|
114
|
-
emitChange([
|
|
115
|
-
...items,
|
|
116
|
-
...newItems
|
|
117
|
-
]);
|
|
118
|
-
newItems.forEach((it)=>startUpload(it));
|
|
119
|
-
}, [
|
|
120
|
-
emitChange,
|
|
121
|
-
items,
|
|
122
|
-
maxCount,
|
|
123
|
-
startUpload
|
|
124
|
-
]);
|
|
125
|
-
const onInputChange = react.useCallback((e)=>{
|
|
126
|
-
selectFiles(e.target.files);
|
|
127
|
-
e.currentTarget.value = "";
|
|
128
|
-
}, [
|
|
129
|
-
selectFiles
|
|
130
|
-
]);
|
|
131
|
-
const onDrop = react.useCallback((e)=>{
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
if (disabled) return;
|
|
134
|
-
selectFiles(e.dataTransfer.files);
|
|
135
|
-
}, [
|
|
136
|
-
disabled,
|
|
137
|
-
selectFiles
|
|
138
|
-
]);
|
|
139
|
-
const onDragOver = react.useCallback((e)=>e.preventDefault(), []);
|
|
140
|
-
const actions = react.useMemo(()=>({
|
|
141
|
-
cancel: (uidStr)=>{
|
|
142
|
-
const ctrl = controllers.current.get(uidStr);
|
|
143
|
-
ctrl?.abort();
|
|
144
|
-
},
|
|
145
|
-
remove: async (uidStr)=>{
|
|
146
|
-
const item = items.find((i)=>i.uid === uidStr);
|
|
147
|
-
if (!item) return;
|
|
148
|
-
// abort any in-flight upload first
|
|
149
|
-
controllers.current.get(uidStr)?.abort();
|
|
150
|
-
// If no server-side removal needed or not uploaded yet, just remove
|
|
151
|
-
if (!onRemove || item.status !== "done") {
|
|
152
|
-
emitChange((list)=>list.filter((i)=>i.uid !== uidStr));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const ctrl = new AbortController();
|
|
156
|
-
removeControllers.current.set(uidStr, ctrl);
|
|
157
|
-
if (removeMode === "optimistic") {
|
|
158
|
-
const prev = items;
|
|
159
|
-
// remove from UI immediately
|
|
160
|
-
emitChange((list)=>list.filter((i)=>i.uid !== uidStr));
|
|
161
|
-
try {
|
|
162
|
-
await onRemove(item, ctrl.signal);
|
|
163
|
-
} catch {
|
|
164
|
-
// rollback UI if server delete fails
|
|
165
|
-
emitChange(prev);
|
|
166
|
-
} finally{
|
|
167
|
-
removeControllers.current.delete(uidStr);
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
// strict: mark as removing, wait for API, then remove
|
|
171
|
-
emitChange((list)=>list.map((it)=>it.uid === uidStr ? {
|
|
172
|
-
...it,
|
|
173
|
-
status: "removing"
|
|
174
|
-
} : it));
|
|
175
|
-
try {
|
|
176
|
-
await onRemove(item, ctrl.signal);
|
|
177
|
-
emitChange((list)=>list.filter((i)=>i.uid !== uidStr));
|
|
178
|
-
} catch {
|
|
179
|
-
// revert to done if delete fails
|
|
180
|
-
emitChange((list)=>list.map((it)=>it.uid === uidStr ? {
|
|
181
|
-
...it,
|
|
182
|
-
status: "done"
|
|
183
|
-
} : it));
|
|
184
|
-
} finally{
|
|
185
|
-
removeControllers.current.delete(uidStr);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
retry: (uidStr)=>{
|
|
190
|
-
const item = items.find((i)=>i.uid === uidStr);
|
|
191
|
-
if (!item) return;
|
|
192
|
-
if (item.file) {
|
|
193
|
-
void startUpload({
|
|
194
|
-
...item,
|
|
195
|
-
status: "idle",
|
|
196
|
-
error: undefined,
|
|
197
|
-
progress: 0
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}), [
|
|
202
|
-
emitChange,
|
|
203
|
-
items,
|
|
204
|
-
onRemove,
|
|
205
|
-
removeMode,
|
|
206
|
-
startUpload
|
|
207
|
-
]);
|
|
208
|
-
react.useEffect(()=>()=>{
|
|
209
|
-
items.forEach((i)=>i.previewUrl && URL.revokeObjectURL(i.previewUrl));
|
|
210
|
-
controllers.current.forEach((c)=>c.abort());
|
|
211
|
-
}, []);
|
|
212
|
-
const ctx = {
|
|
213
|
-
items,
|
|
214
|
-
disabled,
|
|
215
|
-
multiple,
|
|
216
|
-
accept,
|
|
217
|
-
actions,
|
|
218
|
-
openFileDialog: ()=>inputRef.current?.click(),
|
|
219
|
-
fileInputProps: {
|
|
220
|
-
ref: inputRef,
|
|
221
|
-
onChange: onInputChange,
|
|
222
|
-
accept,
|
|
223
|
-
multiple,
|
|
224
|
-
disabled
|
|
225
|
-
},
|
|
226
|
-
getDropzoneProps: ()=>({
|
|
227
|
-
role: "button",
|
|
228
|
-
tabIndex: 0,
|
|
229
|
-
onDrop,
|
|
230
|
-
onDragOver,
|
|
231
|
-
onKeyDown: (e)=>{
|
|
232
|
-
if (disabled) return;
|
|
233
|
-
if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
|
|
234
|
-
},
|
|
235
|
-
"data-disabled": disabled ? "" : undefined,
|
|
236
|
-
"data-multiple": multiple ? "" : undefined
|
|
237
|
-
}),
|
|
238
|
-
hiddenInputValue,
|
|
239
|
-
name
|
|
240
|
-
};
|
|
241
|
-
return /*#__PURE__*/ jsxRuntime.jsx(UploaderCtx.Provider, {
|
|
242
|
-
value: ctx,
|
|
243
|
-
children: /*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
244
|
-
"data-part": "root",
|
|
245
|
-
children: [
|
|
246
|
-
/*#__PURE__*/ jsxRuntime.jsx("input", {
|
|
247
|
-
type: "file",
|
|
248
|
-
hidden: true,
|
|
249
|
-
...ctx.fileInputProps
|
|
250
|
-
}),
|
|
251
|
-
children
|
|
252
|
-
]
|
|
253
|
-
})
|
|
254
|
-
});
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const useUplofile = ()=>{
|
|
258
|
-
const ctx = react.useContext(UploaderCtx);
|
|
259
|
-
if (!ctx) throw new Error("useUplofile hook must be used within <Uplofile.Root>");
|
|
260
|
-
return ctx;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const Dropzone = ({ asChild, ...rest })=>{
|
|
264
|
-
const { getDropzoneProps } = useUplofile();
|
|
265
|
-
const Comp = asChild ? reactSlot.Slot : "div";
|
|
266
|
-
return /*#__PURE__*/ jsxRuntime.jsx(Comp, {
|
|
267
|
-
"data-part": "dropzone",
|
|
268
|
-
...getDropzoneProps(),
|
|
269
|
-
...rest
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const Preview = ({ render })=>{
|
|
274
|
-
const { items, actions } = useUplofile();
|
|
275
|
-
if (render && typeof render === "function") return render({
|
|
276
|
-
items,
|
|
277
|
-
actions
|
|
278
|
-
});
|
|
279
|
-
return /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
280
|
-
"data-part": "preview",
|
|
281
|
-
className: "uplofile-preview",
|
|
282
|
-
children: /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
283
|
-
className: "uplofile-preview__wrapper grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4",
|
|
284
|
-
children: items.map((item)=>/*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
285
|
-
onClick: (e)=>e.stopPropagation(),
|
|
286
|
-
className: "uplofile-preview__item relative overflow-hidden rounded-xl border size-32",
|
|
287
|
-
"data-state": item.status,
|
|
288
|
-
children: [
|
|
289
|
-
item.url || item.previewUrl ? /*#__PURE__*/ jsxRuntime.jsx("img", {
|
|
290
|
-
src: item.url || item.previewUrl,
|
|
291
|
-
alt: item.name,
|
|
292
|
-
className: "uplofile-preview__image size-full object-cover"
|
|
293
|
-
}) : /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
294
|
-
className: "uplofile-preview__no-preview flex h-32 w-full items-center justify-center text-xs text-gray-500",
|
|
295
|
-
children: "No preview"
|
|
296
|
-
}),
|
|
297
|
-
item.status === "uploading" && /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
298
|
-
className: "uplofile-preview__progress absolute bottom-0 left-0 right-0 h-1 bg-gray-200",
|
|
299
|
-
children: /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
300
|
-
className: "uplofile-preview__progress-bar h-full bg-black/80",
|
|
301
|
-
style: {
|
|
302
|
-
width: `${Math.max(0, Math.min(100, item.progress ?? 0))}%`
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
}),
|
|
306
|
-
/*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
307
|
-
className: "uplofile-preview__actions absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-black/60 to-transparent p-2",
|
|
308
|
-
children: [
|
|
309
|
-
item.status === "uploading" && /*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
310
|
-
type: "button",
|
|
311
|
-
className: "uplofile-preview__button uplofile-preview__button--cancel rounded-xl bg-black/50 px-2 py-1 text-xs text-white",
|
|
312
|
-
onClick: ()=>actions.cancel(item.uid),
|
|
313
|
-
children: "Cancel"
|
|
314
|
-
}),
|
|
315
|
-
(item.status === "error" || item.status === "canceled") && /*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
316
|
-
type: "button",
|
|
317
|
-
className: "uplofile-preview__button uplofile-preview__button--retry rounded-xl bg-black/50 px-2 py-1 text-xs text-white",
|
|
318
|
-
onClick: ()=>actions.retry(item.uid),
|
|
319
|
-
children: "Retry"
|
|
320
|
-
}),
|
|
321
|
-
/*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
322
|
-
type: "button",
|
|
323
|
-
className: "uplofile-preview__button uplofile-preview__button--remove rounded-xl bg-black/50 px-2 py-1 text-xs text-white",
|
|
324
|
-
onClick: ()=>actions.remove(item.uid),
|
|
325
|
-
disabled: item.status === "removing",
|
|
326
|
-
children: item.status === "removing" ? "Removing..." : "Remove"
|
|
327
|
-
})
|
|
328
|
-
]
|
|
329
|
-
})
|
|
330
|
-
]
|
|
331
|
-
}, item.uid))
|
|
332
|
-
})
|
|
333
|
-
});
|
|
334
|
-
};
|
|
335
|
-
const HiddenInput = ({ name })=>{
|
|
336
|
-
const { hiddenInputValue, name: defaultName } = useUplofile();
|
|
337
|
-
return /*#__PURE__*/ jsxRuntime.jsx("input", {
|
|
338
|
-
type: "hidden",
|
|
339
|
-
name: name ?? defaultName,
|
|
340
|
-
value: hiddenInputValue
|
|
341
|
-
});
|
|
342
|
-
};
|
|
343
|
-
const Cancel = ({ uid, asChild, alwaysVisible = false, ...rest })=>{
|
|
344
|
-
const { actions, items } = useUplofile();
|
|
345
|
-
const isUploading = items.find((i)=>i.uid === uid)?.status === "uploading";
|
|
346
|
-
const Comp = asChild ? reactSlot.Slot : "button";
|
|
347
|
-
if (!isUploading && !alwaysVisible) return null;
|
|
348
|
-
return /*#__PURE__*/ jsxRuntime.jsx(Comp, {
|
|
349
|
-
onClick: (e)=>{
|
|
350
|
-
e.stopPropagation();
|
|
351
|
-
actions.cancel(uid);
|
|
352
|
-
},
|
|
353
|
-
...rest
|
|
354
|
-
});
|
|
355
|
-
};
|
|
356
|
-
const Retry = ({ uid, asChild, ...rest })=>{
|
|
357
|
-
const { actions } = useUplofile();
|
|
358
|
-
const Comp = asChild ? reactSlot.Slot : "button";
|
|
359
|
-
return /*#__PURE__*/ jsxRuntime.jsx(Comp, {
|
|
360
|
-
onClick: (e)=>{
|
|
361
|
-
e.stopPropagation();
|
|
362
|
-
actions.retry(uid);
|
|
363
|
-
},
|
|
364
|
-
...rest
|
|
365
|
-
});
|
|
366
|
-
};
|
|
367
|
-
const Remove = ({ uid, asChild, ...rest })=>{
|
|
368
|
-
const { actions } = useUplofile();
|
|
369
|
-
const Comp = asChild ? reactSlot.Slot : "button";
|
|
370
|
-
return /*#__PURE__*/ jsxRuntime.jsx(Comp, {
|
|
371
|
-
onClick: (e)=>{
|
|
372
|
-
e.stopPropagation();
|
|
373
|
-
actions.remove(uid);
|
|
374
|
-
},
|
|
375
|
-
...rest
|
|
376
|
-
});
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
380
|
-
const { openFileDialog, disabled, items } = useUplofile();
|
|
381
|
-
const Comp = asChild ? reactSlot.Slot : "button";
|
|
382
|
-
const uploading = items.filter((i)=>i.status === "uploading");
|
|
383
|
-
const uploadingCount = uploading.length;
|
|
384
|
-
const doneCount = items.filter((i)=>i.status === "done").length;
|
|
385
|
-
const errorCount = items.filter((i)=>i.status === "error").length;
|
|
386
|
-
const totalProgress = uploadingCount ? Math.round(uploading.reduce((acc, it)=>acc + (typeof it.progress === "number" ? it.progress : 0), 0) / uploadingCount) : undefined;
|
|
387
|
-
const api = {
|
|
388
|
-
items,
|
|
389
|
-
isUploading: uploadingCount > 0,
|
|
390
|
-
uploadingCount,
|
|
391
|
-
doneCount,
|
|
392
|
-
errorCount,
|
|
393
|
-
totalProgress,
|
|
394
|
-
open: openFileDialog
|
|
395
|
-
};
|
|
396
|
-
return /*#__PURE__*/ jsxRuntime.jsx(Comp, {
|
|
397
|
-
type: asChild ? undefined : "button",
|
|
398
|
-
"aria-disabled": disabled,
|
|
399
|
-
"data-part": "trigger",
|
|
400
|
-
onClick: (e)=>{
|
|
401
|
-
if (disabled) return;
|
|
402
|
-
rest.onClick?.(e);
|
|
403
|
-
openFileDialog();
|
|
404
|
-
},
|
|
405
|
-
...rest,
|
|
406
|
-
children: render ? render(api) : children
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
exports.Cancel = Cancel;
|
|
411
|
-
exports.Dropzone = Dropzone;
|
|
412
|
-
exports.HiddenInput = HiddenInput;
|
|
413
|
-
exports.Preview = Preview;
|
|
414
|
-
exports.Remove = Remove;
|
|
415
|
-
exports.Retry = Retry;
|
|
416
|
-
exports.Root = Root;
|
|
417
|
-
exports.Trigger = Trigger;
|
|
418
|
-
exports.useUplofile = useUplofile;
|
package/dist/index.d.mts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import React, { HTMLAttributes, PropsWithChildren, RefObject, ChangeEvent, DragEvent, ButtonHTMLAttributes } from 'react';
|
|
3
|
-
|
|
4
|
-
declare const Dropzone: ({ asChild, ...rest }: {
|
|
5
|
-
asChild?: boolean;
|
|
6
|
-
} & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
|
|
7
|
-
|
|
8
|
-
type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
|
|
9
|
-
type UploadFileItem = {
|
|
10
|
-
uid: string;
|
|
11
|
-
id?: string;
|
|
12
|
-
name: string;
|
|
13
|
-
url?: string;
|
|
14
|
-
previewUrl?: string;
|
|
15
|
-
file?: File;
|
|
16
|
-
status: UploadStatus;
|
|
17
|
-
progress?: number;
|
|
18
|
-
error?: string;
|
|
19
|
-
data?: any;
|
|
20
|
-
};
|
|
21
|
-
type UploadResult = {
|
|
22
|
-
url: string;
|
|
23
|
-
id?: string;
|
|
24
|
-
};
|
|
25
|
-
type RootProps = PropsWithChildren<{
|
|
26
|
-
multiple?: boolean;
|
|
27
|
-
initial?: Array<Pick<UploadFileItem, "uid" | "id" | "name" | "url">>;
|
|
28
|
-
/**
|
|
29
|
-
* optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
|
|
30
|
-
* strict: call onRemove first; only remove from UI if it succeeds.
|
|
31
|
-
*/
|
|
32
|
-
removeMode?: "optimistic" | "strict";
|
|
33
|
-
name?: string;
|
|
34
|
-
maxCount?: number;
|
|
35
|
-
disabled?: boolean;
|
|
36
|
-
accept?: string;
|
|
37
|
-
onChange?: (items: UploadFileItem[]) => Promise<void> | void;
|
|
38
|
-
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
|
|
39
|
-
onRemove?: (item: UploadFileItem, signal: AbortSignal) => Promise<void | any>;
|
|
40
|
-
}>;
|
|
41
|
-
type ItemActions = {
|
|
42
|
-
cancel: (uid: string) => void;
|
|
43
|
-
remove: (uid: string) => void;
|
|
44
|
-
retry: (uid: string) => void;
|
|
45
|
-
};
|
|
46
|
-
type ImageUploaderContextValue = {
|
|
47
|
-
items: UploadFileItem[];
|
|
48
|
-
disabled?: boolean;
|
|
49
|
-
multiple: boolean;
|
|
50
|
-
accept: string;
|
|
51
|
-
actions: ItemActions;
|
|
52
|
-
openFileDialog: () => void;
|
|
53
|
-
fileInputProps: {
|
|
54
|
-
ref: RefObject<HTMLInputElement>;
|
|
55
|
-
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
56
|
-
accept: string;
|
|
57
|
-
multiple: boolean;
|
|
58
|
-
disabled?: boolean;
|
|
59
|
-
};
|
|
60
|
-
getDropzoneProps: () => {
|
|
61
|
-
role: string;
|
|
62
|
-
tabIndex: number;
|
|
63
|
-
onDrop: (e: DragEvent) => void;
|
|
64
|
-
onDragOver: (e: DragEvent) => void;
|
|
65
|
-
onKeyDown: (e: KeyboardEvent) => void;
|
|
66
|
-
"data-disabled"?: string;
|
|
67
|
-
"data-multiple"?: string;
|
|
68
|
-
};
|
|
69
|
-
hiddenInputValue: string;
|
|
70
|
-
name: string;
|
|
71
|
-
};
|
|
72
|
-
type TriggerRenderProps = {
|
|
73
|
-
items: UploadFileItem[];
|
|
74
|
-
isUploading: boolean;
|
|
75
|
-
uploadingCount: number;
|
|
76
|
-
doneCount: number;
|
|
77
|
-
errorCount: number;
|
|
78
|
-
totalProgress?: number;
|
|
79
|
-
open: () => void;
|
|
80
|
-
};
|
|
81
|
-
type PreviewRenderProps = {
|
|
82
|
-
items: UploadFileItem[];
|
|
83
|
-
actions: ItemActions;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
type Props = {
|
|
87
|
-
render?: (api: PreviewRenderProps) => React.ReactNode;
|
|
88
|
-
};
|
|
89
|
-
declare const Preview: ({ render }: Props) => string | number | boolean | Iterable<React.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
90
|
-
declare const HiddenInput: ({ name }: {
|
|
91
|
-
name?: string;
|
|
92
|
-
}) => react_jsx_runtime.JSX.Element;
|
|
93
|
-
type ButtonProps = {
|
|
94
|
-
uid: string;
|
|
95
|
-
alwaysVisible?: boolean;
|
|
96
|
-
asChild?: boolean;
|
|
97
|
-
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
|
98
|
-
declare const Cancel: ({ uid, asChild, alwaysVisible, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element | null;
|
|
99
|
-
declare const Retry: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
100
|
-
declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
101
|
-
|
|
102
|
-
declare const Root: ({ multiple, initial, onChange, upload, removeMode, onRemove, accept, name, maxCount, disabled, children, }: RootProps) => react_jsx_runtime.JSX.Element;
|
|
103
|
-
|
|
104
|
-
declare const Trigger: ({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
105
|
-
asChild?: boolean;
|
|
106
|
-
render?: (api: TriggerRenderProps) => React.ReactNode;
|
|
107
|
-
children?: React.ReactNode | ((api: TriggerRenderProps) => React.ReactNode);
|
|
108
|
-
} & React.HTMLAttributes<HTMLElement>>) => react_jsx_runtime.JSX.Element;
|
|
109
|
-
|
|
110
|
-
declare const useUplofile: () => ImageUploaderContextValue;
|
|
111
|
-
|
|
112
|
-
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, useUplofile };
|
|
113
|
-
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus };
|
|
114
|
-
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sources":["../src/components/dropzone.tsx","../src/types.ts","../src/components/preview.tsx","../src/context.tsx","../src/components/trigger.tsx","../src/hook.ts"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { HTMLAttributes } from \"react\";\n\nimport { useUplofile } from \"../hook\";\n\nexport const Dropzone = ({\n asChild,\n ...rest\n}: { asChild?: boolean } & HTMLAttributes<HTMLElement>) => {\n const { getDropzoneProps } = useUplofile();\n const Comp: any = asChild ? Slot : \"div\";\n return <Comp data-part=\"dropzone\" {...getDropzoneProps()} {...rest} />;\n};\n","import type {\n ChangeEvent,\n DragEvent,\n PropsWithChildren,\n RefObject,\n} from \"react\";\n\nexport type UploadStatus =\n | \"idle\"\n | \"uploading\"\n | \"done\"\n | \"error\"\n | \"canceled\"\n | \"removing\";\n\nexport type UploadFileItem = {\n uid: string;\n id?: string;\n name: string;\n url?: string;\n previewUrl?: string;\n file?: File;\n status: UploadStatus;\n progress?: number;\n error?: string;\n data?: any;\n};\n\nexport type UploadResult = { url: string; id?: string };\n\nexport type RootProps = PropsWithChildren<{\n multiple?: boolean;\n initial?: Array<Pick<UploadFileItem, \"uid\" | \"id\" | \"name\" | \"url\">>;\n /**\n * optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.\n * strict: call onRemove first; only remove from UI if it succeeds.\n */\n removeMode?: \"optimistic\" | \"strict\";\n name?: string;\n maxCount?: number;\n disabled?: boolean;\n accept?: string;\n onChange?: (items: UploadFileItem[]) => Promise<void> | void;\n upload: (\n file: File,\n signal: AbortSignal,\n setProgress?: (pct: number) => void,\n ) => Promise<UploadResult>;\n onRemove?: (item: UploadFileItem, signal: AbortSignal) => Promise<void | any>;\n}>;\n\nexport type ItemActions = {\n cancel: (uid: string) => void;\n remove: (uid: string) => void;\n retry: (uid: string) => void;\n};\n\nexport type ImageUploaderContextValue = {\n items: UploadFileItem[];\n disabled?: boolean;\n multiple: boolean;\n accept: string;\n actions: ItemActions;\n openFileDialog: () => void;\n fileInputProps: {\n ref: RefObject<HTMLInputElement>;\n onChange: (e: ChangeEvent<HTMLInputElement>) => void;\n accept: string;\n multiple: boolean;\n disabled?: boolean;\n };\n getDropzoneProps: () => {\n role: string;\n tabIndex: number;\n onDrop: (e: DragEvent) => void;\n onDragOver: (e: DragEvent) => void;\n onKeyDown: (e: KeyboardEvent) => void;\n \"data-disabled\"?: string;\n \"data-multiple\"?: string;\n };\n hiddenInputValue: string;\n name: string;\n};\n\nexport type TriggerRenderProps = {\n items: UploadFileItem[];\n isUploading: boolean;\n uploadingCount: number;\n doneCount: number;\n errorCount: number;\n totalProgress?: number;\n open: () => void;\n};\n\nexport type PreviewRenderProps = {\n items: UploadFileItem[];\n actions: ItemActions;\n};\n","import { Slot } from \"@radix-ui/react-slot\";\nimport React, { ButtonHTMLAttributes } from \"react\";\n\nimport { useUplofile } from \"../hook\";\n\nimport type { PreviewRenderProps } from \"../types\";\n\ntype Props = {\n render?: (api: PreviewRenderProps) => React.ReactNode;\n};\n\nexport const Preview = ({ render }: Props) => {\n const { items, actions } = useUplofile();\n\n if (render && typeof render === \"function\") return render({ items, actions });\n\n return (\n <div data-part=\"preview\" className={\"uplofile-preview\"}>\n <div className=\"uplofile-preview__wrapper grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4\">\n {items.map((item) => (\n <div\n key={item.uid}\n onClick={(e) => e.stopPropagation()}\n className=\"uplofile-preview__item relative overflow-hidden rounded-xl border size-32\"\n data-state={item.status}\n >\n {item.url || item.previewUrl ? (\n <img\n src={item.url || item.previewUrl}\n alt={item.name}\n className=\"uplofile-preview__image size-full object-cover\"\n />\n ) : (\n <div className=\"uplofile-preview__no-preview flex h-32 w-full items-center justify-center text-xs text-gray-500\">\n No preview\n </div>\n )}\n {item.status === \"uploading\" && (\n <div className=\"uplofile-preview__progress absolute bottom-0 left-0 right-0 h-1 bg-gray-200\">\n <div\n className=\"uplofile-preview__progress-bar h-full bg-black/80\"\n style={{\n width: `${Math.max(0, Math.min(100, item.progress ?? 0))}%`,\n }}\n />\n </div>\n )}\n <div className=\"uplofile-preview__actions absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-black/60 to-transparent p-2\">\n {item.status === \"uploading\" && (\n <button\n type=\"button\"\n className=\"uplofile-preview__button uplofile-preview__button--cancel rounded-xl bg-black/50 px-2 py-1 text-xs text-white\"\n onClick={() => actions.cancel(item.uid)}\n >\n Cancel\n </button>\n )}\n {(item.status === \"error\" || item.status === \"canceled\") && (\n <button\n type=\"button\"\n className=\"uplofile-preview__button uplofile-preview__button--retry rounded-xl bg-black/50 px-2 py-1 text-xs text-white\"\n onClick={() => actions.retry(item.uid)}\n >\n Retry\n </button>\n )}\n <button\n type=\"button\"\n className=\"uplofile-preview__button uplofile-preview__button--remove rounded-xl bg-black/50 px-2 py-1 text-xs text-white\"\n onClick={() => actions.remove(item.uid)}\n disabled={item.status === \"removing\"}\n >\n {item.status === \"removing\" ? \"Removing...\" : \"Remove\"}\n </button>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport const HiddenInput = ({ name }: { name?: string }) => {\n const { hiddenInputValue, name: defaultName } = useUplofile();\n return (\n <input type=\"hidden\" name={name ?? defaultName} value={hiddenInputValue} />\n );\n};\n\ntype ButtonProps = {\n uid: string;\n alwaysVisible?: boolean;\n asChild?: boolean;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\nexport const Cancel = ({\n uid,\n asChild,\n alwaysVisible = false,\n ...rest\n}: ButtonProps) => {\n const { actions, items } = useUplofile();\n const isUploading = items.find((i) => i.uid === uid)?.status === \"uploading\";\n const Comp: any = asChild ? Slot : \"button\";\n\n if (!isUploading && !alwaysVisible) return null;\n\n return (\n <Comp\n onClick={(e: { stopPropagation: () => void }) => {\n e.stopPropagation();\n actions.cancel(uid);\n }}\n {...rest}\n />\n );\n};\n\nexport const Retry = ({ uid, asChild, ...rest }: ButtonProps) => {\n const { actions } = useUplofile();\n const Comp: any = asChild ? Slot : \"button\";\n return (\n <Comp\n onClick={(e: { stopPropagation: () => void }) => {\n e.stopPropagation();\n actions.retry(uid);\n }}\n {...rest}\n />\n );\n};\n\nexport const Remove = ({ uid, asChild, ...rest }: ButtonProps) => {\n const { actions } = useUplofile();\n const Comp: any = asChild ? Slot : \"button\";\n return (\n <Comp\n onClick={(e: { stopPropagation: () => void }) => {\n e.stopPropagation();\n actions.remove(uid);\n }}\n {...rest}\n />\n );\n};\n","import type { DragEvent, RefObject } from \"react\";\nimport React, {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type {\n ImageUploaderContextValue,\n ItemActions,\n RootProps,\n UploadFileItem,\n} from \"./types\";\nimport { uid } from \"./utils\";\n\nexport const UploaderCtx = createContext<ImageUploaderContextValue | null>(\n null,\n);\n\nexport const Root = ({\n multiple = true,\n initial = [],\n onChange,\n upload,\n removeMode = \"optimistic\",\n onRemove,\n accept = \"image/*\",\n name = \"images\",\n maxCount,\n disabled,\n children,\n}: RootProps) => {\n const [items, setItems] = useState<UploadFileItem[]>([]);\n const controllers = useRef(new Map<string, AbortController>());\n const removeControllers = useRef(new Map<string, AbortController>());\n const inputRef = useRef<HTMLInputElement | null>(null);\n const hasHydratedInitialRef = useRef(false);\n\n // Hydrate initial items from the server and keep them marked as done\n useEffect(() => {\n if (hasHydratedInitialRef.current) return;\n const arr = initial ?? [];\n if (!Array.isArray(arr) || arr.length === 0) return;\n\n const mapped: UploadFileItem[] = arr.map((it) => {\n return {\n uid: it.uid || it.id,\n id: it.id,\n name: it.name,\n url: it.url,\n status: \"done\",\n } as UploadFileItem;\n });\n\n // Only hydrate if the user hasn't already added/modified items locally\n setItems((prev) => (prev.length === 0 ? mapped : prev));\n hasHydratedInitialRef.current = true;\n }, [initial]);\n\n const hiddenInputValue = useMemo(() => {\n const done = items.filter((i) => i.status === \"done\" && i.url);\n return JSON.stringify(\n done.map(\n ({\n uid: _u,\n previewUrl: _p,\n file: _f,\n status: _s,\n progress: _pr,\n error: _e,\n ...rest\n }) => rest,\n ),\n );\n }, [items]);\n\n const emitChange = useCallback(\n (\n next: UploadFileItem[] | ((prev: UploadFileItem[]) => UploadFileItem[]),\n ) => {\n setItems((prev) => {\n const nextState =\n typeof next === \"function\" ? (next as any)(prev) : next;\n if (onChange) Promise.resolve(onChange(nextState)).catch(() => {});\n return nextState;\n });\n },\n [onChange],\n );\n\n const startUpload = useCallback(\n async (item: UploadFileItem) => {\n if (!item.file) return;\n const controller = new AbortController();\n controllers.current.set(item.uid, controller);\n\n const setProgress = (pct: number) => {\n emitChange((items) =>\n items.map((it) =>\n it.uid === item.uid\n ? { ...it, progress: Math.max(0, Math.min(100, pct)) }\n : it,\n ),\n );\n };\n\n emitChange((items) =>\n items.map((it) =>\n it.uid === item.uid\n ? { ...it, status: \"uploading\", error: undefined }\n : it,\n ),\n );\n\n try {\n const result = await upload(item.file, controller.signal, setProgress);\n\n emitChange((items) =>\n items.map((it) => {\n if (it.uid !== item.uid) return it;\n // Revoke the local objectURL preview to avoid memory leaks\n if (it.previewUrl && it.previewUrl.startsWith(\"blob:\")) {\n try {\n URL.revokeObjectURL(it.previewUrl);\n } catch {\n /*fail silently*/\n }\n }\n const serverPreview = (result as any)?.preview || result.url;\n return {\n ...it,\n status: \"done\",\n url: result.url,\n id: result.id,\n previewUrl: serverPreview,\n progress: 100,\n };\n }),\n );\n } catch (err: any) {\n const wasAborted = controller.signal.aborted;\n emitChange((items) =>\n items.map((it) =>\n it.uid === item.uid\n ? {\n ...it,\n status: wasAborted ? \"canceled\" : \"error\",\n error: wasAborted\n ? undefined\n : err?.message || \"Upload failed\",\n }\n : it,\n ),\n );\n } finally {\n controllers.current.delete(item.uid);\n }\n },\n [emitChange, upload],\n );\n\n const selectFiles = useCallback(\n (files: FileList | null) => {\n if (!files || files.length === 0) return;\n const selected = Array.from(files);\n const remaining = maxCount\n ? Math.max(\n 0,\n maxCount - items.filter((i) => i.status !== \"canceled\").length,\n )\n : undefined;\n const toUse =\n typeof remaining === \"number\" ? selected.slice(0, remaining) : selected;\n\n const newItems: UploadFileItem[] = toUse.map((file) => ({\n uid: uid(),\n name: file.name,\n file,\n previewUrl: URL.createObjectURL(file),\n status: \"idle\",\n progress: 0,\n }));\n\n emitChange([...items, ...newItems]);\n newItems.forEach((it) => startUpload(it));\n },\n [emitChange, items, maxCount, startUpload],\n );\n\n const onInputChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n selectFiles(e.target.files);\n e.currentTarget.value = \"\";\n },\n [selectFiles],\n );\n\n const onDrop = useCallback(\n (e: DragEvent) => {\n e.preventDefault();\n if (disabled) return;\n selectFiles(e.dataTransfer.files);\n },\n [disabled, selectFiles],\n );\n\n const onDragOver = useCallback((e: DragEvent) => e.preventDefault(), []);\n\n const actions: ItemActions = useMemo(\n () => ({\n cancel: (uidStr: string) => {\n const ctrl = controllers.current.get(uidStr);\n ctrl?.abort();\n },\n remove: async (uidStr: string) => {\n const item = items.find((i) => i.uid === uidStr);\n if (!item) return;\n\n // abort any in-flight upload first\n controllers.current.get(uidStr)?.abort();\n\n // If no server-side removal needed or not uploaded yet, just remove\n if (!onRemove || item.status !== \"done\") {\n emitChange((list) => list.filter((i) => i.uid !== uidStr));\n return;\n }\n\n const ctrl = new AbortController();\n removeControllers.current.set(uidStr, ctrl);\n\n if (removeMode === \"optimistic\") {\n const prev = items;\n // remove from UI immediately\n emitChange((list) => list.filter((i) => i.uid !== uidStr));\n try {\n await onRemove(item, ctrl.signal);\n } catch {\n // rollback UI if server delete fails\n emitChange(prev);\n } finally {\n removeControllers.current.delete(uidStr);\n }\n } else {\n // strict: mark as removing, wait for API, then remove\n emitChange((list) =>\n list.map((it) =>\n it.uid === uidStr ? { ...it, status: \"removing\" as const } : it,\n ),\n );\n try {\n await onRemove(item, ctrl.signal);\n emitChange((list) => list.filter((i) => i.uid !== uidStr));\n } catch {\n // revert to done if delete fails\n emitChange((list) =>\n list.map((it) =>\n it.uid === uidStr ? { ...it, status: \"done\" as const } : it,\n ),\n );\n } finally {\n removeControllers.current.delete(uidStr);\n }\n }\n },\n retry: (uidStr: string) => {\n const item = items.find((i) => i.uid === uidStr);\n if (!item) return;\n if (item.file) {\n void startUpload({\n ...item,\n status: \"idle\",\n error: undefined,\n progress: 0,\n });\n }\n },\n }),\n [emitChange, items, onRemove, removeMode, startUpload],\n );\n\n useEffect(\n () => () => {\n items.forEach((i) => i.previewUrl && URL.revokeObjectURL(i.previewUrl));\n controllers.current.forEach((c) => c.abort());\n },\n [],\n );\n\n const ctx: ImageUploaderContextValue = {\n items,\n disabled,\n multiple,\n accept,\n actions,\n openFileDialog: () => inputRef.current?.click(),\n fileInputProps: {\n ref: inputRef as RefObject<HTMLInputElement>,\n onChange: onInputChange,\n accept,\n multiple,\n disabled,\n },\n getDropzoneProps: () => ({\n role: \"button\",\n tabIndex: 0,\n onDrop,\n onDragOver,\n onKeyDown: (e) => {\n if (disabled) return;\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n },\n \"data-disabled\": disabled ? \"\" : undefined,\n \"data-multiple\": multiple ? \"\" : undefined,\n }),\n hiddenInputValue,\n name,\n };\n\n return (\n <UploaderCtx.Provider value={ctx}>\n <div data-part=\"root\">\n <input type=\"file\" hidden {...ctx.fileInputProps} />\n {children}\n </div>\n </UploaderCtx.Provider>\n );\n};\n","import { Slot } from \"@radix-ui/react-slot\";\nimport React, { PropsWithChildren } from \"react\";\n\nimport { useUplofile } from \"../hook\";\nimport type { TriggerRenderProps } from \"../types\";\n\nexport const Trigger = ({\n asChild,\n children,\n render,\n ...rest\n}: PropsWithChildren<\n {\n asChild?: boolean;\n render?: (api: TriggerRenderProps) => React.ReactNode;\n children?: React.ReactNode | ((api: TriggerRenderProps) => React.ReactNode);\n } & React.HTMLAttributes<HTMLElement>\n>) => {\n const { openFileDialog, disabled, items } = useUplofile();\n const Comp: any = asChild ? Slot : \"button\";\n\n const uploading = items.filter((i) => i.status === \"uploading\");\n const uploadingCount = uploading.length;\n const doneCount = items.filter((i) => i.status === \"done\").length;\n const errorCount = items.filter((i) => i.status === \"error\").length;\n const totalProgress = uploadingCount\n ? Math.round(\n uploading.reduce(\n (acc, it) =>\n acc + (typeof it.progress === \"number\" ? it.progress : 0),\n 0,\n ) / uploadingCount,\n )\n : undefined;\n\n const api: TriggerRenderProps = {\n items,\n isUploading: uploadingCount > 0,\n uploadingCount,\n doneCount,\n errorCount,\n totalProgress,\n open: openFileDialog,\n };\n\n return (\n <Comp\n type={asChild ? undefined : \"button\"}\n aria-disabled={disabled}\n data-part=\"trigger\"\n onClick={(e: any) => {\n if (disabled) return;\n (rest as any).onClick?.(e);\n openFileDialog();\n }}\n {...rest}\n >\n {render ? render(api) : children}\n </Comp>\n );\n};\n","import { useContext } from \"react\";\n\nimport { UploaderCtx } from \"./context\";\n\nexport const useUplofile = () => {\n const ctx = useContext(UploaderCtx);\n if (!ctx)\n throw new Error(\n \"useUplofile hook must be used within <Uplofile.Root>\",\n );\n return ctx;\n};\n"],"names":[],"mappings":";;;AACO;AACP;AACA;;ACFO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;AC3EA;AACA;AACA;AACO;AACA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACA;AACA;;ACbA;;ACDA;AACP;AACA;AACA;AACA;;ACNO;;;"}
|