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 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
@@ -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
- 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;
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: "uplofile-preview__wrapper grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5",
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
- rest.onClick?.(e);
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.0",
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"