uplofile 2.0.0 → 2.2.0

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 CHANGED
@@ -10,6 +10,7 @@ Accessible, unstyled primitives for building your own upload UI — with drag‑
10
10
  - React 16+ compatible
11
11
  - Drag‑and‑drop or click‑to‑upload
12
12
  - Upload progress, plus cancel/retry/remove actions
13
+ - **Custom Validation:** Use `beforeUpload` to validate files before they start uploading
13
14
  - Hidden input for form submissions
14
15
  - Unstyled — bring your own design
15
16
 
@@ -29,53 +30,17 @@ pnpm add uplofile
29
30
 
30
31
  ## Quick Start
31
32
 
32
- Import the primitives:
33
+ Import and use the components in your React component:
33
34
 
34
- ```typescript
35
- import * as Uplofile from "uplofile";
36
-
37
- const UplofileRoot = Uplofile.Root;
38
-
39
- const UplofileTrigger = Uplofile.Trigger;
40
-
41
- const UplofileHiddenInput = Uplofile.HiddenInput;
42
-
43
- const UplofileDropzone = Uplofile.Dropzone;
44
-
45
- const UplofilePreview = Uplofile.Preview;
46
-
47
- const UplofileCancel = Uplofile.Cancel;
48
-
49
- const UplofileRetry = Uplofile.Retry;
50
-
51
- const UplofileRemove = Uplofile.Remove;
52
-
53
- export {
54
- UplofileRoot,
55
- UplofileTrigger,
56
- UplofileHiddenInput,
57
- UplofileDropzone,
58
- UplofilePreview,
59
- UplofileCancel,
60
- UplofileRetry,
61
- UplofileRemove,
62
- };
63
-
64
- ```
65
-
66
- Then use them in your React component:
67
35
  ```tsx
68
36
  "use client";
69
37
 
70
38
  import {
71
- UplofileCancel,
72
39
  UplofileDropzone,
73
40
  UplofilePreview,
74
- UplofileRemove,
75
- UplofileRetry,
76
41
  UplofileRoot,
77
42
  UplofileTrigger,
78
- } from "./components/ui/uplofile";
43
+ } from "uplofile";
79
44
 
80
45
  export default function Basic() {
81
46
  return (
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,9 +20,11 @@ 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;
@@ -31,9 +34,16 @@ type UplofileRootRef<TMeta = any> = {
31
34
  openFileDialog: () => void;
32
35
  actions: ItemActions;
33
36
  };
37
+ type BeforeUploadResult<TMeta = any> = boolean | Array<{
38
+ valid: boolean;
39
+ meta?: TMeta;
40
+ id?: string;
41
+ uid: string;
42
+ reason?: string;
43
+ }>;
34
44
  type RootProps<TMeta = any> = PropsWithChildren<{
35
45
  multiple?: boolean;
36
- initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>;
46
+ initial?: MaybePromise<Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>>;
37
47
  /**
38
48
  * optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
39
49
  * strict: call onRemove first; only remove from UI if it succeeds.
@@ -43,8 +53,9 @@ type RootProps<TMeta = any> = PropsWithChildren<{
43
53
  maxCount?: number;
44
54
  disabled?: boolean;
45
55
  accept?: string;
56
+ beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
46
57
  onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
47
- upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
58
+ upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<TMeta>>;
48
59
  onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
49
60
  }>;
50
61
  type ItemActions = {
@@ -97,7 +108,7 @@ type PreviewRenderProps<TMeta = any> = {
97
108
  type Props<TMeta = any> = {
98
109
  render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
99
110
  };
100
- declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | Iterable<React.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
111
+ declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
101
112
  declare const HiddenInput: ({ name }: {
102
113
  name?: string;
103
114
  }) => react_jsx_runtime.JSX.Element;
@@ -112,14 +123,15 @@ declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runt
112
123
 
113
124
  declare const Root: React.ForwardRefExoticComponent<{
114
125
  multiple?: boolean;
115
- initial?: Pick<UploadFileItem<unknown>, "name" | "uid" | "id" | "url" | "meta">[] | undefined;
126
+ initial?: MaybePromise<Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[]> | undefined;
116
127
  removeMode?: "optimistic" | "strict";
117
128
  name?: string;
118
129
  maxCount?: number;
119
130
  disabled?: boolean;
120
131
  accept?: string;
132
+ beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
121
133
  onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
122
- upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
134
+ upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<unknown>>;
123
135
  onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
124
136
  } & {
125
137
  children?: React.ReactNode | undefined;
@@ -138,4 +150,4 @@ declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[
138
150
  declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
139
151
 
140
152
  export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
141
- export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
153
+ export type { BeforeUploadResult, ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
package/dist/index.mjs CHANGED
@@ -115,7 +115,7 @@ 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/*", name = "image", maxCount, disabled, children }, ref)=>{
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
120
  const controllers = useRef(new Map());
121
121
  const removeControllers = useRef(new Map());
@@ -124,21 +124,24 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
124
124
  // Hydrate initial items from the server and keep them marked as done
125
125
  useEffect(()=>{
126
126
  if (hasHydratedInitialRef.current) return;
127
- const arr = initial ?? [];
128
- if (!Array.isArray(arr) || arr.length === 0) return;
129
- const mapped = arr.map((it)=>{
130
- return {
131
- uid: it.uid || it.id,
132
- id: it.id,
133
- name: it.name,
134
- url: it.url,
135
- status: "done",
136
- meta: it.meta
137
- };
138
- });
139
- // Only hydrate if the user hasn't already added/modified items locally
140
- setItems((prev)=>prev.length === 0 ? mapped : prev);
141
- hasHydratedInitialRef.current = true;
127
+ const hydrate = async ()=>{
128
+ const arr = await initial;
129
+ if (!Array.isArray(arr) || arr.length === 0) return;
130
+ const mapped = arr.map((it)=>{
131
+ return {
132
+ uid: it.uid || it.id,
133
+ id: it.id,
134
+ name: it.name,
135
+ url: it.url,
136
+ status: "done",
137
+ meta: it.meta
138
+ };
139
+ });
140
+ // Only hydrate if the user hasn't already added/modified items locally
141
+ setItems((prev)=>prev.length === 0 ? mapped : prev);
142
+ hasHydratedInitialRef.current = true;
143
+ };
144
+ void hydrate();
142
145
  }, [
143
146
  initial
144
147
  ]);
@@ -183,13 +186,14 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
183
186
  } catch {
184
187
  /*fail silently*/ }
185
188
  }
186
- const serverPreview = result?.preview || result.url;
189
+ const serverPreview = result.previewUrl ?? result.preview ?? result.url;
187
190
  return {
188
191
  ...it,
189
192
  status: "done",
190
193
  url: result.url,
191
- id: result.id,
194
+ id: result.id ?? it.id,
192
195
  previewUrl: serverPreview,
196
+ meta: result.meta ?? it.meta,
193
197
  progress: 100
194
198
  };
195
199
  }));
@@ -207,13 +211,13 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
207
211
  emitChange,
208
212
  upload
209
213
  ]);
210
- const selectFiles = useCallback((files)=>{
214
+ const selectFiles = useCallback(async (files)=>{
211
215
  if (!files) return;
212
216
  const selected = Array.isArray(files) ? files : Array.from(files);
213
217
  if (selected.length === 0) return;
214
218
  const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
215
219
  const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
216
- const newItems = toUse.map((file)=>({
220
+ let newItems = toUse.map((file)=>({
217
221
  uid: uid(),
218
222
  name: file.name,
219
223
  file,
@@ -221,12 +225,59 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
221
225
  status: "idle",
222
226
  progress: 0
223
227
  }));
224
- emitChange([
225
- ...items,
226
- ...newItems
227
- ]);
228
- newItems.forEach((it)=>startUpload(it));
228
+ if (beforeUpload) {
229
+ const result = await beforeUpload(newItems);
230
+ if (result === false) {
231
+ newItems.forEach((it)=>{
232
+ if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);
233
+ });
234
+ return;
235
+ }
236
+ if (Array.isArray(result)) {
237
+ const resultMap = new Map(result.map((r)=>[
238
+ r.uid,
239
+ r
240
+ ]));
241
+ const processedItems = [];
242
+ for (const item of newItems){
243
+ const validation = resultMap.get(item.uid);
244
+ if (!validation) {
245
+ if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
246
+ continue;
247
+ }
248
+ if (validation.valid) {
249
+ processedItems.push({
250
+ ...item,
251
+ meta: validation.meta !== undefined ? validation.meta : item.meta,
252
+ id: validation.id !== undefined ? validation.id : item.id
253
+ });
254
+ } else if (validation.reason) {
255
+ processedItems.push({
256
+ ...item,
257
+ status: "error",
258
+ error: validation.reason,
259
+ meta: validation.meta !== undefined ? validation.meta : item.meta,
260
+ id: validation.id !== undefined ? validation.id : item.id
261
+ });
262
+ } else {
263
+ if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
264
+ }
265
+ }
266
+ newItems = processedItems;
267
+ }
268
+ }
269
+ if (newItems.length === 0) return;
270
+ emitChange((prev)=>[
271
+ ...prev,
272
+ ...newItems
273
+ ]);
274
+ newItems.forEach((it)=>{
275
+ if (it.status === "idle") {
276
+ startUpload(it);
277
+ }
278
+ });
229
279
  }, [
280
+ beforeUpload,
230
281
  emitChange,
231
282
  items,
232
283
  maxCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplofile",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Composable file‑upload components for React.",
5
5
  "license": "MIT",
6
6
  "author": "Chris Josh <KristofaJosh>",