uplofile 1.1.1 → 2.1.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
@@ -1,13 +1,13 @@
1
1
  /// <reference lib="es2015.iterable" />
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
- import React, { HTMLAttributes, PropsWithChildren, RefObject, ChangeEvent, DragEvent, ButtonHTMLAttributes } from 'react';
3
+ import React, { HTMLAttributes, DragEvent, RefObject, ChangeEvent, PropsWithChildren, ButtonHTMLAttributes } from 'react';
4
4
 
5
5
  declare const Dropzone: ({ asChild, ...rest }: {
6
6
  asChild?: boolean;
7
7
  } & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
8
8
 
9
9
  type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
10
- type UploadFileItem = {
10
+ type UploadFileItem<TMeta = any> = {
11
11
  uid: string;
12
12
  id?: string;
13
13
  name: string;
@@ -17,15 +17,30 @@ type UploadFileItem = {
17
17
  status: UploadStatus;
18
18
  progress?: number;
19
19
  error?: string;
20
- data?: any;
20
+ meta?: TMeta;
21
21
  };
22
22
  type UploadResult = {
23
23
  url: string;
24
24
  id?: string;
25
25
  };
26
- type RootProps = PropsWithChildren<{
26
+ type UplofileRootRef<TMeta = any> = {
27
+ setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
28
+ getItems: () => UploadFileItem<TMeta>[];
29
+ onDrop: (e: DragEvent) => void;
30
+ onDragOver: (e: DragEvent) => void;
31
+ openFileDialog: () => void;
32
+ actions: ItemActions;
33
+ };
34
+ type BeforeUploadResult<TMeta = any> = boolean | Array<{
35
+ valid: boolean;
36
+ meta?: TMeta;
37
+ id?: string;
38
+ uid: string;
39
+ reason?: string;
40
+ }>;
41
+ type RootProps<TMeta = any> = PropsWithChildren<{
27
42
  multiple?: boolean;
28
- initial?: Array<Pick<UploadFileItem, "uid" | "id" | "name" | "url">>;
43
+ initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>;
29
44
  /**
30
45
  * optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
31
46
  * strict: call onRemove first; only remove from UI if it succeeds.
@@ -35,18 +50,19 @@ type RootProps = PropsWithChildren<{
35
50
  maxCount?: number;
36
51
  disabled?: boolean;
37
52
  accept?: string;
38
- onChange?: (items: UploadFileItem[]) => Promise<void> | void;
53
+ beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
54
+ onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
39
55
  upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
40
- onRemove?: (item: UploadFileItem, signal: AbortSignal) => Promise<void | any>;
56
+ onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
41
57
  }>;
42
58
  type ItemActions = {
43
59
  cancel: (uid: string) => void;
44
60
  remove: (uid: string) => void;
45
61
  retry: (uid: string) => void;
46
62
  };
47
- type ImageUploaderContextValue = {
48
- items: UploadFileItem[];
49
- setItems: (items: UploadFileItem[]) => void;
63
+ type ImageUploaderContextValue<TMeta = any> = {
64
+ items: UploadFileItem<TMeta>[];
65
+ setItems: (items: UploadFileItem<TMeta>[]) => void;
50
66
  disabled?: boolean;
51
67
  multiple: boolean;
52
68
  accept: string;
@@ -71,8 +87,8 @@ type ImageUploaderContextValue = {
71
87
  hiddenInputValue: string;
72
88
  name: string;
73
89
  };
74
- type TriggerRenderProps = {
75
- items: UploadFileItem[];
90
+ type TriggerRenderProps<TMeta = any> = {
91
+ items: UploadFileItem<TMeta>[];
76
92
  isUploading: boolean;
77
93
  uploadingCount: number;
78
94
  doneCount: number;
@@ -80,16 +96,16 @@ type TriggerRenderProps = {
80
96
  totalProgress?: number;
81
97
  open: () => void;
82
98
  };
83
- type PreviewRenderProps = {
84
- items: UploadFileItem[];
85
- setItems: (items: UploadFileItem[]) => void;
99
+ type PreviewRenderProps<TMeta = any> = {
100
+ items: UploadFileItem<TMeta>[];
101
+ setItems: (items: UploadFileItem<TMeta>[]) => void;
86
102
  actions: ItemActions;
87
103
  };
88
104
 
89
- type Props = {
90
- render?: (api: PreviewRenderProps) => React.ReactNode;
105
+ type Props<TMeta = any> = {
106
+ render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
91
107
  };
92
- declare const Preview: ({ render }: Props) => string | number | boolean | Iterable<React.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
108
+ declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
93
109
  declare const HiddenInput: ({ name }: {
94
110
  name?: string;
95
111
  }) => react_jsx_runtime.JSX.Element;
@@ -102,17 +118,33 @@ declare const Cancel: ({ uid, asChild, alwaysVisible, ...rest }: ButtonProps) =>
102
118
  declare const Retry: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
103
119
  declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
104
120
 
105
- declare const Root: ({ multiple, initial, onChange, upload, removeMode, onRemove, accept, name, maxCount, disabled, children, }: RootProps) => react_jsx_runtime.JSX.Element;
121
+ declare const Root: React.ForwardRefExoticComponent<{
122
+ multiple?: boolean;
123
+ initial?: Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[] | undefined;
124
+ removeMode?: "optimistic" | "strict";
125
+ name?: string;
126
+ maxCount?: number;
127
+ disabled?: boolean;
128
+ accept?: string;
129
+ beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
130
+ onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
131
+ upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
132
+ onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
133
+ } & {
134
+ children?: React.ReactNode | undefined;
135
+ } & React.RefAttributes<UplofileRootRef<unknown>>>;
106
136
 
107
- declare const Trigger: ({ asChild, children, render, ...rest }: PropsWithChildren<{
137
+ declare const Trigger: <TMeta = any>({ asChild, children, render, ...rest }: PropsWithChildren<{
108
138
  asChild?: boolean;
109
- render?: (api: TriggerRenderProps) => React.ReactNode;
110
- children?: React.ReactNode | ((api: TriggerRenderProps) => React.ReactNode);
139
+ render?: (api: TriggerRenderProps<TMeta>) => React.ReactNode;
140
+ children?: React.ReactNode | ((api: TriggerRenderProps<TMeta>) => React.ReactNode);
111
141
  } & React.HTMLAttributes<HTMLElement>>) => react_jsx_runtime.JSX.Element;
112
142
 
113
- declare const useUplofile: () => ImageUploaderContextValue;
143
+ declare const useUplofile: <TMeta = any>() => ImageUploaderContextValue<TMeta>;
114
144
 
115
- declare const isVideoFile: (item: UploadFileItem) => boolean;
145
+ declare const getExtension: (path: string) => string | undefined;
146
+ declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
147
+ declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
116
148
 
117
- export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
118
- export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus };
149
+ export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
150
+ export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
package/dist/index.mjs CHANGED
@@ -1,45 +1,121 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { Slot } from '@radix-ui/react-slot';
3
- import { useState, useRef, useEffect, useMemo, useCallback, createContext, useContext } from 'react';
3
+ import { forwardRef, useState, useRef, useEffect, useMemo, useCallback, useImperativeHandle, createContext, useContext } from 'react';
4
4
 
5
5
  const uid = ()=>Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
6
- const isVideoFile = (item)=>{
6
+ const VIDEO_EXTENSIONS = [
7
+ "mp4",
8
+ "webm",
9
+ "ogg",
10
+ "mov",
11
+ "avi",
12
+ "mkv",
13
+ "wmv",
14
+ "flv",
15
+ "3gp",
16
+ "m4v",
17
+ "mpg",
18
+ "mpeg"
19
+ ];
20
+ const IMAGE_EXTENSIONS = [
21
+ "jpg",
22
+ "jpeg",
23
+ "png",
24
+ "gif",
25
+ "webp",
26
+ "svg",
27
+ "avif",
28
+ "bmp",
29
+ "ico",
30
+ "tiff"
31
+ ];
32
+ const getExtension = (path)=>{
33
+ try {
34
+ const url = new URL(path, "http://localhost");
35
+ const pathname = url.pathname;
36
+ const parts = pathname.split(".");
37
+ if (parts.length > 1) return parts.pop()?.toLowerCase();
38
+ return undefined;
39
+ } catch {
40
+ const parts = path.split("?")[0].split("#")[0].split(".");
41
+ if (parts.length > 1) return parts.pop()?.toLowerCase();
42
+ return undefined;
43
+ }
44
+ };
45
+ const isVideoFile = (item, extraExtensions = [])=>{
7
46
  if (item.file) {
8
47
  return item.file.type.startsWith("video/");
9
48
  }
49
+ const allExtensions = [
50
+ ...VIDEO_EXTENSIONS,
51
+ ...extraExtensions
52
+ ];
53
+ if (item.url) {
54
+ const extension = getExtension(item.url);
55
+ if (extension && allExtensions.includes(extension)) {
56
+ return true;
57
+ }
58
+ }
59
+ if (item.name) {
60
+ const extension = getExtension(item.name);
61
+ if (extension && allExtensions.includes(extension)) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ };
67
+ const isImageFile = (item, extraExtensions = [])=>{
68
+ if (item.file) {
69
+ return item.file.type.startsWith("image/");
70
+ }
71
+ const allExtensions = [
72
+ ...IMAGE_EXTENSIONS,
73
+ ...extraExtensions
74
+ ];
10
75
  if (item.url) {
11
- const extension = item.url.split(".").pop()?.toLowerCase();
12
- const videoExtensions = [
13
- "mp4",
14
- "webm",
15
- "ogg",
16
- "mov",
17
- "avi",
18
- "mkv"
19
- ];
20
- if (extension && videoExtensions.includes(extension)) {
76
+ const extension = getExtension(item.url);
77
+ if (extension && allExtensions.includes(extension)) {
21
78
  return true;
22
79
  }
23
80
  }
24
81
  if (item.name) {
25
- const extension = item.name.split(".").pop()?.toLowerCase();
26
- const videoExtensions = [
27
- "mp4",
28
- "webm",
29
- "ogg",
30
- "mov",
31
- "avi",
32
- "mkv"
33
- ];
34
- if (extension && videoExtensions.includes(extension)) {
82
+ const extension = getExtension(item.name);
83
+ if (extension && allExtensions.includes(extension)) {
35
84
  return true;
36
85
  }
37
86
  }
38
87
  return false;
39
88
  };
89
+ const acceptsFile = (file, accept)=>{
90
+ if (!accept || accept.trim() === "") return true;
91
+ const type = (file.type || "").toLowerCase();
92
+ const name = (file.name || "").toLowerCase();
93
+ const tokens = accept.split(",").map((t)=>t.trim().toLowerCase()).filter(Boolean);
94
+ // If accept is malformed, be permissive (browser file input also ignores)
95
+ if (tokens.length === 0) return true;
96
+ // Helper: match extension token like .jpg or jpg
97
+ const hasExt = (token)=>{
98
+ const extToken = token.startsWith(".") ? token.slice(1) : token;
99
+ const fileExt = name.includes(".") ? name.split(".").pop() : undefined;
100
+ return !!fileExt && fileExt === extToken;
101
+ };
102
+ // Check any token matches
103
+ return tokens.some((token)=>{
104
+ if (token === "*/*") return true;
105
+ // MIME type with wildcard, e.g., image/*
106
+ if (token.endsWith("/*")) {
107
+ const prefix = token.slice(0, -1); // keep the slash
108
+ return type.startsWith(prefix);
109
+ }
110
+ // Full MIME type e.g., image/png
111
+ if (token.includes("/")) return type === token;
112
+ // File extensions, with or without a leading dot
113
+ return hasExt(token);
114
+ });
115
+ };
40
116
 
41
117
  const UploaderCtx = /*#__PURE__*/ createContext(null);
42
- const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "images", maxCount, disabled, children })=>{
118
+ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", beforeUpload, name = "image", maxCount, disabled, children }, ref)=>{
43
119
  const [items, setItems] = useState([]);
44
120
  const controllers = useRef(new Map());
45
121
  const removeControllers = useRef(new Map());
@@ -56,7 +132,8 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
56
132
  id: it.id,
57
133
  name: it.name,
58
134
  url: it.url,
59
- status: "done"
135
+ status: "done",
136
+ meta: it.meta
60
137
  };
61
138
  });
62
139
  // Only hydrate if the user hasn't already added/modified items locally
@@ -111,7 +188,7 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
111
188
  ...it,
112
189
  status: "done",
113
190
  url: result.url,
114
- id: result.id,
191
+ id: result.id ?? it.id,
115
192
  previewUrl: serverPreview,
116
193
  progress: 100
117
194
  };
@@ -130,12 +207,13 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
130
207
  emitChange,
131
208
  upload
132
209
  ]);
133
- const selectFiles = useCallback((files)=>{
134
- if (!files || files.length === 0) return;
135
- const selected = Array.from(files);
210
+ const selectFiles = useCallback(async (files)=>{
211
+ if (!files) return;
212
+ const selected = Array.isArray(files) ? files : Array.from(files);
213
+ if (selected.length === 0) return;
136
214
  const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
137
215
  const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
138
- const newItems = toUse.map((file)=>({
216
+ let newItems = toUse.map((file)=>({
139
217
  uid: uid(),
140
218
  name: file.name,
141
219
  file,
@@ -143,12 +221,59 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
143
221
  status: "idle",
144
222
  progress: 0
145
223
  }));
146
- emitChange([
147
- ...items,
148
- ...newItems
149
- ]);
150
- newItems.forEach((it)=>startUpload(it));
224
+ if (beforeUpload) {
225
+ const result = await beforeUpload(newItems);
226
+ if (result === false) {
227
+ newItems.forEach((it)=>{
228
+ if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);
229
+ });
230
+ return;
231
+ }
232
+ if (Array.isArray(result)) {
233
+ const resultMap = new Map(result.map((r)=>[
234
+ r.uid,
235
+ r
236
+ ]));
237
+ const processedItems = [];
238
+ for (const item of newItems){
239
+ const validation = resultMap.get(item.uid);
240
+ if (!validation) {
241
+ if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
242
+ continue;
243
+ }
244
+ if (validation.valid) {
245
+ processedItems.push({
246
+ ...item,
247
+ meta: validation.meta !== undefined ? validation.meta : item.meta,
248
+ id: validation.id !== undefined ? validation.id : item.id
249
+ });
250
+ } else if (validation.reason) {
251
+ processedItems.push({
252
+ ...item,
253
+ status: "error",
254
+ error: validation.reason,
255
+ meta: validation.meta !== undefined ? validation.meta : item.meta,
256
+ id: validation.id !== undefined ? validation.id : item.id
257
+ });
258
+ } else {
259
+ if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
260
+ }
261
+ }
262
+ newItems = processedItems;
263
+ }
264
+ }
265
+ if (newItems.length === 0) return;
266
+ emitChange((prev)=>[
267
+ ...prev,
268
+ ...newItems
269
+ ]);
270
+ newItems.forEach((it)=>{
271
+ if (it.status === "idle") {
272
+ startUpload(it);
273
+ }
274
+ });
151
275
  }, [
276
+ beforeUpload,
152
277
  emitChange,
153
278
  items,
154
279
  maxCount,
@@ -163,10 +288,15 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
163
288
  const onDrop = useCallback((e)=>{
164
289
  e.preventDefault();
165
290
  if (disabled) return;
166
- selectFiles(e.dataTransfer.files);
291
+ const dtFiles = e.dataTransfer?.files;
292
+ if (!dtFiles || dtFiles.length === 0) return;
293
+ const accepted = Array.from(dtFiles).filter((f)=>acceptsFile(f, accept));
294
+ if (accepted.length === 0) return;
295
+ selectFiles(accepted);
167
296
  }, [
168
297
  disabled,
169
- selectFiles
298
+ selectFiles,
299
+ accept
170
300
  ]);
171
301
  const onDragOver = useCallback((e)=>e.preventDefault(), []);
172
302
  const actions = useMemo(()=>({
@@ -271,6 +401,21 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
271
401
  hiddenInputValue,
272
402
  name
273
403
  };
404
+ // Expose imperative methods via ref
405
+ useImperativeHandle(ref, ()=>({
406
+ setItems: emitChange,
407
+ getItems: ()=>items,
408
+ onDrop,
409
+ onDragOver,
410
+ openFileDialog: ()=>inputRef.current?.click(),
411
+ actions
412
+ }), [
413
+ emitChange,
414
+ items,
415
+ onDrop,
416
+ onDragOver,
417
+ actions
418
+ ]);
274
419
  return /*#__PURE__*/ jsx(UploaderCtx.Provider, {
275
420
  value: ctx,
276
421
  children: /*#__PURE__*/ jsxs("div", {
@@ -285,7 +430,7 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
285
430
  ]
286
431
  })
287
432
  });
288
- };
433
+ });
289
434
 
290
435
  const useUplofile = ()=>{
291
436
  const ctx = useContext(UploaderCtx);
@@ -582,33 +727,36 @@ const Cancel = ({ uid, asChild, alwaysVisible = false, ...rest })=>{
582
727
  const Comp = asChild ? Slot : "button";
583
728
  if (!isUploading && !alwaysVisible) return null;
584
729
  return /*#__PURE__*/ jsx(Comp, {
730
+ ...rest,
585
731
  onClick: (e)=>{
586
732
  e.stopPropagation();
587
733
  actions.cancel(uid);
588
- },
589
- ...rest
734
+ rest.onClick?.(e);
735
+ }
590
736
  });
591
737
  };
592
738
  const Retry = ({ uid, asChild, ...rest })=>{
593
739
  const { actions } = useUplofile();
594
740
  const Comp = asChild ? Slot : "button";
595
741
  return /*#__PURE__*/ jsx(Comp, {
742
+ ...rest,
596
743
  onClick: (e)=>{
597
744
  e.stopPropagation();
598
745
  actions.retry(uid);
599
- },
600
- ...rest
746
+ rest.onClick?.(e);
747
+ }
601
748
  });
602
749
  };
603
750
  const Remove = ({ uid, asChild, ...rest })=>{
604
751
  const { actions } = useUplofile();
605
752
  const Comp = asChild ? Slot : "button";
606
753
  return /*#__PURE__*/ jsx(Comp, {
754
+ ...rest,
607
755
  onClick: (e)=>{
608
756
  e.stopPropagation();
609
757
  actions.remove(uid);
610
- },
611
- ...rest
758
+ rest.onClick?.(e);
759
+ }
612
760
  });
613
761
  };
614
762
 
@@ -643,4 +791,4 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
643
791
  });
644
792
  };
645
793
 
646
- export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
794
+ export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplofile",
3
- "version": "1.1.1",
3
+ "version": "2.1.0",
4
4
  "description": "Composable file‑upload components for React.",
5
5
  "license": "MIT",
6
6
  "author": "Chris Josh <KristofaJosh>",