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 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
- UplofileDropzone,
40
- UplofilePreview,
41
- UplofileRoot,
42
- UplofileTrigger,
39
+ UplofileDropzone,
40
+ UplofilePreview,
41
+ UplofileRoot,
42
+ UplofileTrigger,
43
43
  } from "uplofile";
44
44
 
45
45
  export default function Basic() {
46
46
  return (
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>
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 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;
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?.preview || result.url;
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.0",
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"