@better-s3/react 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamidreza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @better-s3/react
2
+
3
+ Headless React hooks for S3 file uploads, downloads, and deletes — full state management, progress tracking, and cancellation. Bring your own UI.
4
+
5
+ > **Want pre-built components?** Use [`@better-s3/ui`](../better-s3-ui) for ready-to-use shadcn/ui components.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @better-s3/react @better-s3/core
11
+ ```
12
+
13
+ ## Hooks
14
+
15
+ ### `useUpload` — Single file upload
16
+
17
+ ```tsx
18
+ import { useUpload } from "@better-s3/react";
19
+ import { createPresignApi } from "@better-s3/core";
20
+
21
+ const presignApi = createPresignApi({ basePath: "/api/s3" });
22
+
23
+ function MyComponent() {
24
+ const { phase, progress, error, upload, cancel, reset } = useUpload({
25
+ presignApi,
26
+ objectKey: (file) => `uploads/${file.name}`,
27
+ accept: ["image/*"],
28
+ maxFileSize: 10 * 1024 * 1024, // 10 MB
29
+ onSuccess: (result) => console.log("Uploaded:", result.url),
30
+ });
31
+
32
+ return <button onClick={() => upload(file)}>Upload</button>;
33
+ }
34
+ ```
35
+
36
+ **Phases:** `idle → validating → presigning → uploading → finalizing → success | error`
37
+
38
+ ### `useUploadControls` — Upload with file picker & drag-drop
39
+
40
+ ```tsx
41
+ import { useUploadControls } from "@better-s3/react";
42
+
43
+ function Uploader() {
44
+ const { phase, progress, openFilePicker, inputProps, dropHandlers } =
45
+ useUploadControls({
46
+ presignApi,
47
+ objectKey: (file) => `uploads/${file.name}`,
48
+ });
49
+
50
+ return (
51
+ <div {...dropHandlers}>
52
+ <input {...inputProps} />
53
+ <button onClick={openFilePicker}>
54
+ {phase === "uploading" ? `${progress}%` : "Choose file"}
55
+ </button>
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ### `useMultiUpload` — Batch file upload
62
+
63
+ ```tsx
64
+ import { useMultiUpload } from "@better-s3/react";
65
+
66
+ const { phase, files, totalProgress, upload, cancel } = useMultiUpload({
67
+ presignApi,
68
+ objectKey: (file) => `uploads/${file.name}`,
69
+ maxFiles: 10,
70
+ concurrentFiles: 3,
71
+ });
72
+
73
+ // files: Array<{ id, name, size, status, progress, error }>
74
+ ```
75
+
76
+ ### `useMultiUploadControls` — Batch upload with file picker
77
+
78
+ Same convenience layer as `useUploadControls`, but for multiple files.
79
+
80
+ ### `useDownload` — File download
81
+
82
+ ```tsx
83
+ import { useDownload } from "@better-s3/react";
84
+
85
+ const { phase, progress, download, cancel } = useDownload({
86
+ presignApi,
87
+ mode: "fetch", // "native" for browser download, "fetch" for streaming
88
+ });
89
+
90
+ // Trigger download
91
+ download({ key: "uploads/photo.jpg", fileName: "photo.jpg" });
92
+ ```
93
+
94
+ **Modes:**
95
+
96
+ - `native` — opens presigned URL in new window (browser handles download)
97
+ - `fetch` — streams file with progress tracking
98
+
99
+ ### `useDelete` — File deletion with confirmation
100
+
101
+ ```tsx
102
+ import { useDelete } from "@better-s3/react";
103
+
104
+ const { phase, pendingKey, requestDelete, confirmDelete, cancelDelete } =
105
+ useDelete({ presignApi });
106
+
107
+ // Two-step flow:
108
+ requestDelete("uploads/photo.jpg"); // phase → "confirming"
109
+ confirmDelete(); // phase → "deleting" → "success"
110
+ ```
111
+
112
+ ## All hooks return
113
+
114
+ - `phase` — current state of the operation
115
+ - `error` — error message if failed
116
+ - `reset()` — return to idle state
117
+ - `cancel()` — abort in-flight operation
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,6 @@
1
+ export { useUpload, type UseUploadOptions, type UseUploadState, type UseUploadReturn, } from "./use-upload";
2
+ export { useMultiUpload, type UseMultiUploadOptions, type UseMultiUploadState, type UseMultiUploadReturn, } from "./use-multi-upload";
3
+ export { useUploadControls, type UseUploadControlsOptions, type UseUploadControlsReturn, } from "./use-upload-controls";
4
+ export { useMultiUploadControls, type UseMultiUploadControlsOptions, type UseMultiUploadControlsReturn, } from "./use-multi-upload-controls";
5
+ export { useDownload, type UseDownloadOptions, type UseDownloadState, type UseDownloadReturn, } from "./use-download";
6
+ export { useDelete, type UseDeleteOptions, type UseDeleteState, type UseDeleteReturn, } from "./use-delete";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import {useState,useRef,useCallback}from'react';import {validateFile,uploadFile,uploadFiles}from'@better-s3/core';var B={loaded:0,total:0,percent:0},R={phase:"idle",progress:B,error:null,result:null,fileName:null,fileSize:null};function x(g){let[h,t]=useState(R),r=useRef(g);r.current=g;let a=useRef(null),m=useCallback(async(o,f)=>{t({...R,phase:"validating",fileName:o.name,fileSize:o.size});let l=r.current,e=validateFile(o,{accept:l.accept,maxFileSize:l.maxFileSize});if(e){t(p=>({...p,phase:"error",error:e})),l.onError?.(o,new Error(e),"validating");return}if(l.beforeUpload&&!await l.beforeUpload(o)){t(u=>({...u,phase:"error",error:"Upload blocked by beforeUpload hook"})),l.onError?.(o,new Error("blocked"),"validating");return}t(p=>({...p,phase:"uploading"})),l.onUploadStart?.(o,f);let U=new AbortController;a.current=U;try{let p=await uploadFile(l.presignApi,o,f,{multipart:l.multipart,multipartThreshold:l.multipartThreshold,concurrentParts:l.concurrentParts},{onProgress:u=>{t(b=>({...b,progress:u})),l.onProgress?.(o,u);}},U.signal);t(u=>({...u,phase:"success",result:p,progress:{loaded:o.size,total:o.size,percent:100}})),await l.onSuccess?.(o,p);}catch(p){if(p.name==="AbortError"){l.onCancel?.(o),t(R);return}let u=p instanceof Error?p.message:"Upload failed";t(b=>({...b,phase:"error",error:u})),l.onError?.(o,p,"uploading");}finally{a.current=null;}},[]),w=useCallback(()=>{a.current?.abort(),t(R);},[]),D=useCallback(()=>{a.current?.abort(),t(R);},[]);return {...h,upload:m,cancel:w,reset:D}}var W={loaded:0,total:0,percent:0},M={phase:"idle",files:[],totalProgress:W,error:null},X=0;function Y(){return `file-${++X}`}function I(g){let[h,t]=useState(M),r=useRef(g);r.current=g;let a=useRef(null),m=useRef(new Map),w=useCallback(async(f,l)=>{let e=r.current,U=[],p=[],u=new Map;if(t(n=>({...n,phase:"validating",error:null})),e.maxFiles&&f.length>e.maxFiles){let n=`Too many files. Maximum is ${e.maxFiles}.`;t(c=>({...c,phase:"error",error:n})),e.onError?.(new Error(n));return}for(let n of f){let c=validateFile(n,{accept:e.accept,maxFileSize:e.maxFileSize});if(c){let S=`${n.name}: ${c}`;t(s=>({...s,phase:"error",error:S})),e.onError?.(new Error(S));return}}if(e.beforeUpload&&!await e.beforeUpload(f)){t(c=>({...c,phase:"error",error:"Upload blocked by beforeUpload hook"})),e.onError?.(new Error("blocked"));return}for(let n of f){let c=Y(),S=l(n);U.push({id:c,file:n,objectKey:S}),u.set(c,n),p.push({id:c,fileName:n.name,fileSize:n.size,status:"pending",progress:{loaded:0,total:n.size,percent:0},error:null});}m.current=u,t({phase:"uploading",files:p,totalProgress:{loaded:0,total:f.reduce((n,c)=>n+c.size,0),percent:0},error:null}),e.onUploadStart?.(f);let b=new AbortController;a.current=b;try{let n=await uploadFiles(e.presignApi,U,{multipart:e.multipart,multipartThreshold:e.multipartThreshold,concurrentParts:e.concurrentParts,concurrentFiles:e.concurrentFiles},{onFileProgress:(s,d)=>{t(P=>({...P,files:P.files.map(i=>i.id===s?{...i,status:"uploading",progress:d}:i)}));let y=u.get(s);y&&e.onFileProgress?.(y,d);},onFileSuccess:(s,d)=>{t(P=>({...P,files:P.files.map(i=>i.id===s?{...i,status:"success",progress:{loaded:i.fileSize,total:i.fileSize,percent:100}}:i)}));let y=u.get(s);y&&e.onFileSuccess?.(y,d);},onFileError:(s,d)=>{t(P=>({...P,files:P.files.map(i=>i.id===s?{...i,status:"error",error:d}:i)}));let y=u.get(s);y&&e.onFileError?.(y,d);},onTotalProgress:s=>{t(d=>({...d,totalProgress:s})),e.onProgress?.(s);}},b.signal),c=n.some(s=>s.status==="error"),S=n.filter(s=>s.result!==null).map(s=>s.result);t(s=>({...s,phase:c?"error":"success",error:c?`${n.filter(d=>d.status==="error").length} file(s) failed`:null,totalProgress:c?s.totalProgress:{loaded:s.totalProgress.total,total:s.totalProgress.total,percent:100}})),c||await e.onSuccess?.(S);}catch(n){if(n.name==="AbortError"){e.onCancel?.(),t(M);return}let c=n instanceof Error?n.message:"Upload failed";t(S=>({...S,phase:"error",error:c})),e.onError?.(n);}finally{a.current=null;}},[]),D=useCallback(()=>{a.current?.abort(),t(M);},[]),o=useCallback(()=>{a.current?.abort(),t(M);},[]);return {...h,upload:w,cancel:D,reset:o}}function te(g){let{objectKey:h,...t}=g,r=x(t),a=useRef(null),[m,w]=useState(null),D=e=>typeof h=="function"?h(e):h,o=async e=>{let U=e?.[0];U&&(w({name:U.name,size:U.size}),await r.upload(U,D(U)));},f=()=>a.current?.click(),l=r.phase==="uploading";return {phase:r.phase,progress:r.progress,error:r.error,fileInfo:m,isUploading:l,handleFiles:o,openFilePicker:f,cancel:r.cancel,reset:()=>{r.reset(),w(null);},inputProps:{ref:a,type:"file",accept:t.accept?.join(","),hidden:true,onChange:e=>{o(e.target.files),e.target.value="";}},dropHandlers:{onDragOver:e=>{e.preventDefault(),e.stopPropagation();},onDrop:e=>{e.preventDefault(),e.stopPropagation(),l||o(e.dataTransfer.files);}}}}function re(g){let{objectKey:h,...t}=g,r=I(t),a=useRef(null),m=async o=>{o?.length&&await r.upload(Array.from(o),h);},w=()=>a.current?.click(),D=r.phase==="uploading";return {phase:r.phase,files:r.files,totalProgress:r.totalProgress,error:r.error,isUploading:D,handleFiles:m,openFilePicker:w,cancel:r.cancel,reset:r.reset,inputProps:{ref:a,type:"file",multiple:true,accept:t.accept?.join(","),hidden:true,onChange:o=>{m(o.target.files),o.target.value="";}},dropHandlers:{onDragOver:o=>{o.preventDefault(),o.stopPropagation();},onDrop:o=>{o.preventDefault(),o.stopPropagation(),D||m(o.dataTransfer.files);}}}}var N={loaded:0,total:0,percent:0},v={phase:"idle",progress:N,error:null,fileName:null,fileSize:null};function ne(g){let[h,t]=useState(v),r=useRef(g);r.current=g;let a=useRef(null),m=useCallback(async(o,f)=>{let l=f??o.split("/").pop()??o,e=r.current,U=e.mode??"native";if(e.beforeDownload&&!await e.beforeDownload(o)){t(u=>({...u,phase:"error",error:"Download blocked by beforeDownload hook"})),e.onError?.(o,new Error("blocked"),"presigning");return}t({phase:"presigning",progress:N,error:null,fileName:l,fileSize:null});try{let{url:p}=await e.presignApi.download(o,l);if(U==="native"){let i=document.createElement("a");i.href=p,i.download=l,i.click(),t(E=>({...E,phase:"success"})),await e.onSuccess?.(o),t(v);return}t(i=>({...i,phase:"downloading"})),e.onDownloadStart?.(o);let u=new AbortController;a.current=u;let b=await fetch(p,{signal:u.signal});if(!b.ok)throw new Error(`Download failed: ${b.statusText}`);let n=Number(b.headers.get("content-length")||0);t(i=>({...i,fileSize:n||null}));let c=b.body?.getReader();if(!c)throw new Error("ReadableStream not supported");let S=[],s=0;for(;;){let{done:i,value:E}=await c.read();if(i)break;S.push(E),s+=E.byteLength;let K=n>0?Math.round(s/n*100):0,z={loaded:s,total:n,percent:K};t(_=>({..._,progress:z})),e.onProgress?.(o,z);}let d=new Blob(S),y=URL.createObjectURL(d),P=document.createElement("a");P.href=y,P.download=l,P.click(),URL.revokeObjectURL(y),t(i=>({...i,phase:"success",fileSize:d.size,progress:{loaded:d.size,total:d.size,percent:100}})),await e.onSuccess?.(o);}catch(p){if(p.name==="AbortError"){e.onCancel?.(o),t(v);return}let u=p instanceof Error?p.message:"Download failed";t(b=>({...b,phase:"error",error:u})),e.onError?.(o,p,"downloading");}finally{a.current=null;}},[]),w=useCallback(()=>{a.current?.abort(),t(v);},[]),D=useCallback(()=>{a.current?.abort(),t(v);},[]);return {...h,download:m,cancel:w,reset:D}}var k={phase:"idle",error:null};function ae(g){let[h,t]=useState(k),[r,a]=useState(null),m=useRef(g);m.current=g;let w=useCallback(l=>{a(l),t({phase:"confirming",error:null});},[]),D=useCallback(async()=>{if(!r)return;let l=m.current;if(l.beforeDelete&&!await l.beforeDelete(r)){t({phase:"error",error:"Delete blocked by beforeDelete hook"}),l.onError?.(r,new Error("blocked"),"confirming"),a(null);return}t({phase:"deleting",error:null}),l.onDeleteStart?.(r);try{await l.presignApi.delete(r),t({phase:"success",error:null}),await l.onSuccess?.(r),a(null);}catch(e){let U=e instanceof Error?e.message:"Delete failed";t({phase:"error",error:U}),l.onError?.(r,e,"deleting");}},[r]),o=useCallback(()=>{a(null),t(k);},[]),f=useCallback(()=>{a(null),t(k);},[]);return {...h,pendingKey:r,requestDelete:w,confirmDelete:D,cancelDelete:o,reset:f}}export{ae as useDelete,ne as useDownload,I as useMultiUpload,re as useMultiUploadControls,x as useUpload,te as useUploadControls};//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/use-upload.ts","../src/use-multi-upload.ts","../src/use-upload-controls.ts","../src/use-multi-upload-controls.ts","../src/use-download.ts","../src/use-delete.ts"],"names":["INITIAL_PROGRESS","INITIAL_STATE","useUpload","options","state","setState","useState","optionsRef","useRef","abortRef","upload","useCallback","file","objectKey","opts","validationError","validateFile","s","controller","result","uploadFile","progress","err","message","cancel","reset","nextId","generateId","useMultiUpload","fileMapRef","files","resolveKey","items","fileStates","fileMap","msg","id","f","results","uploadFiles","error","hasErrors","r","successResults","useUploadControls","hookOptions","ctx","inputRef","fileInfo","setFileInfo","handleFiles","openFilePicker","isUploading","useMultiUploadControls","e","useDownload","download","key","downloadName","name","mode","url","anchor","res","contentLength","reader","chunks","loaded","done","value","percent","blob","blobUrl","useDelete","pendingKey","setPendingKey","requestDelete","confirmDelete","cancelDelete"],"mappings":"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","file":"index.js","sourcesContent":["\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type {\n PresignApi,\n UploadConfig,\n UploadHooks,\n UploadPhase,\n UploadProgress,\n UploadResult,\n} from \"@better-s3/core\";\nimport { uploadFile, validateFile } from \"@better-s3/core\";\n\nexport type UseUploadOptions = UploadConfig &\n UploadHooks & {\n presignApi: PresignApi;\n };\n\nexport type UseUploadState = {\n phase: UploadPhase;\n progress: UploadProgress;\n error: string | null;\n result: UploadResult | null;\n fileName: string | null;\n fileSize: number | null;\n};\n\nexport type UseUploadReturn = UseUploadState & {\n upload: (file: File, objectKey: string) => Promise<void>;\n cancel: () => void;\n reset: () => void;\n};\n\nconst INITIAL_PROGRESS: UploadProgress = { loaded: 0, total: 0, percent: 0 };\n\nconst INITIAL_STATE: UseUploadState = {\n phase: \"idle\",\n progress: INITIAL_PROGRESS,\n error: null,\n result: null,\n fileName: null,\n fileSize: null,\n};\n\nexport function useUpload(options: UseUploadOptions): UseUploadReturn {\n const [state, setState] = useState<UseUploadState>(INITIAL_STATE);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const abortRef = useRef<AbortController | null>(null);\n\n const upload = useCallback(async (file: File, objectKey: string) => {\n setState({\n ...INITIAL_STATE,\n phase: \"validating\",\n fileName: file.name,\n fileSize: file.size,\n });\n const opts = optionsRef.current;\n\n const validationError = validateFile(file, {\n accept: opts.accept,\n maxFileSize: opts.maxFileSize,\n });\n if (validationError) {\n setState((s) => ({ ...s, phase: \"error\", error: validationError }));\n opts.onError?.(file, new Error(validationError), \"validating\");\n return;\n }\n\n if (opts.beforeUpload) {\n const allowed = await opts.beforeUpload(file);\n if (!allowed) {\n setState((s) => ({\n ...s,\n phase: \"error\",\n error: \"Upload blocked by beforeUpload hook\",\n }));\n opts.onError?.(file, new Error(\"blocked\"), \"validating\");\n return;\n }\n }\n\n setState((s) => ({ ...s, phase: \"uploading\" }));\n opts.onUploadStart?.(file, objectKey);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n try {\n const result = await uploadFile(\n opts.presignApi,\n file,\n objectKey,\n {\n multipart: opts.multipart,\n multipartThreshold: opts.multipartThreshold,\n concurrentParts: opts.concurrentParts,\n },\n {\n onProgress: (progress) => {\n setState((s) => ({ ...s, progress }));\n opts.onProgress?.(file, progress);\n },\n },\n controller.signal,\n );\n\n setState((s) => ({\n ...s,\n phase: \"success\",\n result,\n progress: { loaded: file.size, total: file.size, percent: 100 },\n }));\n await opts.onSuccess?.(file, result);\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n opts.onCancel?.(file);\n setState(INITIAL_STATE);\n return;\n }\n const message = err instanceof Error ? err.message : \"Upload failed\";\n setState((s) => ({ ...s, phase: \"error\", error: message }));\n opts.onError?.(file, err, \"uploading\");\n } finally {\n abortRef.current = null;\n }\n }, []);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n const reset = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n return { ...state, upload, cancel, reset };\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type {\n PresignApi,\n UploadConfig,\n UploadProgress,\n UploadResult,\n MultiUploadPhase,\n MultiUploadFileState,\n MultiUploadHooks,\n} from \"@better-s3/core\";\nimport { uploadFiles, validateFile } from \"@better-s3/core\";\n\nexport type UseMultiUploadOptions = UploadConfig &\n MultiUploadHooks & {\n presignApi: PresignApi;\n };\n\nexport type UseMultiUploadState = {\n phase: MultiUploadPhase;\n files: MultiUploadFileState[];\n totalProgress: UploadProgress;\n error: string | null;\n};\n\nexport type UseMultiUploadReturn = UseMultiUploadState & {\n upload: (files: File[], resolveKey: (file: File) => string) => Promise<void>;\n cancel: () => void;\n reset: () => void;\n};\n\nconst INITIAL_PROGRESS: UploadProgress = { loaded: 0, total: 0, percent: 0 };\n\nconst INITIAL_STATE: UseMultiUploadState = {\n phase: \"idle\",\n files: [],\n totalProgress: INITIAL_PROGRESS,\n error: null,\n};\n\nlet nextId = 0;\nfunction generateId() {\n return `file-${++nextId}`;\n}\n\nexport function useMultiUpload(\n options: UseMultiUploadOptions,\n): UseMultiUploadReturn {\n const [state, setState] = useState<UseMultiUploadState>(INITIAL_STATE);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const abortRef = useRef<AbortController | null>(null);\n const fileMapRef = useRef<Map<string, File>>(new Map());\n\n const upload = useCallback(\n async (files: File[], resolveKey: (file: File) => string) => {\n const opts = optionsRef.current;\n\n const items: Array<{\n id: string;\n file: File;\n objectKey: string;\n }> = [];\n const fileStates: MultiUploadFileState[] = [];\n const fileMap = new Map<string, File>();\n\n setState((s) => ({ ...s, phase: \"validating\", error: null }));\n\n if (opts.maxFiles && files.length > opts.maxFiles) {\n const msg = `Too many files. Maximum is ${opts.maxFiles}.`;\n setState((s) => ({ ...s, phase: \"error\", error: msg }));\n opts.onError?.(new Error(msg));\n return;\n }\n\n for (const file of files) {\n const validationError = validateFile(file, {\n accept: opts.accept,\n maxFileSize: opts.maxFileSize,\n });\n if (validationError) {\n const msg = `${file.name}: ${validationError}`;\n setState((s) => ({ ...s, phase: \"error\", error: msg }));\n opts.onError?.(new Error(msg));\n return;\n }\n }\n\n if (opts.beforeUpload) {\n const allowed = await opts.beforeUpload(files);\n if (!allowed) {\n setState((s) => ({\n ...s,\n phase: \"error\",\n error: \"Upload blocked by beforeUpload hook\",\n }));\n opts.onError?.(new Error(\"blocked\"));\n return;\n }\n }\n\n for (const file of files) {\n const id = generateId();\n const objectKey = resolveKey(file);\n items.push({ id, file, objectKey });\n fileMap.set(id, file);\n fileStates.push({\n id,\n fileName: file.name,\n fileSize: file.size,\n status: \"pending\",\n progress: { loaded: 0, total: file.size, percent: 0 },\n error: null,\n });\n }\n\n fileMapRef.current = fileMap;\n\n setState({\n phase: \"uploading\",\n files: fileStates,\n totalProgress: {\n loaded: 0,\n total: files.reduce((s, f) => s + f.size, 0),\n percent: 0,\n },\n error: null,\n });\n\n opts.onUploadStart?.(files);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n try {\n const results = await uploadFiles(\n opts.presignApi,\n items,\n {\n multipart: opts.multipart,\n multipartThreshold: opts.multipartThreshold,\n concurrentParts: opts.concurrentParts,\n concurrentFiles: opts.concurrentFiles,\n },\n {\n onFileProgress: (id, progress) => {\n setState((s) => ({\n ...s,\n files: s.files.map((f) =>\n f.id === id ? { ...f, status: \"uploading\", progress } : f,\n ),\n }));\n const file = fileMap.get(id);\n if (file) opts.onFileProgress?.(file, progress);\n },\n onFileSuccess: (id, result) => {\n setState((s) => ({\n ...s,\n files: s.files.map((f) =>\n f.id === id\n ? {\n ...f,\n status: \"success\",\n progress: {\n loaded: f.fileSize,\n total: f.fileSize,\n percent: 100,\n },\n }\n : f,\n ),\n }));\n const file = fileMap.get(id);\n if (file) opts.onFileSuccess?.(file, result);\n },\n onFileError: (id, error) => {\n setState((s) => ({\n ...s,\n files: s.files.map((f) =>\n f.id === id ? { ...f, status: \"error\", error } : f,\n ),\n }));\n const file = fileMap.get(id);\n if (file) opts.onFileError?.(file, error);\n },\n onTotalProgress: (progress) => {\n setState((s) => ({ ...s, totalProgress: progress }));\n opts.onProgress?.(progress);\n },\n },\n controller.signal,\n );\n\n const hasErrors = results.some((r) => r.status === \"error\");\n const successResults = results\n .filter((r) => r.result !== null)\n .map((r) => r.result!);\n\n setState((s) => ({\n ...s,\n phase: hasErrors ? \"error\" : \"success\",\n error: hasErrors\n ? `${results.filter((r) => r.status === \"error\").length} file(s) failed`\n : null,\n totalProgress: hasErrors\n ? s.totalProgress\n : {\n loaded: s.totalProgress.total,\n total: s.totalProgress.total,\n percent: 100,\n },\n }));\n\n if (!hasErrors) {\n await opts.onSuccess?.(successResults);\n }\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n opts.onCancel?.();\n setState(INITIAL_STATE);\n return;\n }\n const message = err instanceof Error ? err.message : \"Upload failed\";\n setState((s) => ({ ...s, phase: \"error\", error: message }));\n opts.onError?.(err);\n } finally {\n abortRef.current = null;\n }\n },\n [],\n );\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n const reset = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n return { ...state, upload, cancel, reset };\n}\n","\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport type { UploadPhase, UploadProgress } from \"@better-s3/core\";\nimport { useUpload, type UseUploadOptions } from \"./use-upload\";\n\nexport type UseUploadControlsOptions = UseUploadOptions & {\n objectKey: string | ((file: File) => string);\n};\n\nexport type UseUploadControlsReturn = {\n /** Current upload phase */\n phase: UploadPhase;\n /** Upload progress (loaded, total, percent) */\n progress: UploadProgress;\n /** Error message if upload failed */\n error: string | null;\n /** Info about the selected file */\n fileInfo: { name: string; size: number } | null;\n /** Whether an upload is currently in progress */\n isUploading: boolean;\n /** Trigger upload from a FileList (e.g. from a file input or drop event) */\n handleFiles: (files: FileList | null) => void;\n /** Open the native file picker dialog */\n openFilePicker: () => void;\n /** Cancel the current upload */\n cancel: () => void;\n /** Reset to idle state */\n reset: () => void;\n /** Spread on a hidden `<input>` element */\n inputProps: {\n ref: React.RefObject<HTMLInputElement | null>;\n type: \"file\";\n accept?: string;\n hidden: true;\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n };\n /** Spread on a container to enable drag-and-drop */\n dropHandlers: {\n onDragOver: (e: React.DragEvent) => void;\n onDrop: (e: React.DragEvent) => void;\n };\n};\n\nexport function useUploadControls(\n options: UseUploadControlsOptions,\n): UseUploadControlsReturn {\n const { objectKey, ...hookOptions } = options;\n const ctx = useUpload(hookOptions);\n const inputRef = useRef<HTMLInputElement>(null);\n const [fileInfo, setFileInfo] = useState<{\n name: string;\n size: number;\n } | null>(null);\n\n const resolveKey = (file: File): string =>\n typeof objectKey === \"function\" ? objectKey(file) : objectKey;\n\n const handleFiles = async (files: FileList | null) => {\n const file = files?.[0];\n if (!file) return;\n setFileInfo({ name: file.name, size: file.size });\n await ctx.upload(file, resolveKey(file));\n };\n\n const openFilePicker = () => inputRef.current?.click();\n\n const isUploading = ctx.phase === \"uploading\";\n\n return {\n phase: ctx.phase,\n progress: ctx.progress,\n error: ctx.error,\n fileInfo,\n isUploading,\n handleFiles,\n openFilePicker,\n cancel: ctx.cancel,\n reset: () => {\n ctx.reset();\n setFileInfo(null);\n },\n inputProps: {\n ref: inputRef,\n type: \"file\",\n accept: hookOptions.accept?.join(\",\"),\n hidden: true,\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => {\n handleFiles(e.target.files);\n e.target.value = \"\";\n },\n },\n dropHandlers: {\n onDragOver: (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n },\n onDrop: (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (!isUploading) handleFiles(e.dataTransfer.files);\n },\n },\n };\n}\n","\"use client\";\n\nimport { useRef } from \"react\";\nimport type {\n UploadProgress,\n MultiUploadPhase,\n MultiUploadFileState,\n} from \"@better-s3/core\";\nimport { useMultiUpload, type UseMultiUploadOptions } from \"./use-multi-upload\";\n\nexport type UseMultiUploadControlsOptions = UseMultiUploadOptions & {\n objectKey: (file: File) => string;\n};\n\nexport type UseMultiUploadControlsReturn = {\n /** Current upload phase */\n phase: MultiUploadPhase;\n /** Per-file upload state */\n files: MultiUploadFileState[];\n /** Aggregated progress across all files */\n totalProgress: UploadProgress;\n /** Error message if upload failed */\n error: string | null;\n /** Whether an upload is currently in progress */\n isUploading: boolean;\n /** Trigger upload from a FileList (e.g. from a file input or drop event) */\n handleFiles: (files: FileList | null) => void;\n /** Open the native file picker dialog */\n openFilePicker: () => void;\n /** Cancel all uploads */\n cancel: () => void;\n /** Reset to idle state */\n reset: () => void;\n /** Spread on a hidden `<input>` element */\n inputProps: {\n ref: React.RefObject<HTMLInputElement | null>;\n type: \"file\";\n multiple: true;\n accept?: string;\n hidden: true;\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n };\n /** Spread on a container to enable drag-and-drop */\n dropHandlers: {\n onDragOver: (e: React.DragEvent) => void;\n onDrop: (e: React.DragEvent) => void;\n };\n};\n\nexport function useMultiUploadControls(\n options: UseMultiUploadControlsOptions,\n): UseMultiUploadControlsReturn {\n const { objectKey, ...hookOptions } = options;\n const ctx = useMultiUpload(hookOptions);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleFiles = async (files: FileList | null) => {\n if (!files?.length) return;\n await ctx.upload(Array.from(files), objectKey);\n };\n\n const openFilePicker = () => inputRef.current?.click();\n\n const isUploading = ctx.phase === \"uploading\";\n\n return {\n phase: ctx.phase,\n files: ctx.files,\n totalProgress: ctx.totalProgress,\n error: ctx.error,\n isUploading,\n handleFiles,\n openFilePicker,\n cancel: ctx.cancel,\n reset: ctx.reset,\n inputProps: {\n ref: inputRef,\n type: \"file\",\n multiple: true,\n accept: hookOptions.accept?.join(\",\"),\n hidden: true,\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => {\n handleFiles(e.target.files);\n e.target.value = \"\";\n },\n },\n dropHandlers: {\n onDragOver: (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n },\n onDrop: (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (!isUploading) handleFiles(e.dataTransfer.files);\n },\n },\n };\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type {\n PresignApi,\n DownloadPhase,\n DownloadProgress,\n DownloadHooks,\n} from \"@better-s3/core\";\n\nexport type UseDownloadOptions = DownloadHooks & {\n presignApi: PresignApi;\n /**\n * `\"native\"` — browser handles download natively via presigned URL (default)\n * `\"fetch\"` — streams via fetch, enables in-app progress tracking\n */\n mode?: \"native\" | \"fetch\";\n};\n\nexport type UseDownloadState = {\n phase: DownloadPhase;\n progress: DownloadProgress;\n error: string | null;\n fileName: string | null;\n fileSize: number | null;\n};\n\nexport type UseDownloadReturn = UseDownloadState & {\n download: (key: string, downloadName?: string) => Promise<void>;\n cancel: () => void;\n reset: () => void;\n};\n\nconst INITIAL_PROGRESS: DownloadProgress = { loaded: 0, total: 0, percent: 0 };\n\nconst INITIAL_STATE: UseDownloadState = {\n phase: \"idle\",\n progress: INITIAL_PROGRESS,\n error: null,\n fileName: null,\n fileSize: null,\n};\n\nexport function useDownload(options: UseDownloadOptions): UseDownloadReturn {\n const [state, setState] = useState<UseDownloadState>(INITIAL_STATE);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n const abortRef = useRef<AbortController | null>(null);\n\n const download = useCallback(async (key: string, downloadName?: string) => {\n const name = downloadName ?? key.split(\"/\").pop() ?? key;\n const opts = optionsRef.current;\n const mode = opts.mode ?? \"native\";\n\n if (opts.beforeDownload) {\n const allowed = await opts.beforeDownload(key);\n if (!allowed) {\n setState((s) => ({\n ...s,\n phase: \"error\",\n error: \"Download blocked by beforeDownload hook\",\n }));\n opts.onError?.(key, new Error(\"blocked\"), \"presigning\");\n return;\n }\n }\n\n setState({\n phase: \"presigning\",\n progress: INITIAL_PROGRESS,\n error: null,\n fileName: name,\n fileSize: null,\n });\n\n try {\n const { url } = await opts.presignApi.download(key, name);\n\n if (mode === \"native\") {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = name;\n anchor.click();\n setState((s) => ({ ...s, phase: \"success\" }));\n await opts.onSuccess?.(key);\n setState(INITIAL_STATE);\n return;\n }\n\n // ── Fetch mode ───────────────────────────────────────────────\n setState((s) => ({ ...s, phase: \"downloading\" }));\n opts.onDownloadStart?.(key);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n const res = await fetch(url, { signal: controller.signal });\n if (!res.ok) throw new Error(`Download failed: ${res.statusText}`);\n\n const contentLength = Number(res.headers.get(\"content-length\") || 0);\n setState((s) => ({ ...s, fileSize: contentLength || null }));\n\n const reader = res.body?.getReader();\n if (!reader) throw new Error(\"ReadableStream not supported\");\n\n const chunks: BlobPart[] = [];\n let loaded = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n loaded += value.byteLength;\n const percent =\n contentLength > 0 ? Math.round((loaded / contentLength) * 100) : 0;\n const progress: DownloadProgress = {\n loaded,\n total: contentLength,\n percent,\n };\n setState((s) => ({ ...s, progress }));\n opts.onProgress?.(key, progress);\n }\n\n const blob = new Blob(chunks);\n const blobUrl = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = blobUrl;\n anchor.download = name;\n anchor.click();\n URL.revokeObjectURL(blobUrl);\n\n setState((s) => ({\n ...s,\n phase: \"success\",\n fileSize: blob.size,\n progress: { loaded: blob.size, total: blob.size, percent: 100 },\n }));\n await opts.onSuccess?.(key);\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n opts.onCancel?.(key);\n setState(INITIAL_STATE);\n return;\n }\n const message = err instanceof Error ? err.message : \"Download failed\";\n setState((s) => ({ ...s, phase: \"error\", error: message }));\n opts.onError?.(key, err, \"downloading\");\n } finally {\n abortRef.current = null;\n }\n }, []);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n const reset = useCallback(() => {\n abortRef.current?.abort();\n setState(INITIAL_STATE);\n }, []);\n\n return { ...state, download, cancel, reset };\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type { PresignApi, DeletePhase, DeleteHooks } from \"@better-s3/core\";\n\nexport type UseDeleteOptions = DeleteHooks & {\n presignApi: PresignApi;\n};\n\nexport type UseDeleteState = {\n phase: DeletePhase;\n error: string | null;\n};\n\nexport type UseDeleteReturn = UseDeleteState & {\n requestDelete: (key: string) => void;\n confirmDelete: () => Promise<void>;\n cancelDelete: () => void;\n reset: () => void;\n pendingKey: string | null;\n};\n\nconst INITIAL_STATE: UseDeleteState = {\n phase: \"idle\",\n error: null,\n};\n\nexport function useDelete(options: UseDeleteOptions): UseDeleteReturn {\n const [state, setState] = useState<UseDeleteState>(INITIAL_STATE);\n const [pendingKey, setPendingKey] = useState<string | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const requestDelete = useCallback((key: string) => {\n setPendingKey(key);\n setState({ phase: \"confirming\", error: null });\n }, []);\n\n const confirmDelete = useCallback(async () => {\n if (!pendingKey) return;\n const opts = optionsRef.current;\n\n if (opts.beforeDelete) {\n const allowed = await opts.beforeDelete(pendingKey);\n if (!allowed) {\n setState({\n phase: \"error\",\n error: \"Delete blocked by beforeDelete hook\",\n });\n opts.onError?.(pendingKey, new Error(\"blocked\"), \"confirming\");\n setPendingKey(null);\n return;\n }\n }\n\n setState({ phase: \"deleting\", error: null });\n opts.onDeleteStart?.(pendingKey);\n\n try {\n await opts.presignApi.delete(pendingKey);\n\n setState({ phase: \"success\", error: null });\n await opts.onSuccess?.(pendingKey);\n setPendingKey(null);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Delete failed\";\n setState({ phase: \"error\", error: message });\n opts.onError?.(pendingKey, err, \"deleting\");\n }\n }, [pendingKey]);\n\n const cancelDelete = useCallback(() => {\n setPendingKey(null);\n setState(INITIAL_STATE);\n }, []);\n\n const reset = useCallback(() => {\n setPendingKey(null);\n setState(INITIAL_STATE);\n }, []);\n\n return {\n ...state,\n pendingKey,\n requestDelete,\n confirmDelete,\n cancelDelete,\n reset,\n };\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { PresignApi, DeletePhase, DeleteHooks } from "@better-s3/core";
2
+ export type UseDeleteOptions = DeleteHooks & {
3
+ presignApi: PresignApi;
4
+ };
5
+ export type UseDeleteState = {
6
+ phase: DeletePhase;
7
+ error: string | null;
8
+ };
9
+ export type UseDeleteReturn = UseDeleteState & {
10
+ requestDelete: (key: string) => void;
11
+ confirmDelete: () => Promise<void>;
12
+ cancelDelete: () => void;
13
+ reset: () => void;
14
+ pendingKey: string | null;
15
+ };
16
+ export declare function useDelete(options: UseDeleteOptions): UseDeleteReturn;
@@ -0,0 +1,22 @@
1
+ import type { PresignApi, DownloadPhase, DownloadProgress, DownloadHooks } from "@better-s3/core";
2
+ export type UseDownloadOptions = DownloadHooks & {
3
+ presignApi: PresignApi;
4
+ /**
5
+ * `"native"` — browser handles download natively via presigned URL (default)
6
+ * `"fetch"` — streams via fetch, enables in-app progress tracking
7
+ */
8
+ mode?: "native" | "fetch";
9
+ };
10
+ export type UseDownloadState = {
11
+ phase: DownloadPhase;
12
+ progress: DownloadProgress;
13
+ error: string | null;
14
+ fileName: string | null;
15
+ fileSize: number | null;
16
+ };
17
+ export type UseDownloadReturn = UseDownloadState & {
18
+ download: (key: string, downloadName?: string) => Promise<void>;
19
+ cancel: () => void;
20
+ reset: () => void;
21
+ };
22
+ export declare function useDownload(options: UseDownloadOptions): UseDownloadReturn;
@@ -0,0 +1,40 @@
1
+ import type { UploadProgress, MultiUploadPhase, MultiUploadFileState } from "@better-s3/core";
2
+ import { type UseMultiUploadOptions } from "./use-multi-upload";
3
+ export type UseMultiUploadControlsOptions = UseMultiUploadOptions & {
4
+ objectKey: (file: File) => string;
5
+ };
6
+ export type UseMultiUploadControlsReturn = {
7
+ /** Current upload phase */
8
+ phase: MultiUploadPhase;
9
+ /** Per-file upload state */
10
+ files: MultiUploadFileState[];
11
+ /** Aggregated progress across all files */
12
+ totalProgress: UploadProgress;
13
+ /** Error message if upload failed */
14
+ error: string | null;
15
+ /** Whether an upload is currently in progress */
16
+ isUploading: boolean;
17
+ /** Trigger upload from a FileList (e.g. from a file input or drop event) */
18
+ handleFiles: (files: FileList | null) => void;
19
+ /** Open the native file picker dialog */
20
+ openFilePicker: () => void;
21
+ /** Cancel all uploads */
22
+ cancel: () => void;
23
+ /** Reset to idle state */
24
+ reset: () => void;
25
+ /** Spread on a hidden `<input>` element */
26
+ inputProps: {
27
+ ref: React.RefObject<HTMLInputElement | null>;
28
+ type: "file";
29
+ multiple: true;
30
+ accept?: string;
31
+ hidden: true;
32
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
33
+ };
34
+ /** Spread on a container to enable drag-and-drop */
35
+ dropHandlers: {
36
+ onDragOver: (e: React.DragEvent) => void;
37
+ onDrop: (e: React.DragEvent) => void;
38
+ };
39
+ };
40
+ export declare function useMultiUploadControls(options: UseMultiUploadControlsOptions): UseMultiUploadControlsReturn;
@@ -0,0 +1,16 @@
1
+ import type { PresignApi, UploadConfig, UploadProgress, MultiUploadPhase, MultiUploadFileState, MultiUploadHooks } from "@better-s3/core";
2
+ export type UseMultiUploadOptions = UploadConfig & MultiUploadHooks & {
3
+ presignApi: PresignApi;
4
+ };
5
+ export type UseMultiUploadState = {
6
+ phase: MultiUploadPhase;
7
+ files: MultiUploadFileState[];
8
+ totalProgress: UploadProgress;
9
+ error: string | null;
10
+ };
11
+ export type UseMultiUploadReturn = UseMultiUploadState & {
12
+ upload: (files: File[], resolveKey: (file: File) => string) => Promise<void>;
13
+ cancel: () => void;
14
+ reset: () => void;
15
+ };
16
+ export declare function useMultiUpload(options: UseMultiUploadOptions): UseMultiUploadReturn;
@@ -0,0 +1,42 @@
1
+ import type { UploadPhase, UploadProgress } from "@better-s3/core";
2
+ import { type UseUploadOptions } from "./use-upload";
3
+ export type UseUploadControlsOptions = UseUploadOptions & {
4
+ objectKey: string | ((file: File) => string);
5
+ };
6
+ export type UseUploadControlsReturn = {
7
+ /** Current upload phase */
8
+ phase: UploadPhase;
9
+ /** Upload progress (loaded, total, percent) */
10
+ progress: UploadProgress;
11
+ /** Error message if upload failed */
12
+ error: string | null;
13
+ /** Info about the selected file */
14
+ fileInfo: {
15
+ name: string;
16
+ size: number;
17
+ } | null;
18
+ /** Whether an upload is currently in progress */
19
+ isUploading: boolean;
20
+ /** Trigger upload from a FileList (e.g. from a file input or drop event) */
21
+ handleFiles: (files: FileList | null) => void;
22
+ /** Open the native file picker dialog */
23
+ openFilePicker: () => void;
24
+ /** Cancel the current upload */
25
+ cancel: () => void;
26
+ /** Reset to idle state */
27
+ reset: () => void;
28
+ /** Spread on a hidden `<input>` element */
29
+ inputProps: {
30
+ ref: React.RefObject<HTMLInputElement | null>;
31
+ type: "file";
32
+ accept?: string;
33
+ hidden: true;
34
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
35
+ };
36
+ /** Spread on a container to enable drag-and-drop */
37
+ dropHandlers: {
38
+ onDragOver: (e: React.DragEvent) => void;
39
+ onDrop: (e: React.DragEvent) => void;
40
+ };
41
+ };
42
+ export declare function useUploadControls(options: UseUploadControlsOptions): UseUploadControlsReturn;
@@ -0,0 +1,18 @@
1
+ import type { PresignApi, UploadConfig, UploadHooks, UploadPhase, UploadProgress, UploadResult } from "@better-s3/core";
2
+ export type UseUploadOptions = UploadConfig & UploadHooks & {
3
+ presignApi: PresignApi;
4
+ };
5
+ export type UseUploadState = {
6
+ phase: UploadPhase;
7
+ progress: UploadProgress;
8
+ error: string | null;
9
+ result: UploadResult | null;
10
+ fileName: string | null;
11
+ fileSize: number | null;
12
+ };
13
+ export type UseUploadReturn = UseUploadState & {
14
+ upload: (file: File, objectKey: string) => Promise<void>;
15
+ cancel: () => void;
16
+ reset: () => void;
17
+ };
18
+ export declare function useUpload(options: UseUploadOptions): UseUploadReturn;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@better-s3/react",
3
+ "version": "1.0.0",
4
+ "description": "React hooks for S3-compatible file uploads, downloads, and deletes",
5
+ "keywords": [
6
+ "s3",
7
+ "react",
8
+ "hooks",
9
+ "upload",
10
+ "download",
11
+ "file-upload"
12
+ ],
13
+ "author": "Hamidrezakz",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/hamidrezakz/better-s3.git",
18
+ "directory": "packages/better-s3-react"
19
+ },
20
+ "homepage": "https://github.com/hamidrezakz/better-s3/tree/master/packages/better-s3-react",
21
+ "bugs": {
22
+ "url": "https://github.com/hamidrezakz/better-s3/issues"
23
+ },
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "dependencies": {
36
+ "@better-s3/core": "1.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "react": ">=18"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react": "^19.0.0",
43
+ "react": "^19.0.0",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^5.8.3"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup --config tsup.config.ts && tsc --emitDeclarationOnly",
52
+ "check-types": "tsc --noEmit"
53
+ }
54
+ }