@uploadista/react 0.0.14 → 0.0.15-beta.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/dist/components/index.mjs +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{upload-zone-8khVLLYF.mjs → upload-zone-D3gie3s1.mjs} +2 -2
- package/dist/{upload-zone-8khVLLYF.mjs.map → upload-zone-D3gie3s1.mjs.map} +1 -1
- package/dist/{use-upload-xcqz090n.mjs → use-upload-CbeBq3kV.mjs} +2 -2
- package/dist/{use-upload-xcqz090n.mjs.map → use-upload-CbeBq3kV.mjs.map} +1 -1
- package/dist/{use-upload-metrics-BE7UcAaz.mjs → use-upload-metrics-dMP4qxq0.mjs} +2 -2
- package/dist/{use-upload-metrics-BE7UcAaz.mjs.map → use-upload-metrics-dMP4qxq0.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/hooks/use-flow-upload.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,s as t}from"../use-upload-
|
|
1
|
+
import{o as e,s as t}from"../use-upload-CbeBq3kV.mjs";import{a as n,c as r,i,l as a,n as o,o as s,r as c,s as l,t as u}from"../upload-zone-D3gie3s1.mjs";export{l as FlowUploadList,n as FlowUploadZone,r as SimpleFlowUploadList,a as SimpleFlowUploadListItem,s as SimpleFlowUploadZone,c as SimpleUploadListItem,u as SimpleUploadZone,i as UploadList,o as UploadZone,e as UploadistaProvider,t as useUploadistaContext};
|
package/dist/hooks/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,i as n,n as r,r as i,t as a}from"../use-upload-
|
|
1
|
+
import{a as e,c as t,i as n,n as r,r as i,t as a}from"../use-upload-CbeBq3kV.mjs";import{t as o}from"../use-upload-metrics-dMP4qxq0.mjs";export{n as useDragDrop,i as useFlowUpload,e as useMultiFlowUpload,r as useMultiUpload,a as useUpload,o as useUploadMetrics,t as useUploadistaClient};
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./use-upload-
|
|
1
|
+
import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./use-upload-CbeBq3kV.mjs";import{a as c,c as l,i as u,l as d,n as f,o as p,r as m,s as h,t as g}from"./upload-zone-D3gie3s1.mjs";import{t as _}from"./use-upload-metrics-dMP4qxq0.mjs";export{h as FlowUploadList,c as FlowUploadZone,l as SimpleFlowUploadList,d as SimpleFlowUploadListItem,p as SimpleFlowUploadZone,m as SimpleUploadListItem,g as SimpleUploadZone,u as UploadList,f as UploadZone,i as UploadistaProvider,n as useDragDrop,a as useFlowUpload,e as useMultiFlowUpload,r as useMultiUpload,s as useUpload,_ as useUploadMetrics,t as useUploadistaClient,o as useUploadistaContext};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,i as t,n,r,t as i}from"./use-upload-xcqz090n.mjs";import{useCallback as a}from"react";import{Fragment as o,jsx as s,jsxs as c}from"react/jsx-runtime";function l({flowConfig:t,options:n,children:r}){let i=e({...n,flowConfig:t});return s(o,{children:r({items:i.state.items,totalProgress:i.state.totalProgress,activeUploads:i.state.activeUploads,completedUploads:i.state.completedUploads,failedUploads:i.state.failedUploads,isUploading:i.isUploading,addFiles:i.addFiles,removeFile:i.removeFile,startUpload:i.startUpload,abortUpload:i.abortUpload,abortAll:i.abortAll,clear:i.clear,retryUpload:i.retryUpload})})}function u({item:e,onAbort:t,onRetry:n,onRemove:r}){return c(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,padding:`8px`,borderBottom:`1px solid #eee`},children:[s(`span`,{style:{color:(()=>{switch(e.status){case`success`:return`green`;case`error`:return`red`;case`uploading`:return`blue`;case`aborted`:return`gray`;default:return`black`}})(),fontSize:`18px`},children:(()=>{switch(e.status){case`success`:return`✓`;case`error`:return`✗`;case`uploading`:return`⟳`;case`aborted`:return`⊘`;default:return`○`}})()}),c(`div`,{style:{flex:1,minWidth:0},children:[s(`div`,{style:{fontSize:`14px`,fontWeight:500,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},children:e.file instanceof File?e.file.name:`Upload`}),e.status===`uploading`&&c(`div`,{style:{marginTop:`4px`},children:[s(`progress`,{value:e.progress,max:100,style:{width:`100%`,height:`4px`}}),c(`div`,{style:{fontSize:`12px`,color:`#666`,marginTop:`2px`},children:[e.progress,`% • `,Math.round(e.bytesUploaded/1024),` KB /`,` `,Math.round(e.totalBytes/1024),` KB`]})]}),e.status===`error`&&s(`div`,{style:{fontSize:`12px`,color:`red`,marginTop:`2px`},children:e.error?.message||`Upload failed`}),e.status===`success`&&s(`div`,{style:{fontSize:`12px`,color:`green`,marginTop:`2px`},children:`Upload complete`})]}),c(`div`,{style:{display:`flex`,gap:`8px`},children:[e.status===`uploading`&&s(`button`,{type:`button`,onClick:t,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Cancel`}),e.status===`error`&&s(`button`,{type:`button`,onClick:n,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Retry`}),(e.status===`pending`||e.status===`error`||e.status===`aborted`)&&s(`button`,{type:`button`,onClick:r,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Remove`})]})]})}function d({flowConfig:e,options:t,className:n=``,showFileInput:r=!0,accept:i}){return s(l,{flowConfig:e,options:t,children:({items:e,addFiles:t,startUpload:a,abortUpload:o,retryUpload:l,removeFile:d,totalProgress:f})=>c(`div`,{className:n,children:[r&&s(`div`,{style:{marginBottom:`16px`},children:s(`input`,{type:`file`,multiple:!0,accept:i,onChange:e=>{e.target.files&&(t(e.target.files),a())},style:{padding:`8px`,border:`1px solid #ccc`,borderRadius:`4px`}})}),e.length>0&&c(`div`,{children:[c(`div`,{style:{marginBottom:`8px`,fontSize:`14px`,color:`#666`},children:[`Total Progress: `,f,`%`]}),s(`div`,{style:{border:`1px solid #eee`,borderRadius:`8px`,overflow:`hidden`},children:e.map(e=>s(u,{item:e,onAbort:()=>o(e.id),onRetry:()=>l(e.id),onRemove:()=>d(e.id)},e.id))})]})]})})}function f({flowConfig:e,options:n,accept:i,multiple:a=!1,children:c}){let l=r({...n,flowConfig:e}),u=t({onFilesReceived:e=>{let t=e[0];t&&l.upload(t)},accept:i?[i]:void 0,multiple:a}),d=e=>{let t=e.target.files?.[0];t&&l.upload(t)};return s(o,{children:c({flowUpload:l,dragDrop:u,isActive:u.state.isDragging||u.state.isOver,openFilePicker:u.openFilePicker,getRootProps:()=>u.dragHandlers,getInputProps:()=>({...u.inputProps,onChange:d})})})}function p({flowConfig:e,options:t,accept:n,className:r=``,dragText:i=`Drop files here`,idleText:a=`Drag & drop files or click to browse`}){return s(f,{flowConfig:e,options:t,accept:n,children:({dragDrop:e,flowUpload:t,getRootProps:n,getInputProps:o,openFilePicker:l})=>c(`div`,{...n(),className:r,style:{border:`2px dashed #ccc`,borderRadius:`8px`,padding:`32px`,textAlign:`center`,cursor:`pointer`,backgroundColor:e.state.isDragging?`#f0f0f0`:`transparent`,transition:`background-color 0.2s`},children:[s(`input`,{...o()}),e.state.isDragging&&s(`p`,{style:{margin:0},children:i}),!e.state.isDragging&&!t.isUploading&&t.state.status===`idle`&&c(`div`,{children:[s(`p`,{style:{margin:`0 0 16px 0`},children:a}),s(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),l()},style:{padding:`8px 16px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Choose Files`})]}),t.isUploading&&c(`div`,{children:[s(`progress`,{value:t.state.progress,max:100,style:{width:`100%`,height:`8px`}}),c(`p`,{style:{margin:`8px 0 0 0`},children:[t.state.progress,`%`]}),s(`button`,{type:`button`,onClick:e=>{e.stopPropagation()},style:{marginTop:`8px`,padding:`4px 12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Cancel`})]}),t.state.status===`success`&&s(`div`,{children:s(`p`,{style:{margin:0,color:`green`},children:`✓ Upload complete!`})}),t.state.status===`error`&&s(`div`,{children:c(`p`,{style:{margin:0,color:`red`},children:[`✗ Error: `,t.state.error?.message]})})]})})}function m({multiUpload:e,filter:t,sortBy:n,children:r}){let i=e.items;t&&(i=i.filter(t)),n&&(i=[...i].sort(n));let a={idle:i.filter(e=>e.state.status===`idle`),uploading:i.filter(e=>e.state.status===`uploading`),success:i.filter(e=>e.state.status===`success`),error:i.filter(e=>e.state.status===`error`),aborted:i.filter(e=>e.state.status===`aborted`)};return s(o,{children:r({items:i,itemsByStatus:a,multiUpload:e,actions:{removeItem:t=>{e.removeItem(t)},retryItem:t=>{e.retryFailed()},abortItem:t=>{e.removeItem(t.id)},startItem:t=>{e.startAll()}}})})}function h({item:e,actions:t,className:n=``,style:r={},showDetails:i=!0}){let a=e=>{switch(e){case`idle`:return`#6c757d`;case`uploading`:return`#007bff`;case`success`:return`#28a745`;case`error`:return`#dc3545`;case`aborted`:return`#6c757d`;default:return`#6c757d`}},l=e=>{switch(e){case`idle`:return`⏳`;case`uploading`:return`📤`;case`success`:return`✅`;case`error`:return`❌`;case`aborted`:return`⏹️`;default:return`❓`}},u=e=>{if(e===0)return`0 Bytes`;let t=1024,n=[`Bytes`,`KB`,`MB`,`GB`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(2))} ${n[r]}`};return c(`div`,{className:`upload-list-item upload-list-item--${e.state.status} ${n}`,style:{padding:`12px`,border:`1px solid #e0e0e0`,borderRadius:`6px`,marginBottom:`8px`,backgroundColor:`#fff`,transition:`all 0.2s ease`,...r},children:[c(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:`8px`},children:[c(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flex:1},children:[s(`span`,{style:{fontSize:`16px`},children:l(e.state.status)}),s(`span`,{style:{fontWeight:`500`,flex:1},children:e.file instanceof File?e.file.name:`File`})]}),s(`span`,{style:{fontSize:`12px`,color:a(e.state.status),fontWeight:`500`,textTransform:`uppercase`},children:e.state.status})]}),e.state.status===`uploading`&&c(`div`,{style:{marginBottom:`8px`},children:[c(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:`4px`},children:[c(`span`,{style:{fontSize:`12px`,color:`#666`},children:[e.state.progress,`%`]}),i&&e.state.totalBytes&&c(`span`,{style:{fontSize:`12px`,color:`#666`},children:[u(e.state.bytesUploaded),` /`,` `,u(e.state.totalBytes)]})]}),s(`div`,{style:{width:`100%`,height:`6px`,backgroundColor:`#e0e0e0`,borderRadius:`3px`,overflow:`hidden`},children:s(`div`,{style:{width:`${e.state.progress}%`,height:`100%`,backgroundColor:`#007bff`,transition:`width 0.2s ease`}})})]}),i&&c(`div`,{style:{fontSize:`12px`,color:`#666`,marginBottom:`8px`},children:[e.state.totalBytes&&s(`span`,{children:u(e.state.totalBytes)}),e.state.status===`uploading`&&e.state.progress>0&&c(`span`,{children:[` • Progress: `,e.state.progress,`%`]}),e.state.status===`error`&&e.state.error&&s(`div`,{style:{color:`#dc3545`,marginTop:`4px`},children:e.state.error.message})]}),c(`div`,{style:{display:`flex`,gap:`8px`,flexWrap:`wrap`},children:[e.state.status===`idle`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.startItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #007bff`,backgroundColor:`#007bff`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Start`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]}),e.state.status===`uploading`&&s(`button`,{type:`button`,onClick:()=>t.abortItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #dc3545`,backgroundColor:`transparent`,color:`#dc3545`,borderRadius:`4px`,cursor:`pointer`},children:`Cancel`}),e.state.status===`error`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.retryItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #28a745`,backgroundColor:`#28a745`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Retry`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]}),e.state.status===`success`&&s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`}),e.state.status===`aborted`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.retryItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #007bff`,backgroundColor:`#007bff`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Retry`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]})]})]})}function g({children:e,multiple:r=!0,multiUploadOptions:c={},uploadOptions:l={},onUploadStart:u,onValidationError:d,...f}){let p=i(l),m=n(c),h=a(e=>{let t=[];if(!r&&e.length>1&&t.push(`Single file mode is enabled. Please select only one file. You selected ${e.length} files.`),f.accept&&f.accept.length>0){let n=e.filter(e=>!f.accept?.some(t=>{if(t.startsWith(`.`))return e.name.toLowerCase().endsWith(t.toLowerCase());if(t.endsWith(`/*`)){let n=t.slice(0,-2);return e.type.startsWith(n)}else return e.type===t}));if(n.length>0){let e=n.map(e=>`"${e.name}" (${e.type})`).join(`, `),r=f.accept.join(`, `);t.push(`Invalid file type(s): ${e}. Accepted types: ${r}.`)}}return t.length>0?t:null},[r,f.accept]),g=e=>{u?.(e),r&&m?(m.addFiles(e),setTimeout(()=>m.startAll(),0)):!r&&p&&e.length>0&&e[0]&&p.upload(e[0])},_=a(e=>{console.error(`Upload zone validation errors:`,e),d?.(e)},[d]),v=t({...f,multiple:r,validator:h,onFilesReceived:g,onValidationError:_}),y=v.state.isDragging||v.state.isOver,b=r?m?.state.isUploading??!1:p?.isUploading??!1;return s(o,{children:e({dragDrop:v,upload:p,multiUpload:m,openFilePicker:v.openFilePicker,isActive:y,isProcessing:b})})}function _({className:e=``,style:t={},text:n={},errorStyle:r={},children:i,...a}){let o={idle:a.multiple?`Drag files here or click to select`:`Drag a file here or click to select`,dragging:a.multiple?`Drop files here...`:`Drop file here...`,uploading:`Uploading...`,...n};return i?s(g,{...a,children:i}):s(g,{...a,children:({dragDrop:n,upload:i,multiUpload:a,openFilePicker:l,isActive:u,isProcessing:d})=>c(`button`,{type:`button`,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&l()},onKeyUp:e=>{(e.key===`Enter`||e.key===` `)&&l()},...n.dragHandlers,onClick:l,className:`upload-zone ${u?`upload-zone--active`:``} ${d?`upload-zone--processing`:``} ${e}`,style:{border:u?`2px dashed #007bff`:`2px dashed #ccc`,borderRadius:`8px`,padding:`2rem`,textAlign:`center`,cursor:`pointer`,backgroundColor:u?`#f8f9fa`:`transparent`,transition:`all 0.2s ease`,minHeight:`120px`,display:`flex`,flexDirection:`column`,alignItems:`center`,justifyContent:`center`,...t},children:[n.state.isDragging?s(`p`,{style:{margin:0,fontSize:`16px`,color:`#007bff`},children:o.dragging}):d?c(`div`,{style:{textAlign:`center`},children:[s(`p`,{style:{margin:`0 0 10px 0`,fontSize:`14px`},children:o.uploading}),i&&c(`div`,{children:[s(`progress`,{value:i.state.progress,max:100,style:{width:`200px`,height:`8px`}}),c(`p`,{style:{margin:`5px 0 0 0`,fontSize:`12px`,color:`#666`},children:[i.state.progress,`%`]})]}),a&&c(`div`,{children:[s(`progress`,{value:a.state.progress,max:100,style:{width:`200px`,height:`8px`}}),c(`p`,{style:{margin:`5px 0 0 0`,fontSize:`12px`,color:`#666`},children:[a.state.progress,`% (`,a.state.uploading,` `,`uploading, `,a.state.successful,` completed)`]})]})]}):s(`p`,{style:{margin:0,fontSize:`16px`,color:`#666`},children:o.idle}),n.state.errors.length>0&&c(`div`,{style:{marginTop:`10px`,padding:`8px 12px`,backgroundColor:`#f8d7da`,border:`1px solid #f5c6cb`,borderRadius:`4px`,maxWidth:`100%`,...r},children:[s(`p`,{style:{margin:`0 0 5px 0`,fontSize:`12px`,fontWeight:`bold`,color:`#721c24`},children:`Validation Errors:`}),n.state.errors.map((e,t)=>c(`p`,{style:{color:`#721c24`,fontSize:`11px`,margin:`2px 0`,lineHeight:`1.3`},children:[`• `,e]},t))]}),s(`input`,{...n.inputProps})]})})}export{f as a,d as c,m as i,u as l,g as n,p as o,h as r,l as s,_ as t};
|
|
2
|
-
//# sourceMappingURL=upload-zone-
|
|
1
|
+
import{a as e,i as t,n,r,t as i}from"./use-upload-CbeBq3kV.mjs";import{useCallback as a}from"react";import{Fragment as o,jsx as s,jsxs as c}from"react/jsx-runtime";function l({flowConfig:t,options:n,children:r}){let i=e({...n,flowConfig:t});return s(o,{children:r({items:i.state.items,totalProgress:i.state.totalProgress,activeUploads:i.state.activeUploads,completedUploads:i.state.completedUploads,failedUploads:i.state.failedUploads,isUploading:i.isUploading,addFiles:i.addFiles,removeFile:i.removeFile,startUpload:i.startUpload,abortUpload:i.abortUpload,abortAll:i.abortAll,clear:i.clear,retryUpload:i.retryUpload})})}function u({item:e,onAbort:t,onRetry:n,onRemove:r}){return c(`div`,{style:{display:`flex`,alignItems:`center`,gap:`12px`,padding:`8px`,borderBottom:`1px solid #eee`},children:[s(`span`,{style:{color:(()=>{switch(e.status){case`success`:return`green`;case`error`:return`red`;case`uploading`:return`blue`;case`aborted`:return`gray`;default:return`black`}})(),fontSize:`18px`},children:(()=>{switch(e.status){case`success`:return`✓`;case`error`:return`✗`;case`uploading`:return`⟳`;case`aborted`:return`⊘`;default:return`○`}})()}),c(`div`,{style:{flex:1,minWidth:0},children:[s(`div`,{style:{fontSize:`14px`,fontWeight:500,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},children:e.file instanceof File?e.file.name:`Upload`}),e.status===`uploading`&&c(`div`,{style:{marginTop:`4px`},children:[s(`progress`,{value:e.progress,max:100,style:{width:`100%`,height:`4px`}}),c(`div`,{style:{fontSize:`12px`,color:`#666`,marginTop:`2px`},children:[e.progress,`% • `,Math.round(e.bytesUploaded/1024),` KB /`,` `,Math.round(e.totalBytes/1024),` KB`]})]}),e.status===`error`&&s(`div`,{style:{fontSize:`12px`,color:`red`,marginTop:`2px`},children:e.error?.message||`Upload failed`}),e.status===`success`&&s(`div`,{style:{fontSize:`12px`,color:`green`,marginTop:`2px`},children:`Upload complete`})]}),c(`div`,{style:{display:`flex`,gap:`8px`},children:[e.status===`uploading`&&s(`button`,{type:`button`,onClick:t,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Cancel`}),e.status===`error`&&s(`button`,{type:`button`,onClick:n,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Retry`}),(e.status===`pending`||e.status===`error`||e.status===`aborted`)&&s(`button`,{type:`button`,onClick:r,style:{padding:`4px 8px`,fontSize:`12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Remove`})]})]})}function d({flowConfig:e,options:t,className:n=``,showFileInput:r=!0,accept:i}){return s(l,{flowConfig:e,options:t,children:({items:e,addFiles:t,startUpload:a,abortUpload:o,retryUpload:l,removeFile:d,totalProgress:f})=>c(`div`,{className:n,children:[r&&s(`div`,{style:{marginBottom:`16px`},children:s(`input`,{type:`file`,multiple:!0,accept:i,onChange:e=>{e.target.files&&(t(e.target.files),a())},style:{padding:`8px`,border:`1px solid #ccc`,borderRadius:`4px`}})}),e.length>0&&c(`div`,{children:[c(`div`,{style:{marginBottom:`8px`,fontSize:`14px`,color:`#666`},children:[`Total Progress: `,f,`%`]}),s(`div`,{style:{border:`1px solid #eee`,borderRadius:`8px`,overflow:`hidden`},children:e.map(e=>s(u,{item:e,onAbort:()=>o(e.id),onRetry:()=>l(e.id),onRemove:()=>d(e.id)},e.id))})]})]})})}function f({flowConfig:e,options:n,accept:i,multiple:a=!1,children:c}){let l=r({...n,flowConfig:e}),u=t({onFilesReceived:e=>{let t=e[0];t&&l.upload(t)},accept:i?[i]:void 0,multiple:a}),d=e=>{let t=e.target.files?.[0];t&&l.upload(t)};return s(o,{children:c({flowUpload:l,dragDrop:u,isActive:u.state.isDragging||u.state.isOver,openFilePicker:u.openFilePicker,getRootProps:()=>u.dragHandlers,getInputProps:()=>({...u.inputProps,onChange:d})})})}function p({flowConfig:e,options:t,accept:n,className:r=``,dragText:i=`Drop files here`,idleText:a=`Drag & drop files or click to browse`}){return s(f,{flowConfig:e,options:t,accept:n,children:({dragDrop:e,flowUpload:t,getRootProps:n,getInputProps:o,openFilePicker:l})=>c(`div`,{...n(),className:r,style:{border:`2px dashed #ccc`,borderRadius:`8px`,padding:`32px`,textAlign:`center`,cursor:`pointer`,backgroundColor:e.state.isDragging?`#f0f0f0`:`transparent`,transition:`background-color 0.2s`},children:[s(`input`,{...o()}),e.state.isDragging&&s(`p`,{style:{margin:0},children:i}),!e.state.isDragging&&!t.isUploading&&t.state.status===`idle`&&c(`div`,{children:[s(`p`,{style:{margin:`0 0 16px 0`},children:a}),s(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),l()},style:{padding:`8px 16px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Choose Files`})]}),t.isUploading&&c(`div`,{children:[s(`progress`,{value:t.state.progress,max:100,style:{width:`100%`,height:`8px`}}),c(`p`,{style:{margin:`8px 0 0 0`},children:[t.state.progress,`%`]}),s(`button`,{type:`button`,onClick:e=>{e.stopPropagation()},style:{marginTop:`8px`,padding:`4px 12px`,borderRadius:`4px`,border:`1px solid #ccc`,backgroundColor:`#fff`,cursor:`pointer`},children:`Cancel`})]}),t.state.status===`success`&&s(`div`,{children:s(`p`,{style:{margin:0,color:`green`},children:`✓ Upload complete!`})}),t.state.status===`error`&&s(`div`,{children:c(`p`,{style:{margin:0,color:`red`},children:[`✗ Error: `,t.state.error?.message]})})]})})}function m({multiUpload:e,filter:t,sortBy:n,children:r}){let i=e.items;t&&(i=i.filter(t)),n&&(i=[...i].sort(n));let a={idle:i.filter(e=>e.state.status===`idle`),uploading:i.filter(e=>e.state.status===`uploading`),success:i.filter(e=>e.state.status===`success`),error:i.filter(e=>e.state.status===`error`),aborted:i.filter(e=>e.state.status===`aborted`)};return s(o,{children:r({items:i,itemsByStatus:a,multiUpload:e,actions:{removeItem:t=>{e.removeItem(t)},retryItem:t=>{e.retryFailed()},abortItem:t=>{e.removeItem(t.id)},startItem:t=>{e.startAll()}}})})}function h({item:e,actions:t,className:n=``,style:r={},showDetails:i=!0}){let a=e=>{switch(e){case`idle`:return`#6c757d`;case`uploading`:return`#007bff`;case`success`:return`#28a745`;case`error`:return`#dc3545`;case`aborted`:return`#6c757d`;default:return`#6c757d`}},l=e=>{switch(e){case`idle`:return`⏳`;case`uploading`:return`📤`;case`success`:return`✅`;case`error`:return`❌`;case`aborted`:return`⏹️`;default:return`❓`}},u=e=>{if(e===0)return`0 Bytes`;let t=1024,n=[`Bytes`,`KB`,`MB`,`GB`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(2))} ${n[r]}`};return c(`div`,{className:`upload-list-item upload-list-item--${e.state.status} ${n}`,style:{padding:`12px`,border:`1px solid #e0e0e0`,borderRadius:`6px`,marginBottom:`8px`,backgroundColor:`#fff`,transition:`all 0.2s ease`,...r},children:[c(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:`8px`},children:[c(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flex:1},children:[s(`span`,{style:{fontSize:`16px`},children:l(e.state.status)}),s(`span`,{style:{fontWeight:`500`,flex:1},children:e.file instanceof File?e.file.name:`File`})]}),s(`span`,{style:{fontSize:`12px`,color:a(e.state.status),fontWeight:`500`,textTransform:`uppercase`},children:e.state.status})]}),e.state.status===`uploading`&&c(`div`,{style:{marginBottom:`8px`},children:[c(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,marginBottom:`4px`},children:[c(`span`,{style:{fontSize:`12px`,color:`#666`},children:[e.state.progress,`%`]}),i&&e.state.totalBytes&&c(`span`,{style:{fontSize:`12px`,color:`#666`},children:[u(e.state.bytesUploaded),` /`,` `,u(e.state.totalBytes)]})]}),s(`div`,{style:{width:`100%`,height:`6px`,backgroundColor:`#e0e0e0`,borderRadius:`3px`,overflow:`hidden`},children:s(`div`,{style:{width:`${e.state.progress}%`,height:`100%`,backgroundColor:`#007bff`,transition:`width 0.2s ease`}})})]}),i&&c(`div`,{style:{fontSize:`12px`,color:`#666`,marginBottom:`8px`},children:[e.state.totalBytes&&s(`span`,{children:u(e.state.totalBytes)}),e.state.status===`uploading`&&e.state.progress>0&&c(`span`,{children:[` • Progress: `,e.state.progress,`%`]}),e.state.status===`error`&&e.state.error&&s(`div`,{style:{color:`#dc3545`,marginTop:`4px`},children:e.state.error.message})]}),c(`div`,{style:{display:`flex`,gap:`8px`,flexWrap:`wrap`},children:[e.state.status===`idle`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.startItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #007bff`,backgroundColor:`#007bff`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Start`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]}),e.state.status===`uploading`&&s(`button`,{type:`button`,onClick:()=>t.abortItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #dc3545`,backgroundColor:`transparent`,color:`#dc3545`,borderRadius:`4px`,cursor:`pointer`},children:`Cancel`}),e.state.status===`error`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.retryItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #28a745`,backgroundColor:`#28a745`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Retry`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]}),e.state.status===`success`&&s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`}),e.state.status===`aborted`&&c(o,{children:[s(`button`,{type:`button`,onClick:()=>t.retryItem(e),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #007bff`,backgroundColor:`#007bff`,color:`white`,borderRadius:`4px`,cursor:`pointer`},children:`Retry`}),s(`button`,{type:`button`,onClick:()=>t.removeItem(e.id),style:{padding:`4px 8px`,fontSize:`12px`,border:`1px solid #6c757d`,backgroundColor:`transparent`,color:`#6c757d`,borderRadius:`4px`,cursor:`pointer`},children:`Remove`})]})]})]})}function g({children:e,multiple:r=!0,multiUploadOptions:c={},uploadOptions:l={},onUploadStart:u,onValidationError:d,...f}){let p=i(l),m=n(c),h=a(e=>{let t=[];if(!r&&e.length>1&&t.push(`Single file mode is enabled. Please select only one file. You selected ${e.length} files.`),f.accept&&f.accept.length>0){let n=e.filter(e=>!f.accept?.some(t=>{if(t.startsWith(`.`))return e.name.toLowerCase().endsWith(t.toLowerCase());if(t.endsWith(`/*`)){let n=t.slice(0,-2);return e.type.startsWith(n)}else return e.type===t}));if(n.length>0){let e=n.map(e=>`"${e.name}" (${e.type})`).join(`, `),r=f.accept.join(`, `);t.push(`Invalid file type(s): ${e}. Accepted types: ${r}.`)}}return t.length>0?t:null},[r,f.accept]),g=e=>{u?.(e),r&&m?(m.addFiles(e),setTimeout(()=>m.startAll(),0)):!r&&p&&e.length>0&&e[0]&&p.upload(e[0])},_=a(e=>{console.error(`Upload zone validation errors:`,e),d?.(e)},[d]),v=t({...f,multiple:r,validator:h,onFilesReceived:g,onValidationError:_}),y=v.state.isDragging||v.state.isOver,b=r?m?.state.isUploading??!1:p?.isUploading??!1;return s(o,{children:e({dragDrop:v,upload:p,multiUpload:m,openFilePicker:v.openFilePicker,isActive:y,isProcessing:b})})}function _({className:e=``,style:t={},text:n={},errorStyle:r={},children:i,...a}){let o={idle:a.multiple?`Drag files here or click to select`:`Drag a file here or click to select`,dragging:a.multiple?`Drop files here...`:`Drop file here...`,uploading:`Uploading...`,...n};return i?s(g,{...a,children:i}):s(g,{...a,children:({dragDrop:n,upload:i,multiUpload:a,openFilePicker:l,isActive:u,isProcessing:d})=>c(`button`,{type:`button`,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&l()},onKeyUp:e=>{(e.key===`Enter`||e.key===` `)&&l()},...n.dragHandlers,onClick:l,className:`upload-zone ${u?`upload-zone--active`:``} ${d?`upload-zone--processing`:``} ${e}`,style:{border:u?`2px dashed #007bff`:`2px dashed #ccc`,borderRadius:`8px`,padding:`2rem`,textAlign:`center`,cursor:`pointer`,backgroundColor:u?`#f8f9fa`:`transparent`,transition:`all 0.2s ease`,minHeight:`120px`,display:`flex`,flexDirection:`column`,alignItems:`center`,justifyContent:`center`,...t},children:[n.state.isDragging?s(`p`,{style:{margin:0,fontSize:`16px`,color:`#007bff`},children:o.dragging}):d?c(`div`,{style:{textAlign:`center`},children:[s(`p`,{style:{margin:`0 0 10px 0`,fontSize:`14px`},children:o.uploading}),i&&c(`div`,{children:[s(`progress`,{value:i.state.progress,max:100,style:{width:`200px`,height:`8px`}}),c(`p`,{style:{margin:`5px 0 0 0`,fontSize:`12px`,color:`#666`},children:[i.state.progress,`%`]})]}),a&&c(`div`,{children:[s(`progress`,{value:a.state.progress,max:100,style:{width:`200px`,height:`8px`}}),c(`p`,{style:{margin:`5px 0 0 0`,fontSize:`12px`,color:`#666`},children:[a.state.progress,`% (`,a.state.uploading,` `,`uploading, `,a.state.successful,` completed)`]})]})]}):s(`p`,{style:{margin:0,fontSize:`16px`,color:`#666`},children:o.idle}),n.state.errors.length>0&&c(`div`,{style:{marginTop:`10px`,padding:`8px 12px`,backgroundColor:`#f8d7da`,border:`1px solid #f5c6cb`,borderRadius:`4px`,maxWidth:`100%`,...r},children:[s(`p`,{style:{margin:`0 0 5px 0`,fontSize:`12px`,fontWeight:`bold`,color:`#721c24`},children:`Validation Errors:`}),n.state.errors.map((e,t)=>c(`p`,{style:{color:`#721c24`,fontSize:`11px`,margin:`2px 0`,lineHeight:`1.3`},children:[`• `,e]},t))]}),s(`input`,{...n.inputProps})]})})}export{f as a,d as c,m as i,u as l,g as n,p as o,h as r,l as s,_ as t};
|
|
2
|
+
//# sourceMappingURL=upload-zone-D3gie3s1.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-zone-8khVLLYF.mjs","names":["errors: string[]"],"sources":["../src/components/flow-upload-list.tsx","../src/components/flow-upload-zone.tsx","../src/components/upload-list.tsx","../src/components/upload-zone.tsx"],"sourcesContent":["import type {\n BrowserUploadInput,\n FlowUploadConfig,\n FlowUploadItem,\n MultiFlowUploadOptions,\n} from \"@uploadista/client-browser\";\nimport type { ReactNode } from \"react\";\nimport { useMultiFlowUpload } from \"../hooks/use-multi-flow-upload\";\n\n/**\n * Render props passed to the FlowUploadList children function.\n * Provides access to upload items, aggregate statistics, and control methods.\n *\n * @property items - All flow upload items in the queue\n * @property totalProgress - Average progress across all uploads (0-100)\n * @property activeUploads - Count of currently uploading items\n * @property completedUploads - Count of successfully completed uploads\n * @property failedUploads - Count of failed uploads\n * @property isUploading - True when any uploads are in progress\n * @property addFiles - Add new files to the upload queue\n * @property removeFile - Remove a specific file from the queue\n * @property startUpload - Begin uploading all pending files\n * @property abortUpload - Cancel a specific active upload\n * @property abortAll - Cancel all active uploads\n * @property clear - Remove all items from the queue\n * @property retryUpload - Retry a specific failed upload\n */\nexport interface FlowUploadListRenderProps {\n /**\n * List of upload items\n */\n items: FlowUploadItem<BrowserUploadInput>[];\n\n /**\n * Total progress across all uploads\n */\n totalProgress: number;\n\n /**\n * Number of active uploads\n */\n activeUploads: number;\n\n /**\n * Number of completed uploads\n */\n completedUploads: number;\n\n /**\n * Number of failed uploads\n */\n failedUploads: number;\n\n /**\n * Whether any uploads are in progress\n */\n isUploading: boolean;\n\n /**\n * Add files to the upload queue\n */\n addFiles: (files: File[] | FileList) => void;\n\n /**\n * Remove a file from the queue\n */\n removeFile: (id: string) => void;\n\n /**\n * Start uploading all pending files\n */\n startUpload: () => void;\n\n /**\n * Abort a specific upload\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all uploads\n */\n abortAll: () => void;\n\n /**\n * Clear all items\n */\n clear: () => void;\n\n /**\n * Retry a failed upload\n */\n retryUpload: (id: string) => void;\n}\n\n/**\n * Props for the FlowUploadList component.\n *\n * @property flowConfig - Flow execution configuration (flowId, storageId, etc.)\n * @property options - Multi-flow upload options (callbacks, concurrency, etc.)\n * @property children - Render function receiving flow upload list state\n */\nexport interface FlowUploadListProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Multi-upload options\n */\n options?: Omit<MultiFlowUploadOptions<BrowserUploadInput>, \"flowConfig\">;\n\n /**\n * Render function for the upload list\n */\n children: (props: FlowUploadListRenderProps) => ReactNode;\n}\n\n/**\n * Headless flow upload list component for managing batch file uploads through a flow.\n * Uses render props pattern to provide complete control over the UI while handling\n * concurrent uploads and flow processing.\n *\n * Each file is uploaded and processed independently through the specified flow,\n * with automatic queue management and concurrency control.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param props - Flow upload list configuration and render prop\n * @returns Rendered flow upload list using the provided render prop\n *\n * @example\n * ```tsx\n * // Batch image processing with custom UI\n * <FlowUploadList\n * flowConfig={{\n * flowId: \"image-batch-processing\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized\",\n * }}\n * options={{\n * maxConcurrent: 3,\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} processed successfully`);\n * },\n * onComplete: (items) => {\n * const successful = items.filter(i => i.status === 'success');\n * console.log(`Batch complete: ${successful.length}/${items.length} successful`);\n * },\n * }}\n * >\n * {({\n * items,\n * totalProgress,\n * activeUploads,\n * completedUploads,\n * failedUploads,\n * addFiles,\n * startUpload,\n * abortUpload,\n * retryUpload,\n * clear,\n * }) => (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * accept=\"image/*\"\n * onChange={(e) => {\n * if (e.target.files) {\n * addFiles(e.target.files);\n * startUpload();\n * }\n * }}\n * />\n *\n * <div style={{ marginTop: '1rem' }}>\n * <h3>Upload Progress</h3>\n * <div>Overall: {totalProgress}%</div>\n * <div>\n * Active: {activeUploads}, Completed: {completedUploads}, Failed: {failedUploads}\n * </div>\n * <button onClick={clear}>Clear All</button>\n * </div>\n *\n * <ul style={{ listStyle: 'none', padding: 0 }}>\n * {items.map((item) => (\n * <li key={item.id} style={{\n * padding: '1rem',\n * border: '1px solid #ccc',\n * marginBottom: '0.5rem'\n * }}>\n * <div>{item.file instanceof File ? item.file.name : 'File'}</div>\n * <div>Status: {item.status}</div>\n *\n * {item.status === \"uploading\" && (\n * <div>\n * <progress value={item.progress} max={100} style={{ width: '100%' }} />\n * <div>{item.progress}%</div>\n * <button onClick={() => abortUpload(item.id)}>Cancel</button>\n * </div>\n * )}\n *\n * {item.status === \"error\" && (\n * <div>\n * <div style={{ color: 'red' }}>{item.error?.message}</div>\n * <button onClick={() => retryUpload(item.id)}>Retry</button>\n * </div>\n * )}\n *\n * {item.status === \"success\" && (\n * <div style={{ color: 'green' }}>✓ Complete</div>\n * )}\n * </li>\n * ))}\n * </ul>\n * </div>\n * )}\n * </FlowUploadList>\n * ```\n *\n * @see {@link SimpleFlowUploadList} for a pre-styled version\n * @see {@link useMultiFlowUpload} for the underlying hook\n */\nexport function FlowUploadList({\n flowConfig,\n options,\n children,\n}: FlowUploadListProps) {\n const multiUpload = useMultiFlowUpload({\n ...options,\n flowConfig,\n });\n\n return (\n <>\n {children({\n items: multiUpload.state.items,\n totalProgress: multiUpload.state.totalProgress,\n activeUploads: multiUpload.state.activeUploads,\n completedUploads: multiUpload.state.completedUploads,\n failedUploads: multiUpload.state.failedUploads,\n isUploading: multiUpload.isUploading,\n addFiles: multiUpload.addFiles,\n removeFile: multiUpload.removeFile,\n startUpload: multiUpload.startUpload,\n abortUpload: multiUpload.abortUpload,\n abortAll: multiUpload.abortAll,\n clear: multiUpload.clear,\n retryUpload: multiUpload.retryUpload,\n })}\n </>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadListItem component.\n *\n * @property item - The flow upload item to display\n * @property onAbort - Called when the abort button is clicked\n * @property onRetry - Called when the retry button is clicked\n * @property onRemove - Called when the remove button is clicked\n */\nexport interface SimpleFlowUploadListItemProps {\n /**\n * Upload item\n */\n item: FlowUploadItem<BrowserUploadInput>;\n\n /**\n * Abort the upload\n */\n onAbort: () => void;\n\n /**\n * Retry the upload\n */\n onRetry: () => void;\n\n /**\n * Remove the item\n */\n onRemove: () => void;\n}\n\n/**\n * Pre-styled flow upload list item component with status indicators.\n * Displays file name, upload progress, status, and contextual action buttons.\n *\n * Features:\n * - Status-specific icons and colors\n * - Progress bar with percentage and byte count\n * - Error message display\n * - Contextual action buttons (cancel, retry, remove)\n *\n * @param props - Upload item and callback functions\n * @returns Styled flow upload list item component\n *\n * @example\n * ```tsx\n * <SimpleFlowUploadListItem\n * item={uploadItem}\n * onAbort={() => console.log('Abort')}\n * onRetry={() => console.log('Retry')}\n * onRemove={() => console.log('Remove')}\n * />\n * ```\n */\nexport function SimpleFlowUploadListItem({\n item,\n onAbort,\n onRetry,\n onRemove,\n}: SimpleFlowUploadListItemProps) {\n const getStatusIcon = () => {\n switch (item.status) {\n case \"success\":\n return \"✓\";\n case \"error\":\n return \"✗\";\n case \"uploading\":\n return \"⟳\";\n case \"aborted\":\n return \"⊘\";\n default:\n return \"○\";\n }\n };\n\n const getStatusColor = () => {\n switch (item.status) {\n case \"success\":\n return \"green\";\n case \"error\":\n return \"red\";\n case \"uploading\":\n return \"blue\";\n case \"aborted\":\n return \"gray\";\n default:\n return \"black\";\n }\n };\n\n return (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"12px\",\n padding: \"8px\",\n borderBottom: \"1px solid #eee\",\n }}\n >\n <span style={{ color: getStatusColor(), fontSize: \"18px\" }}>\n {getStatusIcon()}\n </span>\n\n <div style={{ flex: 1, minWidth: 0 }}>\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: 500,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }}\n >\n {item.file instanceof File ? item.file.name : \"Upload\"}\n </div>\n\n {item.status === \"uploading\" && (\n <div style={{ marginTop: \"4px\" }}>\n <progress\n value={item.progress}\n max={100}\n style={{ width: \"100%\", height: \"4px\" }}\n />\n <div style={{ fontSize: \"12px\", color: \"#666\", marginTop: \"2px\" }}>\n {item.progress}% • {Math.round(item.bytesUploaded / 1024)} KB /{\" \"}\n {Math.round(item.totalBytes / 1024)} KB\n </div>\n </div>\n )}\n\n {item.status === \"error\" && (\n <div style={{ fontSize: \"12px\", color: \"red\", marginTop: \"2px\" }}>\n {item.error?.message || \"Upload failed\"}\n </div>\n )}\n\n {item.status === \"success\" && (\n <div style={{ fontSize: \"12px\", color: \"green\", marginTop: \"2px\" }}>\n Upload complete\n </div>\n )}\n </div>\n\n <div style={{ display: \"flex\", gap: \"8px\" }}>\n {item.status === \"uploading\" && (\n <button\n type=\"button\"\n onClick={onAbort}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n )}\n\n {item.status === \"error\" && (\n <button\n type=\"button\"\n onClick={onRetry}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n )}\n\n {(item.status === \"pending\" ||\n item.status === \"error\" ||\n item.status === \"aborted\") && (\n <button\n type=\"button\"\n onClick={onRemove}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadList component.\n *\n * @property flowConfig - Flow execution configuration\n * @property options - Multi-flow upload options (callbacks, concurrency)\n * @property className - CSS class name for the container\n * @property showFileInput - Whether to display the file input (default: true)\n * @property accept - Accepted file types for the file input\n */\nexport interface SimpleFlowUploadListProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Multi-upload options\n */\n options?: Omit<MultiFlowUploadOptions<BrowserUploadInput>, \"flowConfig\">;\n\n /**\n * CSS class for the container\n */\n className?: string;\n\n /**\n * Show file input\n */\n showFileInput?: boolean;\n\n /**\n * File input accept attribute\n */\n accept?: string;\n}\n\n/**\n * Simple pre-styled flow upload list component with built-in UI.\n * Provides a ready-to-use interface for batch file uploads with flow processing.\n *\n * Features:\n * - Built-in file input\n * - Overall progress display\n * - Individual item progress tracking\n * - Status indicators and action buttons\n * - Automatic upload start on file selection\n *\n * @param props - Flow upload list configuration with styling options\n * @returns Styled flow upload list component\n *\n * @example\n * ```tsx\n * // Basic batch image upload\n * <SimpleFlowUploadList\n * flowConfig={{\n * flowId: \"image-batch-processing\",\n * storageId: \"s3-images\",\n * }}\n * options={{\n * maxConcurrent: 3,\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} processed`);\n * },\n * onComplete: (items) => {\n * console.log(\"Batch complete:\", items.length);\n * },\n * }}\n * accept=\"image/*\"\n * className=\"my-upload-list\"\n * />\n *\n * // Without file input (add files programmatically)\n * <SimpleFlowUploadList\n * flowConfig={{\n * flowId: \"document-processing\",\n * storageId: \"docs\",\n * }}\n * showFileInput={false}\n * options={{\n * maxConcurrent: 2,\n * }}\n * />\n * ```\n *\n * @see {@link FlowUploadList} for the headless version with full control\n */\nexport function SimpleFlowUploadList({\n flowConfig,\n options,\n className = \"\",\n showFileInput = true,\n accept,\n}: SimpleFlowUploadListProps) {\n return (\n <FlowUploadList flowConfig={flowConfig} options={options}>\n {({\n items,\n addFiles,\n startUpload,\n abortUpload,\n retryUpload,\n removeFile,\n totalProgress,\n }) => (\n <div className={className}>\n {showFileInput && (\n <div style={{ marginBottom: \"16px\" }}>\n <input\n type=\"file\"\n multiple\n accept={accept}\n onChange={(e) => {\n if (e.target.files) {\n addFiles(e.target.files);\n startUpload();\n }\n }}\n style={{\n padding: \"8px\",\n border: \"1px solid #ccc\",\n borderRadius: \"4px\",\n }}\n />\n </div>\n )}\n\n {items.length > 0 && (\n <div>\n <div\n style={{ marginBottom: \"8px\", fontSize: \"14px\", color: \"#666\" }}\n >\n Total Progress: {totalProgress}%\n </div>\n\n <div\n style={{\n border: \"1px solid #eee\",\n borderRadius: \"8px\",\n overflow: \"hidden\",\n }}\n >\n {items.map((item) => (\n <SimpleFlowUploadListItem\n key={item.id}\n item={item}\n onAbort={() => abortUpload(item.id)}\n onRetry={() => retryUpload(item.id)}\n onRemove={() => removeFile(item.id)}\n />\n ))}\n </div>\n </div>\n )}\n </div>\n )}\n </FlowUploadList>\n );\n}\n","import type {\n FlowUploadConfig,\n FlowUploadOptions,\n} from \"@uploadista/client-browser\";\nimport type { ReactNode } from \"react\";\nimport { type UseDragDropReturn, useDragDrop } from \"../hooks/use-drag-drop\";\nimport {\n type UseFlowUploadReturn,\n useFlowUpload,\n} from \"../hooks/use-flow-upload\";\n\n/**\n * Render props passed to the FlowUploadZone children function.\n * Provides access to flow upload state, drag-drop handlers, and helper functions.\n *\n * @property dragDrop - Complete drag-and-drop state and handlers\n * @property flowUpload - Flow upload hook with upload state and controls\n * @property isActive - True when dragging over zone\n * @property openFilePicker - Programmatically open file selection dialog\n * @property getRootProps - Returns props to spread on the drop zone container\n * @property getInputProps - Returns props to spread on the hidden file input\n */\nexport interface FlowUploadZoneRenderProps {\n /**\n * Drag and drop state and handlers\n */\n dragDrop: UseDragDropReturn;\n\n /**\n * Flow upload functionality\n */\n flowUpload: UseFlowUploadReturn;\n\n /**\n * Whether the zone is currently active (dragging or uploading)\n */\n isActive: boolean;\n\n /**\n * Open file picker\n */\n openFilePicker: () => void;\n\n /**\n * Props to spread on the drop zone element\n */\n getRootProps: () => {\n onDragEnter: (e: React.DragEvent) => void;\n onDragOver: (e: React.DragEvent) => void;\n onDragLeave: (e: React.DragEvent) => void;\n onDrop: (e: React.DragEvent) => void;\n };\n\n /**\n * Props to spread on the file input element\n */\n getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>;\n}\n\n/**\n * Props for the FlowUploadZone component.\n *\n * @property flowConfig - Flow execution configuration (flowId, storageId, etc.)\n * @property options - Flow upload options (callbacks, metadata, etc.)\n * @property accept - Accepted file types (e.g., \"image/*\", \".pdf\")\n * @property multiple - Allow multiple file selection (default: false)\n * @property children - Render function receiving flow upload zone state\n */\nexport interface FlowUploadZoneProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Upload options\n */\n options?: Omit<FlowUploadOptions, \"flowConfig\">;\n\n /**\n * Accepted file types (e.g., \"image/*\", \".pdf\", etc.)\n */\n accept?: string;\n\n /**\n * Whether to allow multiple files (uses multi-upload internally)\n */\n multiple?: boolean;\n\n /**\n * Render function for the drop zone\n */\n children: (props: FlowUploadZoneRenderProps) => ReactNode;\n}\n\n/**\n * Headless flow upload zone component with drag-and-drop support.\n * Combines drag-drop functionality with flow processing, using render props\n * for complete UI control.\n *\n * Files uploaded through this zone are automatically processed through the\n * specified flow, which can perform operations like image optimization,\n * storage saving, webhooks, etc.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param props - Flow upload zone configuration and render prop\n * @returns Rendered flow upload zone using the provided render prop\n *\n * @example\n * ```tsx\n * // Image upload with flow processing\n * <FlowUploadZone\n * flowConfig={{\n * flowId: \"image-processing-flow\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized-image\",\n * }}\n * options={{\n * onSuccess: (result) => console.log('Processed:', result),\n * onFlowComplete: (outputs) => {\n * console.log('All outputs:', outputs);\n * },\n * }}\n * accept=\"image/*\"\n * >\n * {({ dragDrop, flowUpload, getRootProps, getInputProps, openFilePicker }) => (\n * <div {...getRootProps()} style={{\n * border: dragDrop.state.isDragging ? '2px solid blue' : '2px dashed gray',\n * padding: '2rem',\n * textAlign: 'center'\n * }}>\n * <input {...getInputProps()} />\n *\n * {dragDrop.state.isDragging && (\n * <p>Drop image here...</p>\n * )}\n *\n * {!dragDrop.state.isDragging && !flowUpload.isUploading && (\n * <div>\n * <p>Drag an image or click to browse</p>\n * <button onClick={openFilePicker}>Choose File</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploadingFile && (\n * <div>\n * <p>Uploading...</p>\n * <progress value={flowUpload.state.progress} max={100} />\n * <span>{flowUpload.state.progress}%</span>\n * </div>\n * )}\n *\n * {flowUpload.isProcessing && (\n * <div>\n * <p>Processing image...</p>\n * {flowUpload.state.currentNodeName && (\n * <span>Step: {flowUpload.state.currentNodeName}</span>\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"success\" && (\n * <div>\n * <p>✓ Upload complete!</p>\n * {flowUpload.state.result && (\n * <img src={flowUpload.state.result.url} alt=\"Uploaded\" />\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"error\" && (\n * <div>\n * <p>Error: {flowUpload.state.error?.message}</p>\n * <button onClick={flowUpload.reset}>Try Again</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploading && (\n * <button onClick={flowUpload.abort}>Cancel</button>\n * )}\n * </div>\n * )}\n * </FlowUploadZone>\n * ```\n *\n * @see {@link SimpleFlowUploadZone} for a pre-styled version\n * @see {@link useFlowUpload} for the underlying hook\n */\nexport function FlowUploadZone({\n flowConfig,\n options,\n accept,\n multiple = false,\n children,\n}: FlowUploadZoneProps) {\n // Hook automatically subscribes to events through context\n const flowUpload = useFlowUpload({\n ...options,\n flowConfig,\n });\n\n const dragDrop = useDragDrop({\n onFilesReceived: (files: File[]) => {\n const file = files[0];\n if (file) {\n flowUpload.upload(file);\n }\n },\n accept: accept ? [accept] : undefined,\n multiple,\n });\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n const file = files?.[0];\n if (file) {\n flowUpload.upload(file);\n }\n };\n\n // Determine active state\n const isActive = dragDrop.state.isDragging || dragDrop.state.isOver;\n\n return (\n <>\n {children({\n flowUpload,\n dragDrop,\n isActive,\n openFilePicker: dragDrop.openFilePicker,\n getRootProps: () => dragDrop.dragHandlers,\n getInputProps: () => ({\n ...dragDrop.inputProps,\n onChange: handleFileChange,\n }),\n })}\n </>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadZone component.\n *\n * @property flowConfig - Flow execution configuration\n * @property options - Flow upload options (callbacks, metadata)\n * @property accept - Accepted file types\n * @property className - CSS class name for custom styling\n * @property dragText - Text displayed when dragging files over zone\n * @property idleText - Text displayed when zone is idle\n */\nexport interface SimpleFlowUploadZoneProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Upload options\n */\n options?: Omit<FlowUploadOptions, \"flowConfig\">;\n\n /**\n * Accepted file types\n */\n accept?: string;\n\n /**\n * CSS class for the container\n */\n className?: string;\n\n /**\n * Custom drag overlay text\n */\n dragText?: string;\n\n /**\n * Custom idle text\n */\n idleText?: string;\n}\n\n/**\n * Simple pre-styled flow upload zone component with built-in UI.\n * Provides a ready-to-use drag-and-drop interface for flow uploads.\n *\n * Features:\n * - Built-in drag-and-drop visual feedback\n * - Automatic progress display for upload and processing phases\n * - Success and error state display\n * - Cancel button during upload\n * - Customizable text labels\n *\n * @param props - Flow upload zone configuration with styling options\n * @returns Styled flow upload zone component\n *\n * @example\n * ```tsx\n * // Basic image upload with flow processing\n * <SimpleFlowUploadZone\n * flowConfig={{\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * }}\n * accept=\"image/*\"\n * options={{\n * onSuccess: (result) => console.log(\"Image processed:\", result),\n * onError: (error) => console.error(\"Processing failed:\", error),\n * }}\n * idleText=\"Drop an image to optimize and upload\"\n * dragText=\"Release to start processing\"\n * className=\"my-upload-zone\"\n * />\n *\n * // Document upload with custom flow\n * <SimpleFlowUploadZone\n * flowConfig={{\n * flowId: \"document-processing-flow\",\n * storageId: \"docs\",\n * outputNodeId: \"processed-doc\",\n * }}\n * accept=\".pdf,.doc,.docx\"\n * options={{\n * onFlowComplete: (outputs) => {\n * console.log('Processing outputs:', outputs);\n * },\n * }}\n * />\n * ```\n *\n * @see {@link FlowUploadZone} for the headless version with full control\n */\nexport function SimpleFlowUploadZone({\n flowConfig,\n options,\n accept,\n className = \"\",\n dragText = \"Drop files here\",\n idleText = \"Drag & drop files or click to browse\",\n}: SimpleFlowUploadZoneProps) {\n return (\n <FlowUploadZone flowConfig={flowConfig} options={options} accept={accept}>\n {({\n dragDrop,\n flowUpload,\n getRootProps,\n getInputProps,\n openFilePicker,\n }) => (\n <div\n {...getRootProps()}\n className={className}\n style={{\n border: \"2px dashed #ccc\",\n borderRadius: \"8px\",\n padding: \"32px\",\n textAlign: \"center\",\n cursor: \"pointer\",\n backgroundColor: dragDrop.state.isDragging\n ? \"#f0f0f0\"\n : \"transparent\",\n transition: \"background-color 0.2s\",\n }}\n >\n <input {...getInputProps()} />\n\n {dragDrop.state.isDragging && <p style={{ margin: 0 }}>{dragText}</p>}\n\n {!dragDrop.state.isDragging &&\n !flowUpload.isUploading &&\n flowUpload.state.status === \"idle\" && (\n <div>\n <p style={{ margin: \"0 0 16px 0\" }}>{idleText}</p>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n openFilePicker();\n }}\n style={{\n padding: \"8px 16px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Choose Files\n </button>\n </div>\n )}\n\n {flowUpload.isUploading && (\n <div>\n <progress\n value={flowUpload.state.progress}\n max={100}\n style={{ width: \"100%\", height: \"8px\" }}\n />\n <p style={{ margin: \"8px 0 0 0\" }}>\n {flowUpload.state.progress}%\n </p>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n // abort() will be passed from parent\n }}\n style={{\n marginTop: \"8px\",\n padding: \"4px 12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {flowUpload.state.status === \"success\" && (\n <div>\n <p style={{ margin: 0, color: \"green\" }}>✓ Upload complete!</p>\n </div>\n )}\n\n {flowUpload.state.status === \"error\" && (\n <div>\n <p style={{ margin: 0, color: \"red\" }}>\n ✗ Error: {flowUpload.state.error?.message}\n </p>\n </div>\n )}\n </div>\n )}\n </FlowUploadZone>\n );\n}\n","import type React from \"react\";\nimport type {\n UploadItem,\n UseMultiUploadReturn,\n} from \"../hooks/use-multi-upload\";\nimport type { UploadStatus } from \"../hooks/use-upload\";\n\n/**\n * Render props passed to the UploadList children function.\n * Provides organized access to upload items, status groupings, and actions.\n *\n * @property items - All upload items (filtered and sorted if configured)\n * @property itemsByStatus - Upload items grouped by their current status\n * @property multiUpload - Complete multi-upload hook instance\n * @property actions - Helper functions for common item operations\n * @property actions.removeItem - Remove an item from the list\n * @property actions.retryItem - Retry a failed upload\n * @property actions.abortItem - Cancel an active upload\n * @property actions.startItem - Begin uploading an idle item\n */\nexport interface UploadListRenderProps {\n /**\n * All upload items\n */\n items: UploadItem[];\n\n /**\n * Items filtered by status\n */\n itemsByStatus: {\n idle: UploadItem[];\n uploading: UploadItem[];\n success: UploadItem[];\n error: UploadItem[];\n aborted: UploadItem[];\n };\n\n /**\n * Multi-upload state and controls\n */\n multiUpload: UseMultiUploadReturn;\n\n /**\n * Helper functions for item management\n */\n actions: {\n removeItem: (id: string) => void;\n retryItem: (item: UploadItem) => void;\n abortItem: (item: UploadItem) => void;\n startItem: (item: UploadItem) => void;\n };\n}\n\n/**\n * Props for the UploadList component.\n *\n * @property multiUpload - Multi-upload hook instance to display\n * @property filter - Optional function to filter which items to show\n * @property sortBy - Optional comparator function to sort items\n * @property children - Render function receiving list state and actions\n */\nexport interface UploadListProps {\n /**\n * Multi-upload instance from useMultiUpload hook\n */\n multiUpload: UseMultiUploadReturn;\n\n /**\n * Optional filter for which items to display\n */\n filter?: (item: UploadItem) => boolean;\n\n /**\n * Optional sorting function for items\n */\n sortBy?: (a: UploadItem, b: UploadItem) => number;\n\n /**\n * Render prop that receives upload list state and actions\n */\n children: (props: UploadListRenderProps) => React.ReactNode;\n}\n\n/**\n * Headless upload list component that provides flexible rendering for upload items.\n * Uses render props pattern to give full control over how upload items are displayed.\n *\n * @param props - Upload list configuration and render prop\n * @returns Rendered upload list using the provided render prop\n *\n * @example\n * ```tsx\n * // Basic upload list with progress bars\n * <UploadList multiUpload={multiUpload}>\n * {({ items, actions }) => (\n * <div>\n * <h3>Upload Queue ({items.length} files)</h3>\n * {items.map((item) => (\n * <div key={item.id} style={{\n * padding: '1rem',\n * border: '1px solid #ccc',\n * marginBottom: '0.5rem',\n * borderRadius: '4px'\n * }}>\n * <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n * <span>{item.file.name}</span>\n * <span>{item.state.status}</span>\n * </div>\n *\n * {item.state.status === 'uploading' && (\n * <div>\n * <progress value={item.state.progress} max={100} />\n * <span>{item.state.progress}%</span>\n * <button onClick={() => actions.abortItem(item)}>Cancel</button>\n * </div>\n * )}\n *\n * {item.state.status === 'error' && (\n * <div>\n * <p style={{ color: 'red' }}>Error: {item.state.error?.message}</p>\n * <button onClick={() => actions.retryItem(item)}>Retry</button>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n *\n * {item.state.status === 'success' && (\n * <div>\n * <p style={{ color: 'green' }}>✓ Uploaded successfully</p>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n *\n * {item.state.status === 'idle' && (\n * <div>\n * <button onClick={() => actions.startItem(item)}>Start Upload</button>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n * </div>\n * ))}\n * </div>\n * )}\n * </UploadList>\n *\n * // Upload list with status filtering and sorting\n * <UploadList\n * multiUpload={multiUpload}\n * filter={(item) => item.state.status !== 'success'} // Hide successful uploads\n * sortBy={(a, b) => {\n * // Sort by status priority, then by filename\n * const statusPriority = { error: 0, uploading: 1, idle: 2, success: 3, aborted: 4 };\n * const aPriority = statusPriority[a.state.status];\n * const bPriority = statusPriority[b.state.status];\n *\n * if (aPriority !== bPriority) {\n * return aPriority - bPriority;\n * }\n *\n * return a.file.name.localeCompare(b.file.name);\n * }}\n * >\n * {({ items, itemsByStatus, multiUpload, actions }) => (\n * <div>\n * {itemsByStatus.error.length > 0 && (\n * <div>\n * <h4 style={{ color: 'red' }}>Failed Uploads ({itemsByStatus.error.length})</h4>\n * {itemsByStatus.error.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n *\n * {itemsByStatus.uploading.length > 0 && (\n * <div>\n * <h4>Uploading ({itemsByStatus.uploading.length})</h4>\n * {itemsByStatus.uploading.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n *\n * {itemsByStatus.idle.length > 0 && (\n * <div>\n * <h4>Pending ({itemsByStatus.idle.length})</h4>\n * {itemsByStatus.idle.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n * </div>\n * )}\n * </UploadList>\n * ```\n */\nexport function UploadList({\n multiUpload,\n filter,\n sortBy,\n children,\n}: UploadListProps) {\n // Apply filtering\n let items = multiUpload.items;\n if (filter) {\n items = items.filter(filter);\n }\n\n // Apply sorting\n if (sortBy) {\n items = [...items].sort(sortBy);\n }\n\n // Group items by status\n const itemsByStatus = {\n idle: items.filter((item) => item.state.status === \"idle\"),\n uploading: items.filter((item) => item.state.status === \"uploading\"),\n success: items.filter((item) => item.state.status === \"success\"),\n error: items.filter((item) => item.state.status === \"error\"),\n aborted: items.filter((item) => item.state.status === \"aborted\"),\n };\n\n // Create action helpers\n const actions = {\n removeItem: (id: string) => {\n multiUpload.removeItem(id);\n },\n retryItem: (_item: UploadItem) => {\n // Retry failed uploads using multiUpload method\n multiUpload.retryFailed();\n },\n abortItem: (item: UploadItem) => {\n // Remove the item to effectively abort it\n multiUpload.removeItem(item.id);\n },\n startItem: (_item: UploadItem) => {\n // Start all pending uploads\n multiUpload.startAll();\n },\n };\n\n // Create render props object\n const renderProps: UploadListRenderProps = {\n items,\n itemsByStatus,\n multiUpload,\n actions,\n };\n\n return <>{children(renderProps)}</>;\n}\n\n/**\n * Props for the SimpleUploadListItem component.\n *\n * @property item - The upload item to display\n * @property actions - Action functions from UploadList render props\n * @property className - Additional CSS class name\n * @property style - Inline styles for the item container\n * @property showDetails - Whether to display file size and upload details\n */\nexport interface SimpleUploadListItemProps {\n /**\n * The upload item to render\n */\n item: UploadItem;\n\n /**\n * Actions from UploadList render props\n */\n actions: UploadListRenderProps[\"actions\"];\n\n /**\n * Additional CSS class name\n */\n className?: string;\n\n /**\n * Inline styles\n */\n style?: React.CSSProperties;\n\n /**\n * Whether to show detailed information (file size, speed, etc.)\n */\n showDetails?: boolean;\n}\n\n/**\n * Pre-styled upload list item component with status indicators and action buttons.\n * Displays file info, progress, errors, and contextual actions based on upload status.\n *\n * Features:\n * - Status-specific color coding and icons\n * - Progress bar for active uploads\n * - Error message display\n * - File size formatting\n * - Contextual action buttons (start, cancel, retry, remove)\n *\n * @param props - Upload item and configuration\n * @returns Styled upload list item component\n *\n * @example\n * ```tsx\n * // Use with UploadList\n * <UploadList multiUpload={multiUpload}>\n * {({ items, actions }) => (\n * <div>\n * {items.map((item) => (\n * <SimpleUploadListItem\n * key={item.id}\n * item={item}\n * actions={actions}\n * showDetails={true}\n * />\n * ))}\n * </div>\n * )}\n * </UploadList>\n *\n * // Custom styling\n * <SimpleUploadListItem\n * item={uploadItem}\n * actions={actions}\n * className=\"my-upload-item\"\n * style={{ borderRadius: '12px', margin: '1rem' }}\n * showDetails={true}\n * />\n * ```\n */\nexport function SimpleUploadListItem({\n item,\n actions,\n className = \"\",\n style = {},\n showDetails = true,\n}: SimpleUploadListItemProps) {\n const getStatusColor = (status: UploadStatus) => {\n switch (status) {\n case \"idle\":\n return \"#6c757d\";\n case \"uploading\":\n return \"#007bff\";\n case \"success\":\n return \"#28a745\";\n case \"error\":\n return \"#dc3545\";\n case \"aborted\":\n return \"#6c757d\";\n default:\n return \"#6c757d\";\n }\n };\n\n const getStatusIcon = (status: UploadStatus) => {\n switch (status) {\n case \"idle\":\n return \"⏳\";\n case \"uploading\":\n return \"📤\";\n case \"success\":\n return \"✅\";\n case \"error\":\n return \"❌\";\n case \"aborted\":\n return \"⏹️\";\n default:\n return \"❓\";\n }\n };\n\n const formatFileSize = (bytes: number) => {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n };\n\n return (\n <div\n className={`upload-list-item upload-list-item--${item.state.status} ${className}`}\n style={{\n padding: \"12px\",\n border: \"1px solid #e0e0e0\",\n borderRadius: \"6px\",\n marginBottom: \"8px\",\n backgroundColor: \"#fff\",\n transition: \"all 0.2s ease\",\n ...style,\n }}\n >\n {/* Header with filename and status */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"8px\",\n }}\n >\n <div\n style={{ display: \"flex\", alignItems: \"center\", gap: \"8px\", flex: 1 }}\n >\n <span style={{ fontSize: \"16px\" }}>\n {getStatusIcon(item.state.status)}\n </span>\n <span style={{ fontWeight: \"500\", flex: 1 }}>\n {item.file instanceof File ? item.file.name : \"File\"}\n </span>\n </div>\n <span\n style={{\n fontSize: \"12px\",\n color: getStatusColor(item.state.status),\n fontWeight: \"500\",\n textTransform: \"uppercase\",\n }}\n >\n {item.state.status}\n </span>\n </div>\n\n {/* Progress bar for uploading items */}\n {item.state.status === \"uploading\" && (\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"4px\",\n }}\n >\n <span style={{ fontSize: \"12px\", color: \"#666\" }}>\n {item.state.progress}%\n </span>\n {showDetails && item.state.totalBytes && (\n <span style={{ fontSize: \"12px\", color: \"#666\" }}>\n {formatFileSize(item.state.bytesUploaded)} /{\" \"}\n {formatFileSize(item.state.totalBytes)}\n </span>\n )}\n </div>\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"#e0e0e0\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${item.state.progress}%`,\n height: \"100%\",\n backgroundColor: \"#007bff\",\n transition: \"width 0.2s ease\",\n }}\n />\n </div>\n </div>\n )}\n\n {/* Details section */}\n {showDetails && (\n <div style={{ fontSize: \"12px\", color: \"#666\", marginBottom: \"8px\" }}>\n {item.state.totalBytes && (\n <span>{formatFileSize(item.state.totalBytes)}</span>\n )}\n {item.state.status === \"uploading\" && item.state.progress > 0 && (\n <span> • Progress: {item.state.progress}%</span>\n )}\n {item.state.status === \"error\" && item.state.error && (\n <div style={{ color: \"#dc3545\", marginTop: \"4px\" }}>\n {item.state.error.message}\n </div>\n )}\n </div>\n )}\n\n {/* Action buttons */}\n <div style={{ display: \"flex\", gap: \"8px\", flexWrap: \"wrap\" }}>\n {item.state.status === \"idle\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.startItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #007bff\",\n backgroundColor: \"#007bff\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Start\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n\n {item.state.status === \"uploading\" && (\n <button\n type=\"button\"\n onClick={() => actions.abortItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #dc3545\",\n backgroundColor: \"transparent\",\n color: \"#dc3545\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n )}\n\n {item.state.status === \"error\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.retryItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #28a745\",\n backgroundColor: \"#28a745\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n\n {item.state.status === \"success\" && (\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n )}\n\n {item.state.status === \"aborted\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.retryItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #007bff\",\n backgroundColor: \"#007bff\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n </div>\n </div>\n );\n}\n","/**\n * Upload Zone Components\n *\n * Enhanced error handling features:\n * - MIME type validation with detailed error messages\n * - File count validation (single vs multiple mode)\n * - Custom validation error callbacks\n * - Built-in error display in SimpleUploadZone\n * - Configurable error styling\n */\n\nimport type React from \"react\";\nimport { useCallback } from \"react\";\nimport type {\n DragDropOptions,\n UseDragDropReturn,\n} from \"../hooks/use-drag-drop\";\nimport { useDragDrop } from \"../hooks/use-drag-drop\";\nimport type {\n MultiUploadOptions,\n UseMultiUploadReturn,\n} from \"../hooks/use-multi-upload\";\nimport { useMultiUpload } from \"../hooks/use-multi-upload\";\nimport type { UseUploadOptions, UseUploadReturn } from \"../hooks/use-upload\";\nimport { useUpload } from \"../hooks/use-upload\";\n\n/**\n * Render props passed to the UploadZone children function.\n * Provides access to drag-drop state, upload controls, and helper functions.\n *\n * @property dragDrop - Complete drag-and-drop state and event handlers\n * @property upload - Single upload hook (null when multiple=true)\n * @property multiUpload - Multi-upload hook (null when multiple=false)\n * @property openFilePicker - Programmatically trigger file selection dialog\n * @property isActive - True when dragging over zone or files selected\n * @property isProcessing - True when uploads are in progress\n */\nexport interface UploadZoneRenderProps {\n /**\n * Drag and drop state and handlers\n */\n dragDrop: UseDragDropReturn;\n\n /**\n * Single upload functionality (if not using multi-upload)\n */\n upload: UseUploadReturn | null;\n\n /**\n * Multi-upload functionality (if using multi-upload)\n */\n multiUpload: UseMultiUploadReturn | null;\n\n /**\n * Helper function to open file picker\n */\n openFilePicker: () => void;\n\n /**\n * Whether the zone is currently active (dragging or uploading)\n */\n isActive: boolean;\n\n /**\n * Whether files are being processed\n */\n isProcessing: boolean;\n}\n\n/**\n * Props for the UploadZone component.\n * Combines drag-drop options with upload configuration.\n *\n * @property multiple - Enable multi-file selection and upload (default: true)\n * @property multiUploadOptions - Configuration for multi-upload mode\n * @property uploadOptions - Configuration for single-upload mode\n * @property children - Render function receiving upload zone state\n * @property onUploadStart - Called when files pass validation and upload begins\n * @property onValidationError - Called when file validation fails\n * @property accept - Accepted file types (e.g., ['image/*', '.pdf'])\n * @property maxFiles - Maximum number of files allowed\n * @property maxFileSize - Maximum file size in bytes\n * @property validator - Custom validation function\n */\nexport interface UploadZoneProps\n extends Omit<DragDropOptions, \"onFilesReceived\"> {\n /**\n * Whether to enable multi-file upload mode\n */\n multiple?: boolean;\n\n /**\n * Multi-upload specific options (only used when multiple=true)\n */\n multiUploadOptions?: MultiUploadOptions;\n\n /**\n * Single upload specific options (only used when multiple=false)\n */\n uploadOptions?: UseUploadOptions;\n\n /**\n * Render prop that receives upload zone state and handlers\n */\n children: (props: UploadZoneRenderProps) => React.ReactNode;\n\n /**\n * Called when files are processed and uploads begin\n */\n onUploadStart?: (files: File[]) => void;\n\n /**\n * Called when validation errors occur\n */\n onValidationError?: (errors: string[]) => void;\n}\n\n/**\n * Headless upload zone component that combines drag and drop functionality\n * with upload management. Uses render props pattern for maximum flexibility.\n * Includes enhanced error handling for MIME type validation and file count validation.\n *\n * @param props - Upload zone configuration and render prop\n * @returns Rendered upload zone using the provided render prop\n *\n * @example\n * ```tsx\n * // Single file upload zone with error handling\n * <UploadZone\n * multiple={false}\n * accept={['image/*']}\n * maxFileSize={5 * 1024 * 1024}\n * onValidationError={(errors) => {\n * console.error('Validation errors:', errors);\n * }}\n * uploadOptions={{\n * onSuccess: (result) => console.log('Upload complete:', result),\n * onError: (error) => console.error('Upload failed:', error),\n * }}\n * >\n * {({ dragDrop, upload, openFilePicker, isActive }) => (\n * <div {...dragDrop.dragHandlers} onClick={openFilePicker}>\n * {dragDrop.state.isDragging ? (\n * <p>Drop file here...</p>\n * ) : upload?.isUploading ? (\n * <p>Uploading... {upload.state.progress}%</p>\n * ) : (\n * <p>Drag a file here or click to select</p>\n * )}\n *\n * {dragDrop.state.errors.length > 0 && (\n * <div style={{ color: 'red' }}>\n * {dragDrop.state.errors.map((error, index) => (\n * <p key={index}>{error}</p>\n * ))}\n * </div>\n * )}\n *\n * <input {...dragDrop.inputProps} />\n * </div>\n * )}\n * </UploadZone>\n * ```\n */\nexport function UploadZone({\n children,\n multiple = true,\n multiUploadOptions = {},\n uploadOptions = {},\n onUploadStart,\n onValidationError,\n ...dragDropOptions\n}: UploadZoneProps) {\n // Always initialize both hooks, but only use the appropriate one\n const singleUpload = useUpload(uploadOptions);\n const multiUpload = useMultiUpload(multiUploadOptions);\n\n // Enhanced validation function for better error handling\n const enhancedValidator = useCallback(\n (files: File[]): string[] | null => {\n const errors: string[] = [];\n\n // Check file count based on multiple setting\n if (!multiple && files.length > 1) {\n errors.push(\n `Single file mode is enabled. Please select only one file. You selected ${files.length} files.`,\n );\n }\n\n // Enhanced MIME type validation with better error messages\n if (dragDropOptions.accept && dragDropOptions.accept.length > 0) {\n const invalidFiles = files.filter((file) => {\n return !dragDropOptions.accept?.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n } else {\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n } else {\n return file.type === acceptType;\n }\n }\n });\n });\n\n if (invalidFiles.length > 0) {\n const fileNames = invalidFiles\n .map((f) => `\"${f.name}\" (${f.type})`)\n .join(\", \");\n const acceptedTypes = dragDropOptions.accept.join(\", \");\n errors.push(\n `Invalid file type(s): ${fileNames}. Accepted types: ${acceptedTypes}.`,\n );\n }\n }\n\n return errors.length > 0 ? errors : null;\n },\n [multiple, dragDropOptions.accept],\n );\n\n // Handle file processing\n const handleFilesReceived = (files: File[]) => {\n onUploadStart?.(files);\n\n if (multiple && multiUpload) {\n // Add files to multi-upload queue\n multiUpload.addFiles(files);\n\n // Auto-start uploads if configured to do so\n // Note: This could be made configurable with an autoStart prop\n setTimeout(() => multiUpload.startAll(), 0);\n } else if (!multiple && singleUpload && files.length > 0 && files[0]) {\n // Start single file upload\n singleUpload.upload(files[0]);\n }\n };\n\n // Handle validation errors\n const handleValidationError = useCallback(\n (errors: string[]) => {\n console.error(\"Upload zone validation errors:\", errors);\n // Call the custom error handler if provided\n onValidationError?.(errors);\n },\n [onValidationError],\n );\n\n // Initialize drag and drop with enhanced validation\n const dragDrop = useDragDrop({\n ...dragDropOptions,\n multiple,\n validator: enhancedValidator,\n onFilesReceived: handleFilesReceived,\n onValidationError: handleValidationError,\n });\n\n // Determine active state\n const isActive = dragDrop.state.isDragging || dragDrop.state.isOver;\n\n // Determine processing state\n const isProcessing = multiple\n ? (multiUpload?.state.isUploading ?? false)\n : (singleUpload?.isUploading ?? false);\n\n // Create render props object\n const renderProps: UploadZoneRenderProps = {\n dragDrop,\n upload: singleUpload,\n multiUpload,\n openFilePicker: dragDrop.openFilePicker,\n isActive,\n isProcessing,\n };\n\n return <>{children(renderProps)}</>;\n}\n\n/**\n * Props for the SimpleUploadZone component with built-in styling.\n *\n * @property className - CSS class name for custom styling\n * @property style - Inline styles for the upload zone container\n * @property text - Custom text labels for different states\n * @property text.idle - Text shown when zone is idle\n * @property text.dragging - Text shown when dragging files over zone\n * @property text.uploading - Text shown during upload\n * @property errorStyle - Custom styles for validation error display\n */\nexport interface SimpleUploadZoneProps extends UploadZoneProps {\n /**\n * Additional CSS class name for styling\n */\n className?: string;\n\n /**\n * Inline styles for the upload zone\n */\n style?: React.CSSProperties;\n\n /**\n * Custom text to display in different states\n */\n text?: {\n idle?: string;\n dragging?: string;\n uploading?: string;\n };\n\n /**\n * Custom error message styling\n */\n errorStyle?: React.CSSProperties;\n}\n\n/**\n * Simple pre-styled upload zone component with built-in UI and error handling.\n * Provides a ready-to-use drag-and-drop upload interface with minimal configuration.\n *\n * Features:\n * - Built-in drag-and-drop visual feedback\n * - Automatic progress display\n * - File validation error display\n * - Customizable text and styling\n * - Keyboard accessible\n *\n * @param props - Upload zone configuration with styling options\n * @returns Styled upload zone component\n *\n * @example\n * ```tsx\n * // Multi-file upload with validation\n * <SimpleUploadZone\n * multiple={true}\n * accept={['image/*', '.pdf']}\n * maxFiles={5}\n * maxFileSize={10 * 1024 * 1024} // 10MB\n * onUploadStart={(files) => console.log('Starting uploads:', files.length)}\n * onValidationError={(errors) => {\n * errors.forEach(err => console.error(err));\n * }}\n * multiUploadOptions={{\n * maxConcurrent: 3,\n * onComplete: (results) => {\n * console.log(`${results.successful.length}/${results.total} uploaded`);\n * },\n * }}\n * style={{\n * width: '400px',\n * height: '200px',\n * margin: '20px auto',\n * }}\n * text={{\n * idle: 'Drop your files here or click to browse',\n * dragging: 'Release to upload',\n * uploading: 'Uploading files...',\n * }}\n * errorStyle={{\n * backgroundColor: '#fff3cd',\n * borderColor: '#ffeaa7',\n * }}\n * />\n *\n * // Single file upload\n * <SimpleUploadZone\n * multiple={false}\n * accept={['image/*']}\n * uploadOptions={{\n * onSuccess: (result) => console.log('Uploaded:', result),\n * onError: (error) => console.error('Failed:', error),\n * }}\n * text={{\n * idle: 'Click or drag an image to upload',\n * }}\n * />\n * ```\n */\nexport function SimpleUploadZone({\n className = \"\",\n style = {},\n text = {},\n errorStyle = {},\n children,\n ...uploadZoneProps\n}: SimpleUploadZoneProps) {\n const defaultText = {\n idle: uploadZoneProps.multiple\n ? \"Drag files here or click to select\"\n : \"Drag a file here or click to select\",\n dragging: uploadZoneProps.multiple\n ? \"Drop files here...\"\n : \"Drop file here...\",\n uploading: \"Uploading...\",\n };\n\n const displayText = { ...defaultText, ...text };\n\n // If children render prop is provided, use UploadZone directly\n if (children) {\n return <UploadZone {...uploadZoneProps}>{children}</UploadZone>;\n }\n\n // Otherwise, provide default UI\n return (\n <UploadZone {...uploadZoneProps}>\n {({\n dragDrop,\n upload,\n multiUpload,\n openFilePicker,\n isActive,\n isProcessing,\n }) => (\n <button\n type=\"button\"\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n openFilePicker();\n }\n }}\n onKeyUp={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n openFilePicker();\n }\n }}\n {...dragDrop.dragHandlers}\n onClick={openFilePicker}\n className={`upload-zone ${isActive ? \"upload-zone--active\" : \"\"} ${isProcessing ? \"upload-zone--processing\" : \"\"} ${className}`}\n style={{\n border: isActive ? \"2px dashed #007bff\" : \"2px dashed #ccc\",\n borderRadius: \"8px\",\n padding: \"2rem\",\n textAlign: \"center\",\n cursor: \"pointer\",\n backgroundColor: isActive ? \"#f8f9fa\" : \"transparent\",\n transition: \"all 0.2s ease\",\n minHeight: \"120px\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n ...style,\n }}\n >\n {dragDrop.state.isDragging ? (\n <p style={{ margin: 0, fontSize: \"16px\", color: \"#007bff\" }}>\n {displayText.dragging}\n </p>\n ) : isProcessing ? (\n <div style={{ textAlign: \"center\" }}>\n <p style={{ margin: \"0 0 10px 0\", fontSize: \"14px\" }}>\n {displayText.uploading}\n </p>\n {upload && (\n <div>\n <progress\n value={upload.state.progress}\n max={100}\n style={{ width: \"200px\", height: \"8px\" }}\n />\n <p\n style={{\n margin: \"5px 0 0 0\",\n fontSize: \"12px\",\n color: \"#666\",\n }}\n >\n {upload.state.progress}%\n </p>\n </div>\n )}\n {multiUpload && (\n <div>\n <progress\n value={multiUpload.state.progress}\n max={100}\n style={{ width: \"200px\", height: \"8px\" }}\n />\n <p\n style={{\n margin: \"5px 0 0 0\",\n fontSize: \"12px\",\n color: \"#666\",\n }}\n >\n {multiUpload.state.progress}% ({multiUpload.state.uploading}{\" \"}\n uploading, {multiUpload.state.successful} completed)\n </p>\n </div>\n )}\n </div>\n ) : (\n <p style={{ margin: 0, fontSize: \"16px\", color: \"#666\" }}>\n {displayText.idle}\n </p>\n )}\n\n {dragDrop.state.errors.length > 0 && (\n <div\n style={{\n marginTop: \"10px\",\n padding: \"8px 12px\",\n backgroundColor: \"#f8d7da\",\n border: \"1px solid #f5c6cb\",\n borderRadius: \"4px\",\n maxWidth: \"100%\",\n ...errorStyle,\n }}\n >\n <p\n style={{\n margin: \"0 0 5px 0\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: \"#721c24\",\n }}\n >\n Validation Errors:\n </p>\n {dragDrop.state.errors.map((error, index) => (\n <p\n // biome-ignore lint/suspicious/noArrayIndexKey: index is used as key\n key={index}\n style={{\n color: \"#721c24\",\n fontSize: \"11px\",\n margin: \"2px 0\",\n lineHeight: \"1.3\",\n }}\n >\n • {error}\n </p>\n ))}\n </div>\n )}\n\n <input {...dragDrop.inputProps} />\n </button>\n )}\n </UploadZone>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"upload-zone-D3gie3s1.mjs","names":["errors: string[]"],"sources":["../src/components/flow-upload-list.tsx","../src/components/flow-upload-zone.tsx","../src/components/upload-list.tsx","../src/components/upload-zone.tsx"],"sourcesContent":["import type {\n BrowserUploadInput,\n FlowUploadConfig,\n FlowUploadItem,\n MultiFlowUploadOptions,\n} from \"@uploadista/client-browser\";\nimport type { ReactNode } from \"react\";\nimport { useMultiFlowUpload } from \"../hooks/use-multi-flow-upload\";\n\n/**\n * Render props passed to the FlowUploadList children function.\n * Provides access to upload items, aggregate statistics, and control methods.\n *\n * @property items - All flow upload items in the queue\n * @property totalProgress - Average progress across all uploads (0-100)\n * @property activeUploads - Count of currently uploading items\n * @property completedUploads - Count of successfully completed uploads\n * @property failedUploads - Count of failed uploads\n * @property isUploading - True when any uploads are in progress\n * @property addFiles - Add new files to the upload queue\n * @property removeFile - Remove a specific file from the queue\n * @property startUpload - Begin uploading all pending files\n * @property abortUpload - Cancel a specific active upload\n * @property abortAll - Cancel all active uploads\n * @property clear - Remove all items from the queue\n * @property retryUpload - Retry a specific failed upload\n */\nexport interface FlowUploadListRenderProps {\n /**\n * List of upload items\n */\n items: FlowUploadItem<BrowserUploadInput>[];\n\n /**\n * Total progress across all uploads\n */\n totalProgress: number;\n\n /**\n * Number of active uploads\n */\n activeUploads: number;\n\n /**\n * Number of completed uploads\n */\n completedUploads: number;\n\n /**\n * Number of failed uploads\n */\n failedUploads: number;\n\n /**\n * Whether any uploads are in progress\n */\n isUploading: boolean;\n\n /**\n * Add files to the upload queue\n */\n addFiles: (files: File[] | FileList) => void;\n\n /**\n * Remove a file from the queue\n */\n removeFile: (id: string) => void;\n\n /**\n * Start uploading all pending files\n */\n startUpload: () => void;\n\n /**\n * Abort a specific upload\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all uploads\n */\n abortAll: () => void;\n\n /**\n * Clear all items\n */\n clear: () => void;\n\n /**\n * Retry a failed upload\n */\n retryUpload: (id: string) => void;\n}\n\n/**\n * Props for the FlowUploadList component.\n *\n * @property flowConfig - Flow execution configuration (flowId, storageId, etc.)\n * @property options - Multi-flow upload options (callbacks, concurrency, etc.)\n * @property children - Render function receiving flow upload list state\n */\nexport interface FlowUploadListProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Multi-upload options\n */\n options?: Omit<MultiFlowUploadOptions<BrowserUploadInput>, \"flowConfig\">;\n\n /**\n * Render function for the upload list\n */\n children: (props: FlowUploadListRenderProps) => ReactNode;\n}\n\n/**\n * Headless flow upload list component for managing batch file uploads through a flow.\n * Uses render props pattern to provide complete control over the UI while handling\n * concurrent uploads and flow processing.\n *\n * Each file is uploaded and processed independently through the specified flow,\n * with automatic queue management and concurrency control.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param props - Flow upload list configuration and render prop\n * @returns Rendered flow upload list using the provided render prop\n *\n * @example\n * ```tsx\n * // Batch image processing with custom UI\n * <FlowUploadList\n * flowConfig={{\n * flowId: \"image-batch-processing\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized\",\n * }}\n * options={{\n * maxConcurrent: 3,\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} processed successfully`);\n * },\n * onComplete: (items) => {\n * const successful = items.filter(i => i.status === 'success');\n * console.log(`Batch complete: ${successful.length}/${items.length} successful`);\n * },\n * }}\n * >\n * {({\n * items,\n * totalProgress,\n * activeUploads,\n * completedUploads,\n * failedUploads,\n * addFiles,\n * startUpload,\n * abortUpload,\n * retryUpload,\n * clear,\n * }) => (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * accept=\"image/*\"\n * onChange={(e) => {\n * if (e.target.files) {\n * addFiles(e.target.files);\n * startUpload();\n * }\n * }}\n * />\n *\n * <div style={{ marginTop: '1rem' }}>\n * <h3>Upload Progress</h3>\n * <div>Overall: {totalProgress}%</div>\n * <div>\n * Active: {activeUploads}, Completed: {completedUploads}, Failed: {failedUploads}\n * </div>\n * <button onClick={clear}>Clear All</button>\n * </div>\n *\n * <ul style={{ listStyle: 'none', padding: 0 }}>\n * {items.map((item) => (\n * <li key={item.id} style={{\n * padding: '1rem',\n * border: '1px solid #ccc',\n * marginBottom: '0.5rem'\n * }}>\n * <div>{item.file instanceof File ? item.file.name : 'File'}</div>\n * <div>Status: {item.status}</div>\n *\n * {item.status === \"uploading\" && (\n * <div>\n * <progress value={item.progress} max={100} style={{ width: '100%' }} />\n * <div>{item.progress}%</div>\n * <button onClick={() => abortUpload(item.id)}>Cancel</button>\n * </div>\n * )}\n *\n * {item.status === \"error\" && (\n * <div>\n * <div style={{ color: 'red' }}>{item.error?.message}</div>\n * <button onClick={() => retryUpload(item.id)}>Retry</button>\n * </div>\n * )}\n *\n * {item.status === \"success\" && (\n * <div style={{ color: 'green' }}>✓ Complete</div>\n * )}\n * </li>\n * ))}\n * </ul>\n * </div>\n * )}\n * </FlowUploadList>\n * ```\n *\n * @see {@link SimpleFlowUploadList} for a pre-styled version\n * @see {@link useMultiFlowUpload} for the underlying hook\n */\nexport function FlowUploadList({\n flowConfig,\n options,\n children,\n}: FlowUploadListProps) {\n const multiUpload = useMultiFlowUpload({\n ...options,\n flowConfig,\n });\n\n return (\n <>\n {children({\n items: multiUpload.state.items,\n totalProgress: multiUpload.state.totalProgress,\n activeUploads: multiUpload.state.activeUploads,\n completedUploads: multiUpload.state.completedUploads,\n failedUploads: multiUpload.state.failedUploads,\n isUploading: multiUpload.isUploading,\n addFiles: multiUpload.addFiles,\n removeFile: multiUpload.removeFile,\n startUpload: multiUpload.startUpload,\n abortUpload: multiUpload.abortUpload,\n abortAll: multiUpload.abortAll,\n clear: multiUpload.clear,\n retryUpload: multiUpload.retryUpload,\n })}\n </>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadListItem component.\n *\n * @property item - The flow upload item to display\n * @property onAbort - Called when the abort button is clicked\n * @property onRetry - Called when the retry button is clicked\n * @property onRemove - Called when the remove button is clicked\n */\nexport interface SimpleFlowUploadListItemProps {\n /**\n * Upload item\n */\n item: FlowUploadItem<BrowserUploadInput>;\n\n /**\n * Abort the upload\n */\n onAbort: () => void;\n\n /**\n * Retry the upload\n */\n onRetry: () => void;\n\n /**\n * Remove the item\n */\n onRemove: () => void;\n}\n\n/**\n * Pre-styled flow upload list item component with status indicators.\n * Displays file name, upload progress, status, and contextual action buttons.\n *\n * Features:\n * - Status-specific icons and colors\n * - Progress bar with percentage and byte count\n * - Error message display\n * - Contextual action buttons (cancel, retry, remove)\n *\n * @param props - Upload item and callback functions\n * @returns Styled flow upload list item component\n *\n * @example\n * ```tsx\n * <SimpleFlowUploadListItem\n * item={uploadItem}\n * onAbort={() => console.log('Abort')}\n * onRetry={() => console.log('Retry')}\n * onRemove={() => console.log('Remove')}\n * />\n * ```\n */\nexport function SimpleFlowUploadListItem({\n item,\n onAbort,\n onRetry,\n onRemove,\n}: SimpleFlowUploadListItemProps) {\n const getStatusIcon = () => {\n switch (item.status) {\n case \"success\":\n return \"✓\";\n case \"error\":\n return \"✗\";\n case \"uploading\":\n return \"⟳\";\n case \"aborted\":\n return \"⊘\";\n default:\n return \"○\";\n }\n };\n\n const getStatusColor = () => {\n switch (item.status) {\n case \"success\":\n return \"green\";\n case \"error\":\n return \"red\";\n case \"uploading\":\n return \"blue\";\n case \"aborted\":\n return \"gray\";\n default:\n return \"black\";\n }\n };\n\n return (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"12px\",\n padding: \"8px\",\n borderBottom: \"1px solid #eee\",\n }}\n >\n <span style={{ color: getStatusColor(), fontSize: \"18px\" }}>\n {getStatusIcon()}\n </span>\n\n <div style={{ flex: 1, minWidth: 0 }}>\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: 500,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }}\n >\n {item.file instanceof File ? item.file.name : \"Upload\"}\n </div>\n\n {item.status === \"uploading\" && (\n <div style={{ marginTop: \"4px\" }}>\n <progress\n value={item.progress}\n max={100}\n style={{ width: \"100%\", height: \"4px\" }}\n />\n <div style={{ fontSize: \"12px\", color: \"#666\", marginTop: \"2px\" }}>\n {item.progress}% • {Math.round(item.bytesUploaded / 1024)} KB /{\" \"}\n {Math.round(item.totalBytes / 1024)} KB\n </div>\n </div>\n )}\n\n {item.status === \"error\" && (\n <div style={{ fontSize: \"12px\", color: \"red\", marginTop: \"2px\" }}>\n {item.error?.message || \"Upload failed\"}\n </div>\n )}\n\n {item.status === \"success\" && (\n <div style={{ fontSize: \"12px\", color: \"green\", marginTop: \"2px\" }}>\n Upload complete\n </div>\n )}\n </div>\n\n <div style={{ display: \"flex\", gap: \"8px\" }}>\n {item.status === \"uploading\" && (\n <button\n type=\"button\"\n onClick={onAbort}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n )}\n\n {item.status === \"error\" && (\n <button\n type=\"button\"\n onClick={onRetry}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n )}\n\n {(item.status === \"pending\" ||\n item.status === \"error\" ||\n item.status === \"aborted\") && (\n <button\n type=\"button\"\n onClick={onRemove}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadList component.\n *\n * @property flowConfig - Flow execution configuration\n * @property options - Multi-flow upload options (callbacks, concurrency)\n * @property className - CSS class name for the container\n * @property showFileInput - Whether to display the file input (default: true)\n * @property accept - Accepted file types for the file input\n */\nexport interface SimpleFlowUploadListProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Multi-upload options\n */\n options?: Omit<MultiFlowUploadOptions<BrowserUploadInput>, \"flowConfig\">;\n\n /**\n * CSS class for the container\n */\n className?: string;\n\n /**\n * Show file input\n */\n showFileInput?: boolean;\n\n /**\n * File input accept attribute\n */\n accept?: string;\n}\n\n/**\n * Simple pre-styled flow upload list component with built-in UI.\n * Provides a ready-to-use interface for batch file uploads with flow processing.\n *\n * Features:\n * - Built-in file input\n * - Overall progress display\n * - Individual item progress tracking\n * - Status indicators and action buttons\n * - Automatic upload start on file selection\n *\n * @param props - Flow upload list configuration with styling options\n * @returns Styled flow upload list component\n *\n * @example\n * ```tsx\n * // Basic batch image upload\n * <SimpleFlowUploadList\n * flowConfig={{\n * flowId: \"image-batch-processing\",\n * storageId: \"s3-images\",\n * }}\n * options={{\n * maxConcurrent: 3,\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} processed`);\n * },\n * onComplete: (items) => {\n * console.log(\"Batch complete:\", items.length);\n * },\n * }}\n * accept=\"image/*\"\n * className=\"my-upload-list\"\n * />\n *\n * // Without file input (add files programmatically)\n * <SimpleFlowUploadList\n * flowConfig={{\n * flowId: \"document-processing\",\n * storageId: \"docs\",\n * }}\n * showFileInput={false}\n * options={{\n * maxConcurrent: 2,\n * }}\n * />\n * ```\n *\n * @see {@link FlowUploadList} for the headless version with full control\n */\nexport function SimpleFlowUploadList({\n flowConfig,\n options,\n className = \"\",\n showFileInput = true,\n accept,\n}: SimpleFlowUploadListProps) {\n return (\n <FlowUploadList flowConfig={flowConfig} options={options}>\n {({\n items,\n addFiles,\n startUpload,\n abortUpload,\n retryUpload,\n removeFile,\n totalProgress,\n }) => (\n <div className={className}>\n {showFileInput && (\n <div style={{ marginBottom: \"16px\" }}>\n <input\n type=\"file\"\n multiple\n accept={accept}\n onChange={(e) => {\n if (e.target.files) {\n addFiles(e.target.files);\n startUpload();\n }\n }}\n style={{\n padding: \"8px\",\n border: \"1px solid #ccc\",\n borderRadius: \"4px\",\n }}\n />\n </div>\n )}\n\n {items.length > 0 && (\n <div>\n <div\n style={{ marginBottom: \"8px\", fontSize: \"14px\", color: \"#666\" }}\n >\n Total Progress: {totalProgress}%\n </div>\n\n <div\n style={{\n border: \"1px solid #eee\",\n borderRadius: \"8px\",\n overflow: \"hidden\",\n }}\n >\n {items.map((item) => (\n <SimpleFlowUploadListItem\n key={item.id}\n item={item}\n onAbort={() => abortUpload(item.id)}\n onRetry={() => retryUpload(item.id)}\n onRemove={() => removeFile(item.id)}\n />\n ))}\n </div>\n </div>\n )}\n </div>\n )}\n </FlowUploadList>\n );\n}\n","import type {\n FlowUploadConfig,\n FlowUploadOptions,\n} from \"@uploadista/client-browser\";\nimport type { ReactNode } from \"react\";\nimport { type UseDragDropReturn, useDragDrop } from \"../hooks/use-drag-drop\";\nimport {\n type UseFlowUploadReturn,\n useFlowUpload,\n} from \"../hooks/use-flow-upload\";\n\n/**\n * Render props passed to the FlowUploadZone children function.\n * Provides access to flow upload state, drag-drop handlers, and helper functions.\n *\n * @property dragDrop - Complete drag-and-drop state and handlers\n * @property flowUpload - Flow upload hook with upload state and controls\n * @property isActive - True when dragging over zone\n * @property openFilePicker - Programmatically open file selection dialog\n * @property getRootProps - Returns props to spread on the drop zone container\n * @property getInputProps - Returns props to spread on the hidden file input\n */\nexport interface FlowUploadZoneRenderProps {\n /**\n * Drag and drop state and handlers\n */\n dragDrop: UseDragDropReturn;\n\n /**\n * Flow upload functionality\n */\n flowUpload: UseFlowUploadReturn;\n\n /**\n * Whether the zone is currently active (dragging or uploading)\n */\n isActive: boolean;\n\n /**\n * Open file picker\n */\n openFilePicker: () => void;\n\n /**\n * Props to spread on the drop zone element\n */\n getRootProps: () => {\n onDragEnter: (e: React.DragEvent) => void;\n onDragOver: (e: React.DragEvent) => void;\n onDragLeave: (e: React.DragEvent) => void;\n onDrop: (e: React.DragEvent) => void;\n };\n\n /**\n * Props to spread on the file input element\n */\n getInputProps: () => React.InputHTMLAttributes<HTMLInputElement>;\n}\n\n/**\n * Props for the FlowUploadZone component.\n *\n * @property flowConfig - Flow execution configuration (flowId, storageId, etc.)\n * @property options - Flow upload options (callbacks, metadata, etc.)\n * @property accept - Accepted file types (e.g., \"image/*\", \".pdf\")\n * @property multiple - Allow multiple file selection (default: false)\n * @property children - Render function receiving flow upload zone state\n */\nexport interface FlowUploadZoneProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Upload options\n */\n options?: Omit<FlowUploadOptions, \"flowConfig\">;\n\n /**\n * Accepted file types (e.g., \"image/*\", \".pdf\", etc.)\n */\n accept?: string;\n\n /**\n * Whether to allow multiple files (uses multi-upload internally)\n */\n multiple?: boolean;\n\n /**\n * Render function for the drop zone\n */\n children: (props: FlowUploadZoneRenderProps) => ReactNode;\n}\n\n/**\n * Headless flow upload zone component with drag-and-drop support.\n * Combines drag-drop functionality with flow processing, using render props\n * for complete UI control.\n *\n * Files uploaded through this zone are automatically processed through the\n * specified flow, which can perform operations like image optimization,\n * storage saving, webhooks, etc.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param props - Flow upload zone configuration and render prop\n * @returns Rendered flow upload zone using the provided render prop\n *\n * @example\n * ```tsx\n * // Image upload with flow processing\n * <FlowUploadZone\n * flowConfig={{\n * flowId: \"image-processing-flow\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized-image\",\n * }}\n * options={{\n * onSuccess: (result) => console.log('Processed:', result),\n * onFlowComplete: (outputs) => {\n * console.log('All outputs:', outputs);\n * },\n * }}\n * accept=\"image/*\"\n * >\n * {({ dragDrop, flowUpload, getRootProps, getInputProps, openFilePicker }) => (\n * <div {...getRootProps()} style={{\n * border: dragDrop.state.isDragging ? '2px solid blue' : '2px dashed gray',\n * padding: '2rem',\n * textAlign: 'center'\n * }}>\n * <input {...getInputProps()} />\n *\n * {dragDrop.state.isDragging && (\n * <p>Drop image here...</p>\n * )}\n *\n * {!dragDrop.state.isDragging && !flowUpload.isUploading && (\n * <div>\n * <p>Drag an image or click to browse</p>\n * <button onClick={openFilePicker}>Choose File</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploadingFile && (\n * <div>\n * <p>Uploading...</p>\n * <progress value={flowUpload.state.progress} max={100} />\n * <span>{flowUpload.state.progress}%</span>\n * </div>\n * )}\n *\n * {flowUpload.isProcessing && (\n * <div>\n * <p>Processing image...</p>\n * {flowUpload.state.currentNodeName && (\n * <span>Step: {flowUpload.state.currentNodeName}</span>\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"success\" && (\n * <div>\n * <p>✓ Upload complete!</p>\n * {flowUpload.state.result && (\n * <img src={flowUpload.state.result.url} alt=\"Uploaded\" />\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"error\" && (\n * <div>\n * <p>Error: {flowUpload.state.error?.message}</p>\n * <button onClick={flowUpload.reset}>Try Again</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploading && (\n * <button onClick={flowUpload.abort}>Cancel</button>\n * )}\n * </div>\n * )}\n * </FlowUploadZone>\n * ```\n *\n * @see {@link SimpleFlowUploadZone} for a pre-styled version\n * @see {@link useFlowUpload} for the underlying hook\n */\nexport function FlowUploadZone({\n flowConfig,\n options,\n accept,\n multiple = false,\n children,\n}: FlowUploadZoneProps) {\n // Hook automatically subscribes to events through context\n const flowUpload = useFlowUpload({\n ...options,\n flowConfig,\n });\n\n const dragDrop = useDragDrop({\n onFilesReceived: (files: File[]) => {\n const file = files[0];\n if (file) {\n flowUpload.upload(file);\n }\n },\n accept: accept ? [accept] : undefined,\n multiple,\n });\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n const file = files?.[0];\n if (file) {\n flowUpload.upload(file);\n }\n };\n\n // Determine active state\n const isActive = dragDrop.state.isDragging || dragDrop.state.isOver;\n\n return (\n <>\n {children({\n flowUpload,\n dragDrop,\n isActive,\n openFilePicker: dragDrop.openFilePicker,\n getRootProps: () => dragDrop.dragHandlers,\n getInputProps: () => ({\n ...dragDrop.inputProps,\n onChange: handleFileChange,\n }),\n })}\n </>\n );\n}\n\n/**\n * Props for the SimpleFlowUploadZone component.\n *\n * @property flowConfig - Flow execution configuration\n * @property options - Flow upload options (callbacks, metadata)\n * @property accept - Accepted file types\n * @property className - CSS class name for custom styling\n * @property dragText - Text displayed when dragging files over zone\n * @property idleText - Text displayed when zone is idle\n */\nexport interface SimpleFlowUploadZoneProps {\n /**\n * Flow configuration\n */\n flowConfig: FlowUploadConfig;\n\n /**\n * Upload options\n */\n options?: Omit<FlowUploadOptions, \"flowConfig\">;\n\n /**\n * Accepted file types\n */\n accept?: string;\n\n /**\n * CSS class for the container\n */\n className?: string;\n\n /**\n * Custom drag overlay text\n */\n dragText?: string;\n\n /**\n * Custom idle text\n */\n idleText?: string;\n}\n\n/**\n * Simple pre-styled flow upload zone component with built-in UI.\n * Provides a ready-to-use drag-and-drop interface for flow uploads.\n *\n * Features:\n * - Built-in drag-and-drop visual feedback\n * - Automatic progress display for upload and processing phases\n * - Success and error state display\n * - Cancel button during upload\n * - Customizable text labels\n *\n * @param props - Flow upload zone configuration with styling options\n * @returns Styled flow upload zone component\n *\n * @example\n * ```tsx\n * // Basic image upload with flow processing\n * <SimpleFlowUploadZone\n * flowConfig={{\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * }}\n * accept=\"image/*\"\n * options={{\n * onSuccess: (result) => console.log(\"Image processed:\", result),\n * onError: (error) => console.error(\"Processing failed:\", error),\n * }}\n * idleText=\"Drop an image to optimize and upload\"\n * dragText=\"Release to start processing\"\n * className=\"my-upload-zone\"\n * />\n *\n * // Document upload with custom flow\n * <SimpleFlowUploadZone\n * flowConfig={{\n * flowId: \"document-processing-flow\",\n * storageId: \"docs\",\n * outputNodeId: \"processed-doc\",\n * }}\n * accept=\".pdf,.doc,.docx\"\n * options={{\n * onFlowComplete: (outputs) => {\n * console.log('Processing outputs:', outputs);\n * },\n * }}\n * />\n * ```\n *\n * @see {@link FlowUploadZone} for the headless version with full control\n */\nexport function SimpleFlowUploadZone({\n flowConfig,\n options,\n accept,\n className = \"\",\n dragText = \"Drop files here\",\n idleText = \"Drag & drop files or click to browse\",\n}: SimpleFlowUploadZoneProps) {\n return (\n <FlowUploadZone flowConfig={flowConfig} options={options} accept={accept}>\n {({\n dragDrop,\n flowUpload,\n getRootProps,\n getInputProps,\n openFilePicker,\n }) => (\n <div\n {...getRootProps()}\n className={className}\n style={{\n border: \"2px dashed #ccc\",\n borderRadius: \"8px\",\n padding: \"32px\",\n textAlign: \"center\",\n cursor: \"pointer\",\n backgroundColor: dragDrop.state.isDragging\n ? \"#f0f0f0\"\n : \"transparent\",\n transition: \"background-color 0.2s\",\n }}\n >\n <input {...getInputProps()} />\n\n {dragDrop.state.isDragging && <p style={{ margin: 0 }}>{dragText}</p>}\n\n {!dragDrop.state.isDragging &&\n !flowUpload.isUploading &&\n flowUpload.state.status === \"idle\" && (\n <div>\n <p style={{ margin: \"0 0 16px 0\" }}>{idleText}</p>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n openFilePicker();\n }}\n style={{\n padding: \"8px 16px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Choose Files\n </button>\n </div>\n )}\n\n {flowUpload.isUploading && (\n <div>\n <progress\n value={flowUpload.state.progress}\n max={100}\n style={{ width: \"100%\", height: \"8px\" }}\n />\n <p style={{ margin: \"8px 0 0 0\" }}>\n {flowUpload.state.progress}%\n </p>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n // abort() will be passed from parent\n }}\n style={{\n marginTop: \"8px\",\n padding: \"4px 12px\",\n borderRadius: \"4px\",\n border: \"1px solid #ccc\",\n backgroundColor: \"#fff\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {flowUpload.state.status === \"success\" && (\n <div>\n <p style={{ margin: 0, color: \"green\" }}>✓ Upload complete!</p>\n </div>\n )}\n\n {flowUpload.state.status === \"error\" && (\n <div>\n <p style={{ margin: 0, color: \"red\" }}>\n ✗ Error: {flowUpload.state.error?.message}\n </p>\n </div>\n )}\n </div>\n )}\n </FlowUploadZone>\n );\n}\n","import type React from \"react\";\nimport type {\n UploadItem,\n UseMultiUploadReturn,\n} from \"../hooks/use-multi-upload\";\nimport type { UploadStatus } from \"../hooks/use-upload\";\n\n/**\n * Render props passed to the UploadList children function.\n * Provides organized access to upload items, status groupings, and actions.\n *\n * @property items - All upload items (filtered and sorted if configured)\n * @property itemsByStatus - Upload items grouped by their current status\n * @property multiUpload - Complete multi-upload hook instance\n * @property actions - Helper functions for common item operations\n * @property actions.removeItem - Remove an item from the list\n * @property actions.retryItem - Retry a failed upload\n * @property actions.abortItem - Cancel an active upload\n * @property actions.startItem - Begin uploading an idle item\n */\nexport interface UploadListRenderProps {\n /**\n * All upload items\n */\n items: UploadItem[];\n\n /**\n * Items filtered by status\n */\n itemsByStatus: {\n idle: UploadItem[];\n uploading: UploadItem[];\n success: UploadItem[];\n error: UploadItem[];\n aborted: UploadItem[];\n };\n\n /**\n * Multi-upload state and controls\n */\n multiUpload: UseMultiUploadReturn;\n\n /**\n * Helper functions for item management\n */\n actions: {\n removeItem: (id: string) => void;\n retryItem: (item: UploadItem) => void;\n abortItem: (item: UploadItem) => void;\n startItem: (item: UploadItem) => void;\n };\n}\n\n/**\n * Props for the UploadList component.\n *\n * @property multiUpload - Multi-upload hook instance to display\n * @property filter - Optional function to filter which items to show\n * @property sortBy - Optional comparator function to sort items\n * @property children - Render function receiving list state and actions\n */\nexport interface UploadListProps {\n /**\n * Multi-upload instance from useMultiUpload hook\n */\n multiUpload: UseMultiUploadReturn;\n\n /**\n * Optional filter for which items to display\n */\n filter?: (item: UploadItem) => boolean;\n\n /**\n * Optional sorting function for items\n */\n sortBy?: (a: UploadItem, b: UploadItem) => number;\n\n /**\n * Render prop that receives upload list state and actions\n */\n children: (props: UploadListRenderProps) => React.ReactNode;\n}\n\n/**\n * Headless upload list component that provides flexible rendering for upload items.\n * Uses render props pattern to give full control over how upload items are displayed.\n *\n * @param props - Upload list configuration and render prop\n * @returns Rendered upload list using the provided render prop\n *\n * @example\n * ```tsx\n * // Basic upload list with progress bars\n * <UploadList multiUpload={multiUpload}>\n * {({ items, actions }) => (\n * <div>\n * <h3>Upload Queue ({items.length} files)</h3>\n * {items.map((item) => (\n * <div key={item.id} style={{\n * padding: '1rem',\n * border: '1px solid #ccc',\n * marginBottom: '0.5rem',\n * borderRadius: '4px'\n * }}>\n * <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n * <span>{item.file.name}</span>\n * <span>{item.state.status}</span>\n * </div>\n *\n * {item.state.status === 'uploading' && (\n * <div>\n * <progress value={item.state.progress} max={100} />\n * <span>{item.state.progress}%</span>\n * <button onClick={() => actions.abortItem(item)}>Cancel</button>\n * </div>\n * )}\n *\n * {item.state.status === 'error' && (\n * <div>\n * <p style={{ color: 'red' }}>Error: {item.state.error?.message}</p>\n * <button onClick={() => actions.retryItem(item)}>Retry</button>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n *\n * {item.state.status === 'success' && (\n * <div>\n * <p style={{ color: 'green' }}>✓ Uploaded successfully</p>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n *\n * {item.state.status === 'idle' && (\n * <div>\n * <button onClick={() => actions.startItem(item)}>Start Upload</button>\n * <button onClick={() => actions.removeItem(item.id)}>Remove</button>\n * </div>\n * )}\n * </div>\n * ))}\n * </div>\n * )}\n * </UploadList>\n *\n * // Upload list with status filtering and sorting\n * <UploadList\n * multiUpload={multiUpload}\n * filter={(item) => item.state.status !== 'success'} // Hide successful uploads\n * sortBy={(a, b) => {\n * // Sort by status priority, then by filename\n * const statusPriority = { error: 0, uploading: 1, idle: 2, success: 3, aborted: 4 };\n * const aPriority = statusPriority[a.state.status];\n * const bPriority = statusPriority[b.state.status];\n *\n * if (aPriority !== bPriority) {\n * return aPriority - bPriority;\n * }\n *\n * return a.file.name.localeCompare(b.file.name);\n * }}\n * >\n * {({ items, itemsByStatus, multiUpload, actions }) => (\n * <div>\n * {itemsByStatus.error.length > 0 && (\n * <div>\n * <h4 style={{ color: 'red' }}>Failed Uploads ({itemsByStatus.error.length})</h4>\n * {itemsByStatus.error.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n *\n * {itemsByStatus.uploading.length > 0 && (\n * <div>\n * <h4>Uploading ({itemsByStatus.uploading.length})</h4>\n * {itemsByStatus.uploading.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n *\n * {itemsByStatus.idle.length > 0 && (\n * <div>\n * <h4>Pending ({itemsByStatus.idle.length})</h4>\n * {itemsByStatus.idle.map((item) => (\n * <UploadListItem key={item.id} item={item} actions={actions} />\n * ))}\n * </div>\n * )}\n * </div>\n * )}\n * </UploadList>\n * ```\n */\nexport function UploadList({\n multiUpload,\n filter,\n sortBy,\n children,\n}: UploadListProps) {\n // Apply filtering\n let items = multiUpload.items;\n if (filter) {\n items = items.filter(filter);\n }\n\n // Apply sorting\n if (sortBy) {\n items = [...items].sort(sortBy);\n }\n\n // Group items by status\n const itemsByStatus = {\n idle: items.filter((item) => item.state.status === \"idle\"),\n uploading: items.filter((item) => item.state.status === \"uploading\"),\n success: items.filter((item) => item.state.status === \"success\"),\n error: items.filter((item) => item.state.status === \"error\"),\n aborted: items.filter((item) => item.state.status === \"aborted\"),\n };\n\n // Create action helpers\n const actions = {\n removeItem: (id: string) => {\n multiUpload.removeItem(id);\n },\n retryItem: (_item: UploadItem) => {\n // Retry failed uploads using multiUpload method\n multiUpload.retryFailed();\n },\n abortItem: (item: UploadItem) => {\n // Remove the item to effectively abort it\n multiUpload.removeItem(item.id);\n },\n startItem: (_item: UploadItem) => {\n // Start all pending uploads\n multiUpload.startAll();\n },\n };\n\n // Create render props object\n const renderProps: UploadListRenderProps = {\n items,\n itemsByStatus,\n multiUpload,\n actions,\n };\n\n return <>{children(renderProps)}</>;\n}\n\n/**\n * Props for the SimpleUploadListItem component.\n *\n * @property item - The upload item to display\n * @property actions - Action functions from UploadList render props\n * @property className - Additional CSS class name\n * @property style - Inline styles for the item container\n * @property showDetails - Whether to display file size and upload details\n */\nexport interface SimpleUploadListItemProps {\n /**\n * The upload item to render\n */\n item: UploadItem;\n\n /**\n * Actions from UploadList render props\n */\n actions: UploadListRenderProps[\"actions\"];\n\n /**\n * Additional CSS class name\n */\n className?: string;\n\n /**\n * Inline styles\n */\n style?: React.CSSProperties;\n\n /**\n * Whether to show detailed information (file size, speed, etc.)\n */\n showDetails?: boolean;\n}\n\n/**\n * Pre-styled upload list item component with status indicators and action buttons.\n * Displays file info, progress, errors, and contextual actions based on upload status.\n *\n * Features:\n * - Status-specific color coding and icons\n * - Progress bar for active uploads\n * - Error message display\n * - File size formatting\n * - Contextual action buttons (start, cancel, retry, remove)\n *\n * @param props - Upload item and configuration\n * @returns Styled upload list item component\n *\n * @example\n * ```tsx\n * // Use with UploadList\n * <UploadList multiUpload={multiUpload}>\n * {({ items, actions }) => (\n * <div>\n * {items.map((item) => (\n * <SimpleUploadListItem\n * key={item.id}\n * item={item}\n * actions={actions}\n * showDetails={true}\n * />\n * ))}\n * </div>\n * )}\n * </UploadList>\n *\n * // Custom styling\n * <SimpleUploadListItem\n * item={uploadItem}\n * actions={actions}\n * className=\"my-upload-item\"\n * style={{ borderRadius: '12px', margin: '1rem' }}\n * showDetails={true}\n * />\n * ```\n */\nexport function SimpleUploadListItem({\n item,\n actions,\n className = \"\",\n style = {},\n showDetails = true,\n}: SimpleUploadListItemProps) {\n const getStatusColor = (status: UploadStatus) => {\n switch (status) {\n case \"idle\":\n return \"#6c757d\";\n case \"uploading\":\n return \"#007bff\";\n case \"success\":\n return \"#28a745\";\n case \"error\":\n return \"#dc3545\";\n case \"aborted\":\n return \"#6c757d\";\n default:\n return \"#6c757d\";\n }\n };\n\n const getStatusIcon = (status: UploadStatus) => {\n switch (status) {\n case \"idle\":\n return \"⏳\";\n case \"uploading\":\n return \"📤\";\n case \"success\":\n return \"✅\";\n case \"error\":\n return \"❌\";\n case \"aborted\":\n return \"⏹️\";\n default:\n return \"❓\";\n }\n };\n\n const formatFileSize = (bytes: number) => {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n };\n\n return (\n <div\n className={`upload-list-item upload-list-item--${item.state.status} ${className}`}\n style={{\n padding: \"12px\",\n border: \"1px solid #e0e0e0\",\n borderRadius: \"6px\",\n marginBottom: \"8px\",\n backgroundColor: \"#fff\",\n transition: \"all 0.2s ease\",\n ...style,\n }}\n >\n {/* Header with filename and status */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"8px\",\n }}\n >\n <div\n style={{ display: \"flex\", alignItems: \"center\", gap: \"8px\", flex: 1 }}\n >\n <span style={{ fontSize: \"16px\" }}>\n {getStatusIcon(item.state.status)}\n </span>\n <span style={{ fontWeight: \"500\", flex: 1 }}>\n {item.file instanceof File ? item.file.name : \"File\"}\n </span>\n </div>\n <span\n style={{\n fontSize: \"12px\",\n color: getStatusColor(item.state.status),\n fontWeight: \"500\",\n textTransform: \"uppercase\",\n }}\n >\n {item.state.status}\n </span>\n </div>\n\n {/* Progress bar for uploading items */}\n {item.state.status === \"uploading\" && (\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"4px\",\n }}\n >\n <span style={{ fontSize: \"12px\", color: \"#666\" }}>\n {item.state.progress}%\n </span>\n {showDetails && item.state.totalBytes && (\n <span style={{ fontSize: \"12px\", color: \"#666\" }}>\n {formatFileSize(item.state.bytesUploaded)} /{\" \"}\n {formatFileSize(item.state.totalBytes)}\n </span>\n )}\n </div>\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"#e0e0e0\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n }}\n >\n <div\n style={{\n width: `${item.state.progress}%`,\n height: \"100%\",\n backgroundColor: \"#007bff\",\n transition: \"width 0.2s ease\",\n }}\n />\n </div>\n </div>\n )}\n\n {/* Details section */}\n {showDetails && (\n <div style={{ fontSize: \"12px\", color: \"#666\", marginBottom: \"8px\" }}>\n {item.state.totalBytes && (\n <span>{formatFileSize(item.state.totalBytes)}</span>\n )}\n {item.state.status === \"uploading\" && item.state.progress > 0 && (\n <span> • Progress: {item.state.progress}%</span>\n )}\n {item.state.status === \"error\" && item.state.error && (\n <div style={{ color: \"#dc3545\", marginTop: \"4px\" }}>\n {item.state.error.message}\n </div>\n )}\n </div>\n )}\n\n {/* Action buttons */}\n <div style={{ display: \"flex\", gap: \"8px\", flexWrap: \"wrap\" }}>\n {item.state.status === \"idle\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.startItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #007bff\",\n backgroundColor: \"#007bff\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Start\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n\n {item.state.status === \"uploading\" && (\n <button\n type=\"button\"\n onClick={() => actions.abortItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #dc3545\",\n backgroundColor: \"transparent\",\n color: \"#dc3545\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Cancel\n </button>\n )}\n\n {item.state.status === \"error\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.retryItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #28a745\",\n backgroundColor: \"#28a745\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n\n {item.state.status === \"success\" && (\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n )}\n\n {item.state.status === \"aborted\" && (\n <>\n <button\n type=\"button\"\n onClick={() => actions.retryItem(item)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #007bff\",\n backgroundColor: \"#007bff\",\n color: \"white\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => actions.removeItem(item.id)}\n style={{\n padding: \"4px 8px\",\n fontSize: \"12px\",\n border: \"1px solid #6c757d\",\n backgroundColor: \"transparent\",\n color: \"#6c757d\",\n borderRadius: \"4px\",\n cursor: \"pointer\",\n }}\n >\n Remove\n </button>\n </>\n )}\n </div>\n </div>\n );\n}\n","/**\n * Upload Zone Components\n *\n * Enhanced error handling features:\n * - MIME type validation with detailed error messages\n * - File count validation (single vs multiple mode)\n * - Custom validation error callbacks\n * - Built-in error display in SimpleUploadZone\n * - Configurable error styling\n */\n\nimport type React from \"react\";\nimport { useCallback } from \"react\";\nimport type {\n DragDropOptions,\n UseDragDropReturn,\n} from \"../hooks/use-drag-drop\";\nimport { useDragDrop } from \"../hooks/use-drag-drop\";\nimport type {\n MultiUploadOptions,\n UseMultiUploadReturn,\n} from \"../hooks/use-multi-upload\";\nimport { useMultiUpload } from \"../hooks/use-multi-upload\";\nimport type { UseUploadOptions, UseUploadReturn } from \"../hooks/use-upload\";\nimport { useUpload } from \"../hooks/use-upload\";\n\n/**\n * Render props passed to the UploadZone children function.\n * Provides access to drag-drop state, upload controls, and helper functions.\n *\n * @property dragDrop - Complete drag-and-drop state and event handlers\n * @property upload - Single upload hook (null when multiple=true)\n * @property multiUpload - Multi-upload hook (null when multiple=false)\n * @property openFilePicker - Programmatically trigger file selection dialog\n * @property isActive - True when dragging over zone or files selected\n * @property isProcessing - True when uploads are in progress\n */\nexport interface UploadZoneRenderProps {\n /**\n * Drag and drop state and handlers\n */\n dragDrop: UseDragDropReturn;\n\n /**\n * Single upload functionality (if not using multi-upload)\n */\n upload: UseUploadReturn | null;\n\n /**\n * Multi-upload functionality (if using multi-upload)\n */\n multiUpload: UseMultiUploadReturn | null;\n\n /**\n * Helper function to open file picker\n */\n openFilePicker: () => void;\n\n /**\n * Whether the zone is currently active (dragging or uploading)\n */\n isActive: boolean;\n\n /**\n * Whether files are being processed\n */\n isProcessing: boolean;\n}\n\n/**\n * Props for the UploadZone component.\n * Combines drag-drop options with upload configuration.\n *\n * @property multiple - Enable multi-file selection and upload (default: true)\n * @property multiUploadOptions - Configuration for multi-upload mode\n * @property uploadOptions - Configuration for single-upload mode\n * @property children - Render function receiving upload zone state\n * @property onUploadStart - Called when files pass validation and upload begins\n * @property onValidationError - Called when file validation fails\n * @property accept - Accepted file types (e.g., ['image/*', '.pdf'])\n * @property maxFiles - Maximum number of files allowed\n * @property maxFileSize - Maximum file size in bytes\n * @property validator - Custom validation function\n */\nexport interface UploadZoneProps\n extends Omit<DragDropOptions, \"onFilesReceived\"> {\n /**\n * Whether to enable multi-file upload mode\n */\n multiple?: boolean;\n\n /**\n * Multi-upload specific options (only used when multiple=true)\n */\n multiUploadOptions?: MultiUploadOptions;\n\n /**\n * Single upload specific options (only used when multiple=false)\n */\n uploadOptions?: UseUploadOptions;\n\n /**\n * Render prop that receives upload zone state and handlers\n */\n children: (props: UploadZoneRenderProps) => React.ReactNode;\n\n /**\n * Called when files are processed and uploads begin\n */\n onUploadStart?: (files: File[]) => void;\n\n /**\n * Called when validation errors occur\n */\n onValidationError?: (errors: string[]) => void;\n}\n\n/**\n * Headless upload zone component that combines drag and drop functionality\n * with upload management. Uses render props pattern for maximum flexibility.\n * Includes enhanced error handling for MIME type validation and file count validation.\n *\n * @param props - Upload zone configuration and render prop\n * @returns Rendered upload zone using the provided render prop\n *\n * @example\n * ```tsx\n * // Single file upload zone with error handling\n * <UploadZone\n * multiple={false}\n * accept={['image/*']}\n * maxFileSize={5 * 1024 * 1024}\n * onValidationError={(errors) => {\n * console.error('Validation errors:', errors);\n * }}\n * uploadOptions={{\n * onSuccess: (result) => console.log('Upload complete:', result),\n * onError: (error) => console.error('Upload failed:', error),\n * }}\n * >\n * {({ dragDrop, upload, openFilePicker, isActive }) => (\n * <div {...dragDrop.dragHandlers} onClick={openFilePicker}>\n * {dragDrop.state.isDragging ? (\n * <p>Drop file here...</p>\n * ) : upload?.isUploading ? (\n * <p>Uploading... {upload.state.progress}%</p>\n * ) : (\n * <p>Drag a file here or click to select</p>\n * )}\n *\n * {dragDrop.state.errors.length > 0 && (\n * <div style={{ color: 'red' }}>\n * {dragDrop.state.errors.map((error, index) => (\n * <p key={index}>{error}</p>\n * ))}\n * </div>\n * )}\n *\n * <input {...dragDrop.inputProps} />\n * </div>\n * )}\n * </UploadZone>\n * ```\n */\nexport function UploadZone({\n children,\n multiple = true,\n multiUploadOptions = {},\n uploadOptions = {},\n onUploadStart,\n onValidationError,\n ...dragDropOptions\n}: UploadZoneProps) {\n // Always initialize both hooks, but only use the appropriate one\n const singleUpload = useUpload(uploadOptions);\n const multiUpload = useMultiUpload(multiUploadOptions);\n\n // Enhanced validation function for better error handling\n const enhancedValidator = useCallback(\n (files: File[]): string[] | null => {\n const errors: string[] = [];\n\n // Check file count based on multiple setting\n if (!multiple && files.length > 1) {\n errors.push(\n `Single file mode is enabled. Please select only one file. You selected ${files.length} files.`,\n );\n }\n\n // Enhanced MIME type validation with better error messages\n if (dragDropOptions.accept && dragDropOptions.accept.length > 0) {\n const invalidFiles = files.filter((file) => {\n return !dragDropOptions.accept?.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n } else {\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n } else {\n return file.type === acceptType;\n }\n }\n });\n });\n\n if (invalidFiles.length > 0) {\n const fileNames = invalidFiles\n .map((f) => `\"${f.name}\" (${f.type})`)\n .join(\", \");\n const acceptedTypes = dragDropOptions.accept.join(\", \");\n errors.push(\n `Invalid file type(s): ${fileNames}. Accepted types: ${acceptedTypes}.`,\n );\n }\n }\n\n return errors.length > 0 ? errors : null;\n },\n [multiple, dragDropOptions.accept],\n );\n\n // Handle file processing\n const handleFilesReceived = (files: File[]) => {\n onUploadStart?.(files);\n\n if (multiple && multiUpload) {\n // Add files to multi-upload queue\n multiUpload.addFiles(files);\n\n // Auto-start uploads if configured to do so\n // Note: This could be made configurable with an autoStart prop\n setTimeout(() => multiUpload.startAll(), 0);\n } else if (!multiple && singleUpload && files.length > 0 && files[0]) {\n // Start single file upload\n singleUpload.upload(files[0]);\n }\n };\n\n // Handle validation errors\n const handleValidationError = useCallback(\n (errors: string[]) => {\n console.error(\"Upload zone validation errors:\", errors);\n // Call the custom error handler if provided\n onValidationError?.(errors);\n },\n [onValidationError],\n );\n\n // Initialize drag and drop with enhanced validation\n const dragDrop = useDragDrop({\n ...dragDropOptions,\n multiple,\n validator: enhancedValidator,\n onFilesReceived: handleFilesReceived,\n onValidationError: handleValidationError,\n });\n\n // Determine active state\n const isActive = dragDrop.state.isDragging || dragDrop.state.isOver;\n\n // Determine processing state\n const isProcessing = multiple\n ? (multiUpload?.state.isUploading ?? false)\n : (singleUpload?.isUploading ?? false);\n\n // Create render props object\n const renderProps: UploadZoneRenderProps = {\n dragDrop,\n upload: singleUpload,\n multiUpload,\n openFilePicker: dragDrop.openFilePicker,\n isActive,\n isProcessing,\n };\n\n return <>{children(renderProps)}</>;\n}\n\n/**\n * Props for the SimpleUploadZone component with built-in styling.\n *\n * @property className - CSS class name for custom styling\n * @property style - Inline styles for the upload zone container\n * @property text - Custom text labels for different states\n * @property text.idle - Text shown when zone is idle\n * @property text.dragging - Text shown when dragging files over zone\n * @property text.uploading - Text shown during upload\n * @property errorStyle - Custom styles for validation error display\n */\nexport interface SimpleUploadZoneProps extends UploadZoneProps {\n /**\n * Additional CSS class name for styling\n */\n className?: string;\n\n /**\n * Inline styles for the upload zone\n */\n style?: React.CSSProperties;\n\n /**\n * Custom text to display in different states\n */\n text?: {\n idle?: string;\n dragging?: string;\n uploading?: string;\n };\n\n /**\n * Custom error message styling\n */\n errorStyle?: React.CSSProperties;\n}\n\n/**\n * Simple pre-styled upload zone component with built-in UI and error handling.\n * Provides a ready-to-use drag-and-drop upload interface with minimal configuration.\n *\n * Features:\n * - Built-in drag-and-drop visual feedback\n * - Automatic progress display\n * - File validation error display\n * - Customizable text and styling\n * - Keyboard accessible\n *\n * @param props - Upload zone configuration with styling options\n * @returns Styled upload zone component\n *\n * @example\n * ```tsx\n * // Multi-file upload with validation\n * <SimpleUploadZone\n * multiple={true}\n * accept={['image/*', '.pdf']}\n * maxFiles={5}\n * maxFileSize={10 * 1024 * 1024} // 10MB\n * onUploadStart={(files) => console.log('Starting uploads:', files.length)}\n * onValidationError={(errors) => {\n * errors.forEach(err => console.error(err));\n * }}\n * multiUploadOptions={{\n * maxConcurrent: 3,\n * onComplete: (results) => {\n * console.log(`${results.successful.length}/${results.total} uploaded`);\n * },\n * }}\n * style={{\n * width: '400px',\n * height: '200px',\n * margin: '20px auto',\n * }}\n * text={{\n * idle: 'Drop your files here or click to browse',\n * dragging: 'Release to upload',\n * uploading: 'Uploading files...',\n * }}\n * errorStyle={{\n * backgroundColor: '#fff3cd',\n * borderColor: '#ffeaa7',\n * }}\n * />\n *\n * // Single file upload\n * <SimpleUploadZone\n * multiple={false}\n * accept={['image/*']}\n * uploadOptions={{\n * onSuccess: (result) => console.log('Uploaded:', result),\n * onError: (error) => console.error('Failed:', error),\n * }}\n * text={{\n * idle: 'Click or drag an image to upload',\n * }}\n * />\n * ```\n */\nexport function SimpleUploadZone({\n className = \"\",\n style = {},\n text = {},\n errorStyle = {},\n children,\n ...uploadZoneProps\n}: SimpleUploadZoneProps) {\n const defaultText = {\n idle: uploadZoneProps.multiple\n ? \"Drag files here or click to select\"\n : \"Drag a file here or click to select\",\n dragging: uploadZoneProps.multiple\n ? \"Drop files here...\"\n : \"Drop file here...\",\n uploading: \"Uploading...\",\n };\n\n const displayText = { ...defaultText, ...text };\n\n // If children render prop is provided, use UploadZone directly\n if (children) {\n return <UploadZone {...uploadZoneProps}>{children}</UploadZone>;\n }\n\n // Otherwise, provide default UI\n return (\n <UploadZone {...uploadZoneProps}>\n {({\n dragDrop,\n upload,\n multiUpload,\n openFilePicker,\n isActive,\n isProcessing,\n }) => (\n <button\n type=\"button\"\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n openFilePicker();\n }\n }}\n onKeyUp={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n openFilePicker();\n }\n }}\n {...dragDrop.dragHandlers}\n onClick={openFilePicker}\n className={`upload-zone ${isActive ? \"upload-zone--active\" : \"\"} ${isProcessing ? \"upload-zone--processing\" : \"\"} ${className}`}\n style={{\n border: isActive ? \"2px dashed #007bff\" : \"2px dashed #ccc\",\n borderRadius: \"8px\",\n padding: \"2rem\",\n textAlign: \"center\",\n cursor: \"pointer\",\n backgroundColor: isActive ? \"#f8f9fa\" : \"transparent\",\n transition: \"all 0.2s ease\",\n minHeight: \"120px\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n ...style,\n }}\n >\n {dragDrop.state.isDragging ? (\n <p style={{ margin: 0, fontSize: \"16px\", color: \"#007bff\" }}>\n {displayText.dragging}\n </p>\n ) : isProcessing ? (\n <div style={{ textAlign: \"center\" }}>\n <p style={{ margin: \"0 0 10px 0\", fontSize: \"14px\" }}>\n {displayText.uploading}\n </p>\n {upload && (\n <div>\n <progress\n value={upload.state.progress}\n max={100}\n style={{ width: \"200px\", height: \"8px\" }}\n />\n <p\n style={{\n margin: \"5px 0 0 0\",\n fontSize: \"12px\",\n color: \"#666\",\n }}\n >\n {upload.state.progress}%\n </p>\n </div>\n )}\n {multiUpload && (\n <div>\n <progress\n value={multiUpload.state.progress}\n max={100}\n style={{ width: \"200px\", height: \"8px\" }}\n />\n <p\n style={{\n margin: \"5px 0 0 0\",\n fontSize: \"12px\",\n color: \"#666\",\n }}\n >\n {multiUpload.state.progress}% ({multiUpload.state.uploading}{\" \"}\n uploading, {multiUpload.state.successful} completed)\n </p>\n </div>\n )}\n </div>\n ) : (\n <p style={{ margin: 0, fontSize: \"16px\", color: \"#666\" }}>\n {displayText.idle}\n </p>\n )}\n\n {dragDrop.state.errors.length > 0 && (\n <div\n style={{\n marginTop: \"10px\",\n padding: \"8px 12px\",\n backgroundColor: \"#f8d7da\",\n border: \"1px solid #f5c6cb\",\n borderRadius: \"4px\",\n maxWidth: \"100%\",\n ...errorStyle,\n }}\n >\n <p\n style={{\n margin: \"0 0 5px 0\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: \"#721c24\",\n }}\n >\n Validation Errors:\n </p>\n {dragDrop.state.errors.map((error, index) => (\n <p\n // biome-ignore lint/suspicious/noArrayIndexKey: index is used as key\n key={index}\n style={{\n color: \"#721c24\",\n fontSize: \"11px\",\n margin: \"2px 0\",\n lineHeight: \"1.3\",\n }}\n >\n • {error}\n </p>\n ))}\n </div>\n )}\n\n <input {...dragDrop.inputProps} />\n </button>\n )}\n </UploadZone>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createContext as e,useCallback as t,useContext as n,useEffect as r,useMemo as i,useRef as a,useState as o}from"react";import{createUploadistaClient as s}from"@uploadista/client-browser";import{jsx as c}from"react/jsx-runtime";import{FlowManager as l,UploadManager as u}from"@uploadista/client-core";import{EventType as d}from"@uploadista/core/flow";import{UploadEventType as f}from"@uploadista/core/types";function p(e){let t=a(e);return t.current=e,{client:i(()=>s({baseUrl:e.baseUrl,storageId:e.storageId,uploadistaBasePath:e.uploadistaBasePath,chunkSize:e.chunkSize,storeFingerprintForResuming:e.storeFingerprintForResuming,retryDelays:e.retryDelays,parallelUploads:e.parallelUploads,parallelChunkSize:e.parallelChunkSize,uploadStrategy:e.uploadStrategy,smartChunking:e.smartChunking,networkMonitoring:e.networkMonitoring,uploadMetrics:e.uploadMetrics,connectionPooling:e.connectionPooling,auth:e.auth,onEvent:e.onEvent}),[e]),config:e}}const m=e(null);function h({children:e,...n}){let r=a(new Set),o=t(e=>{console.log(`[UploadistaProvider] Received event:`,e),n.onEvent?.(e),console.log(`[UploadistaProvider] Broadcasting to`,r.current.size,`subscribers`),r.current.forEach(t=>{try{t(e)}catch(e){console.error(`Error in event subscriber:`,e)}})},[n.onEvent]),s=p({...n,onEvent:o}),l=t(e=>(r.current.add(e),()=>{r.current.delete(e)}),[]),u=i(()=>({...s,subscribeToEvents:l}),[s,l]);return c(m.Provider,{value:u,children:e})}function g(){let e=n(m);if(e===null)throw Error(`useUploadistaContext must be used within an UploadistaProvider. Make sure to wrap your component tree with <UploadistaProvider>.`);return e}function _(e){let n=g(),[r,i]=o([]),s=a(new Map),c=a([]),l=a(0),u=e.maxConcurrent??3,d=t(e=>{if(e.length===0)return 0;let t=e.reduce((e,t)=>e+t.progress,0);return Math.round(t/e.length)},[]),f=t(async()=>{if(l.current>=u||c.current.length===0)return;let t=c.current.shift();if(!t)return;let a=r.find(e=>e.id===t);if(!a||a.status!==`pending`){f();return}l.current++,i(e=>e.map(e=>e.id===t?{...e,status:`uploading`}:e));try{let{abort:r,jobId:o}=await n.client.uploadWithFlow(a.file,e.flowConfig,{onJobStart:e=>{i(n=>n.map(n=>n.id===t?{...n,jobId:e}:n))},onProgress:(n,r,a)=>{let o=a?Math.round(r/a*100):0;i(n=>{let i=n.map(e=>e.id===t?{...e,progress:o,bytesUploaded:r,totalBytes:a||0}:e),s=i.find(e=>e.id===t);return s&&e.onItemProgress?.(s),i})},onSuccess:n=>{i(r=>{let i=r.map(e=>e.id===t?{...e,status:`success`,result:n,progress:100}:e),a=i.find(e=>e.id===t);return a&&e.onItemSuccess?.(a),i.every(e=>e.status===`success`||e.status===`error`||e.status===`aborted`)&&e.onComplete?.(i),i}),s.current.delete(t),l.current--,f()},onError:n=>{i(r=>{let i=r.map(e=>e.id===t?{...e,status:`error`,error:n}:e),a=i.find(e=>e.id===t);return a&&e.onItemError?.(a,n),i.every(e=>e.status===`success`||e.status===`error`||e.status===`aborted`)&&e.onComplete?.(i),i}),s.current.delete(t),l.current--,f()},onShouldRetry:e.onShouldRetry});s.current.set(t,r),i(e=>e.map(e=>e.id===t?{...e,jobId:o}:e))}catch(e){i(n=>n.map(n=>n.id===t?{...n,status:`error`,error:e}:n)),l.current--,f()}},[n,r,u,e]),p=t(e=>{let t=Array.from(e).map(e=>({id:`${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:e,status:`pending`,progress:0,bytesUploaded:0,totalBytes:e.size,error:null,result:null,jobId:null}));i(e=>[...e,...t])},[]),m=t(e=>{let t=s.current.get(e);t&&(t(),s.current.delete(e)),i(t=>t.filter(t=>t.id!==e)),c.current=c.current.filter(t=>t!==e)},[]),h=t(()=>{let e=r.filter(e=>e.status===`pending`);c.current.push(...e.map(e=>e.id));for(let e=0;e<u;e++)f()},[r,u,f]),_=t(e=>{let t=s.current.get(e);t&&(t(),s.current.delete(e),i(t=>t.map(t=>t.id===e?{...t,status:`aborted`}:t)),l.current--,f())},[f]),v=t(()=>{for(let e of s.current.values())e();s.current.clear(),c.current=[],l.current=0,i(e=>e.map(e=>e.status===`uploading`?{...e,status:`aborted`}:e))},[]),y=t(()=>{v(),i([])},[v]),b=t(e=>{i(t=>t.map(t=>t.id===e?{...t,status:`pending`,progress:0,bytesUploaded:0,error:null}:t)),c.current.push(e),f()},[f]),x={items:r,totalProgress:d(r),activeUploads:r.filter(e=>e.status===`uploading`).length,completedUploads:r.filter(e=>e.status===`success`).length,failedUploads:r.filter(e=>e.status===`error`).length};return{state:x,addFiles:p,removeFile:m,startUpload:h,abortUpload:_,abortAll:v,clear:y,retryUpload:b,isUploading:x.activeUploads>0}}const v={isDragging:!1,isOver:!1,isValid:!0,errors:[]};function y(e={}){let{accept:n,maxFiles:r,maxFileSize:i,multiple:s=!0,validator:c,onFilesReceived:l,onValidationError:u,onDragStateChange:d}=e,[f,p]=o(v),m=a(null),h=a(0),g=t(e=>{p(t=>({...t,...e}))},[]),_=t(e=>{let t=[];r&&e.length>r&&t.push(`Maximum ${r} files allowed. You selected ${e.length} files.`);for(let r of e){if(i&&r.size>i){let e=(i/(1024*1024)).toFixed(1),n=(r.size/(1024*1024)).toFixed(1);t.push(`File "${r.name}" (${n}MB) exceeds maximum size of ${e}MB.`)}n&&n.length>0&&(n.some(e=>{if(e.startsWith(`.`))return r.name.toLowerCase().endsWith(e.toLowerCase());if(e.endsWith(`/*`)){let t=e.slice(0,-2);return r.type.startsWith(t)}else return r.type===e})||t.push(`File "${r.name}" type "${r.type}" is not accepted. Accepted types: ${n.join(`, `)}.`))}if(c){let n=c(e);n&&t.push(...n)}return t},[n,r,i,c]),y=t(e=>{let t=Array.from(e),n=_(t);n.length>0?(g({errors:n,isValid:!1}),u?.(n)):(g({errors:[],isValid:!0}),l?.(t))},[_,g,l,u]),b=t(e=>{let t=[];if(e.items)for(let n=0;n<e.items.length;n++){let r=e.items[n];if(r&&r.kind===`file`){let e=r.getAsFile();e&&t.push(e)}}else for(let n=0;n<e.files.length;n++){let r=e.files[n];r&&t.push(r)}return t},[]),x=t(e=>{e.preventDefault(),e.stopPropagation(),h.current++,h.current===1&&(g({isDragging:!0,isOver:!0}),d?.(!0))},[g,d]),S=t(e=>{e.preventDefault(),e.stopPropagation(),e.dataTransfer&&(e.dataTransfer.dropEffect=`copy`)},[]),C=t(e=>{e.preventDefault(),e.stopPropagation(),h.current--,h.current===0&&(g({isDragging:!1,isOver:!1,errors:[]}),d?.(!1))},[g,d]),w=t(e=>{if(e.preventDefault(),e.stopPropagation(),h.current=0,g({isDragging:!1,isOver:!1}),d?.(!1),e.dataTransfer){let t=b(e.dataTransfer);t.length>0&&y(t)}},[g,d,b,y]),T=t(()=>{m.current?.click()},[]),E=t(e=>{e.target.files&&e.target.files.length>0&&y(Array.from(e.target.files)),e.target.value=``},[y]),D=t(()=>{p(v),h.current=0},[]);return{state:f,dragHandlers:{onDragEnter:x,onDragOver:S,onDragLeave:C,onDrop:w},inputProps:{type:`file`,multiple:s,accept:n?.join(`, `),onChange:E,style:{display:`none`},ref:m},openFilePicker:T,processFiles:y,reset:D}}function b(e){let t=e;return t.eventType===d.FlowStart||t.eventType===d.FlowEnd||t.eventType===d.FlowError||t.eventType===d.NodeStart||t.eventType===d.NodeEnd||t.eventType===d.NodePause||t.eventType===d.NodeResume||t.eventType===d.NodeError}const x={status:`idle`,progress:0,bytesUploaded:0,totalBytes:null,error:null,result:null,jobId:null,flowStarted:!1,currentNodeName:null,currentNodeType:null,flowOutputs:null};function S(e){let n=g(),[i,s]=o(x),c=a(null);return r(()=>(c.current=new l(async(e,t,r)=>{let i=await n.client.uploadWithFlow(e,t,{onJobStart:r.onJobStart,onProgress:r.onProgress,onChunkComplete:r.onChunkComplete,onSuccess:r.onSuccess,onError:r.onError,onShouldRetry:r.onShouldRetry});return{abort:async()=>{await i.abort()},pause:async()=>{await i.pause()}}},{onStateChange:s,onProgress:e.onProgress,onChunkComplete:e.onChunkComplete,onFlowComplete:e.onFlowComplete,onSuccess:e.onSuccess,onError:e.onError,onAbort:e.onAbort},e),()=>{c.current?.cleanup()}),[n]),r(()=>n.subscribeToEvents(e=>{if(b(e)){c.current?.handleFlowEvent(e);return}let t=e;if(t.type===f.UPLOAD_PROGRESS&&t.flow?.jobId===c.current?.getJobId()&&t.data){let{progress:e,total:n}=t.data;c.current?.handleUploadProgress(t.data.id,e,n)}}),[n]),{state:i,upload:t(async e=>{await c.current?.upload(e)},[]),abort:t(()=>{c.current?.abort()},[]),pause:t(()=>{c.current?.pause()},[]),reset:t(()=>{c.current?.reset()},[]),isUploading:i.status===`uploading`||i.status===`processing`,isUploadingFile:i.status===`uploading`,isProcessing:i.status===`processing`}}function C(e={}){let n=g(),{maxConcurrent:r=3}=e,[i,s]=o([]),c=a([]),l=a(0),u=a(new Set),d=a(new Map);c.current=i;let f=t(()=>`upload-${Date.now()}-${l.current++}`,[]),p=t((e,t)=>{s(n=>{let r=n.map(n=>n.id===e?{...n,state:{...n.state,...t}}:n);return c.current=r,r})},[]),m=t(()=>{let t=c.current;if(t.every(e=>[`success`,`error`,`aborted`].includes(e.state.status))&&t.length>0){let n=t.filter(e=>e.state.status===`success`),r=t.filter(e=>[`error`,`aborted`].includes(e.state.status));e.onComplete?.({successful:n,failed:r,total:t.length})}},[e]),h=t(()=>{if(u.current.size>=r)return;let t=c.current.find(e=>e.state.status===`idle`&&!u.current.has(e.id));t&&(async()=>{u.current.add(t.id),e.onUploadStart?.(t),p(t.id,{status:`uploading`});try{let r=await n.client.upload(t.file,{metadata:e.metadata,uploadLengthDeferred:e.uploadLengthDeferred,uploadSize:e.uploadSize,onProgress:(n,r,i)=>{let a=i?Math.round(r/i*100):0;p(t.id,{progress:a,bytesUploaded:r,totalBytes:i}),e.onUploadProgress?.(t,a,r,i)},onChunkComplete:()=>{},onSuccess:n=>{p(t.id,{status:`success`,result:n,progress:100});let r={...t,state:{...t.state,status:`success`,result:n}};e.onUploadSuccess?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()},onError:n=>{p(t.id,{status:`error`,error:n});let r={...t,state:{...t.state,status:`error`,error:n}};e.onUploadError?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()},onShouldRetry:e.onShouldRetry});d.current.set(t.id,r)}catch(n){p(t.id,{status:`error`,error:n});let r={...t,state:{...t.state,status:`error`,error:n}};e.onUploadError?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()}})()},[r,n,e,p,m]),_={total:i.length,completed:i.filter(e=>[`success`,`error`,`aborted`].includes(e.state.status)).length,successful:i.filter(e=>e.state.status===`success`).length,failed:i.filter(e=>[`error`,`aborted`].includes(e.state.status)).length,uploading:i.filter(e=>e.state.status===`uploading`).length,progress:i.length>0?Math.round(i.reduce((e,t)=>e+t.state.progress,0)/i.length):0,totalBytesUploaded:i.reduce((e,t)=>e+t.state.bytesUploaded,0),totalBytes:i.reduce((e,t)=>e+(t.state.totalBytes||0),0),isUploading:i.some(e=>e.state.status===`uploading`),isComplete:i.length>0&&i.every(e=>[`success`,`error`,`aborted`].includes(e.state.status))},v=t(e=>{let t=e.map(e=>({id:f(),file:e,state:{status:`idle`,progress:0,bytesUploaded:0,totalBytes:e instanceof File?e.size:null,error:null,result:null}}));console.log(`addFiles: Adding`,t.length,`files`);let n=[...c.current,...t];c.current=n,console.log(`addFiles: Updated itemsRef.current to`,n.length,`items`),s(n)},[f]),y=t(e=>{let t=c.current.find(t=>t.id===e);if(t&&t.state.status===`uploading`){let t=d.current.get(e);t&&(t.abort(),d.current.delete(e))}s(t=>{let n=t.filter(t=>t.id!==e);return c.current=n,n}),u.current.delete(e)},[]),b=t(e=>{let t=c.current.find(t=>t.id===e);if(t&&t.state.status===`uploading`){let t=d.current.get(e);t&&(t.abort(),d.current.delete(e)),u.current.delete(e),s(t=>{let n=t.map(t=>t.id===e?{...t,state:{...t.state,status:`aborted`}}:t);return c.current=n,n}),h()}},[h]),x=t(e=>{let t=c.current.find(t=>t.id===e);t&&[`error`,`aborted`].includes(t.state.status)&&(s(t=>{let n=t.map(t=>t.id===e?{...t,state:{...t.state,status:`idle`,error:null}}:t);return c.current=n,n}),setTimeout(()=>h(),0))},[h]),S=t(()=>{let e=c.current;console.log(`Starting all uploads`,e);let t=e.filter(e=>e.state.status===`idle`),n=r-u.current.size,i=t.slice(0,n);for(let e of i)console.log(`Starting next upload`,e),h()},[r,h]),C=t(()=>{c.current.filter(e=>e.state.status===`uploading`).forEach(e=>{let t=d.current.get(e.id);t&&(t.abort(),d.current.delete(e.id))}),u.current.clear(),s(e=>{let t=e.map(e=>e.state.status===`uploading`?{...e,state:{...e.state,status:`aborted`}}:e);return c.current=t,t})},[]);return{state:_,items:i,addFiles:v,removeItem:y,removeFile:y,startAll:S,abortUpload:b,abortAll:C,retryUpload:x,retryFailed:t(()=>{let e=c.current.filter(e=>[`error`,`aborted`].includes(e.state.status));e.length>0&&(s(t=>{let n=t.map(t=>e.some(e=>e.id===t.id)?{...t,state:{...t.state,status:`idle`,error:null}}:t);return c.current=n,n}),setTimeout(S,0))},[S]),clearCompleted:t(()=>{s(e=>{let t=e.filter(e=>![`success`,`error`,`aborted`].includes(e.state.status));return c.current=t,t})},[]),clearAll:t(()=>{C(),s([]),c.current=[],u.current.clear()},[C]),getItemsByStatus:t(e=>c.current.filter(t=>t.state.status===e),[]),metrics:{getInsights:()=>n.client.getChunkingInsights(),exportMetrics:()=>n.client.exportMetrics(),getNetworkMetrics:()=>n.client.getNetworkMetrics(),getNetworkCondition:()=>n.client.getNetworkCondition(),resetMetrics:()=>n.client.resetMetrics()}}}const w={status:`idle`,progress:0,bytesUploaded:0,totalBytes:null,error:null,result:null};function T(e={}){let n=g(),[i,s]=o(w),c=a(null);return r(()=>(c.current=new u((e,t)=>n.client.upload(e,t),{onStateChange:s,onProgress:e.onProgress,onChunkComplete:e.onChunkComplete,onSuccess:e.onSuccess,onError:e.onError,onAbort:e.onAbort},{metadata:e.metadata,uploadLengthDeferred:e.uploadLengthDeferred,uploadSize:e.uploadSize,onShouldRetry:e.onShouldRetry}),()=>{c.current?.cleanup()}),[n,e]),{state:i,upload:t(e=>{c.current?.upload(e)},[]),abort:t(()=>{c.current?.abort()},[]),reset:t(()=>{c.current?.reset()},[]),retry:t(()=>{c.current?.retry()},[]),isUploading:i.status===`uploading`,canRetry:c.current?.canRetry()??!1,metrics:{getInsights:()=>n.client.getChunkingInsights(),exportMetrics:()=>n.client.exportMetrics(),getNetworkMetrics:()=>n.client.getNetworkMetrics(),getNetworkCondition:()=>n.client.getNetworkCondition(),resetMetrics:()=>n.client.resetMetrics()}}}export{_ as a,p as c,y as i,C as n,h as o,S as r,g as s,T as t};
|
|
2
|
-
//# sourceMappingURL=use-upload-
|
|
1
|
+
import{createContext as e,useCallback as t,useContext as n,useEffect as r,useMemo as i,useRef as a,useState as o}from"react";import{createUploadistaClient as s}from"@uploadista/client-browser";import{jsx as c}from"react/jsx-runtime";import{FlowManager as l,UploadManager as u}from"@uploadista/client-core";import{EventType as d}from"@uploadista/core/flow";import{UploadEventType as f}from"@uploadista/core/types";function p(e){let t=a(e);return t.current=e,{client:i(()=>s({baseUrl:e.baseUrl,storageId:e.storageId,uploadistaBasePath:e.uploadistaBasePath,chunkSize:e.chunkSize,storeFingerprintForResuming:e.storeFingerprintForResuming,retryDelays:e.retryDelays,parallelUploads:e.parallelUploads,parallelChunkSize:e.parallelChunkSize,uploadStrategy:e.uploadStrategy,smartChunking:e.smartChunking,networkMonitoring:e.networkMonitoring,uploadMetrics:e.uploadMetrics,connectionPooling:e.connectionPooling,auth:e.auth,onEvent:e.onEvent}),[e]),config:e}}const m=e(null);function h({children:e,...n}){let r=a(new Set),o=t(e=>{console.log(`[UploadistaProvider] Received event:`,e),n.onEvent?.(e),console.log(`[UploadistaProvider] Broadcasting to`,r.current.size,`subscribers`),r.current.forEach(t=>{try{t(e)}catch(e){console.error(`Error in event subscriber:`,e)}})},[n.onEvent]),s=p({...n,onEvent:o}),l=t(e=>(r.current.add(e),()=>{r.current.delete(e)}),[]),u=i(()=>({...s,subscribeToEvents:l}),[s,l]);return c(m.Provider,{value:u,children:e})}function g(){let e=n(m);if(e===null)throw Error(`useUploadistaContext must be used within an UploadistaProvider. Make sure to wrap your component tree with <UploadistaProvider>.`);return e}function _(e){let n=g(),[r,i]=o([]),s=a(new Map),c=a([]),l=a(0),u=e.maxConcurrent??3,d=t(e=>{if(e.length===0)return 0;let t=e.reduce((e,t)=>e+t.progress,0);return Math.round(t/e.length)},[]),f=t(async()=>{if(l.current>=u||c.current.length===0)return;let t=c.current.shift();if(!t)return;let a=r.find(e=>e.id===t);if(!a||a.status!==`pending`){f();return}l.current++,i(e=>e.map(e=>e.id===t?{...e,status:`uploading`}:e));try{let{abort:r,jobId:o}=await n.client.uploadWithFlow(a.file,e.flowConfig,{onJobStart:e=>{i(n=>n.map(n=>n.id===t?{...n,jobId:e}:n))},onProgress:(n,r,a)=>{let o=a?Math.round(r/a*100):0;i(n=>{let i=n.map(e=>e.id===t?{...e,progress:o,bytesUploaded:r,totalBytes:a||0}:e),s=i.find(e=>e.id===t);return s&&e.onItemProgress?.(s),i})},onSuccess:n=>{i(r=>{let i=r.map(e=>e.id===t?{...e,status:`success`,result:n,progress:100}:e),a=i.find(e=>e.id===t);return a&&e.onItemSuccess?.(a),i.every(e=>e.status===`success`||e.status===`error`||e.status===`aborted`)&&e.onComplete?.(i),i}),s.current.delete(t),l.current--,f()},onError:n=>{i(r=>{let i=r.map(e=>e.id===t?{...e,status:`error`,error:n}:e),a=i.find(e=>e.id===t);return a&&e.onItemError?.(a,n),i.every(e=>e.status===`success`||e.status===`error`||e.status===`aborted`)&&e.onComplete?.(i),i}),s.current.delete(t),l.current--,f()},onShouldRetry:e.onShouldRetry});s.current.set(t,r),i(e=>e.map(e=>e.id===t?{...e,jobId:o}:e))}catch(e){i(n=>n.map(n=>n.id===t?{...n,status:`error`,error:e}:n)),l.current--,f()}},[n,r,u,e]),p=t(e=>{let t=Array.from(e).map(e=>({id:`${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:e,status:`pending`,progress:0,bytesUploaded:0,totalBytes:e.size,error:null,result:null,jobId:null}));i(e=>[...e,...t])},[]),m=t(e=>{let t=s.current.get(e);t&&(t(),s.current.delete(e)),i(t=>t.filter(t=>t.id!==e)),c.current=c.current.filter(t=>t!==e)},[]),h=t(()=>{let e=r.filter(e=>e.status===`pending`);c.current.push(...e.map(e=>e.id));for(let e=0;e<u;e++)f()},[r,u,f]),_=t(e=>{let t=s.current.get(e);t&&(t(),s.current.delete(e),i(t=>t.map(t=>t.id===e?{...t,status:`aborted`}:t)),l.current--,f())},[f]),v=t(()=>{for(let e of s.current.values())e();s.current.clear(),c.current=[],l.current=0,i(e=>e.map(e=>e.status===`uploading`?{...e,status:`aborted`}:e))},[]),y=t(()=>{v(),i([])},[v]),b=t(e=>{i(t=>t.map(t=>t.id===e?{...t,status:`pending`,progress:0,bytesUploaded:0,error:null}:t)),c.current.push(e),f()},[f]),x={items:r,totalProgress:d(r),activeUploads:r.filter(e=>e.status===`uploading`).length,completedUploads:r.filter(e=>e.status===`success`).length,failedUploads:r.filter(e=>e.status===`error`).length};return{state:x,addFiles:p,removeFile:m,startUpload:h,abortUpload:_,abortAll:v,clear:y,retryUpload:b,isUploading:x.activeUploads>0}}const v={isDragging:!1,isOver:!1,isValid:!0,errors:[]};function y(e={}){let{accept:n,maxFiles:r,maxFileSize:i,multiple:s=!0,validator:c,onFilesReceived:l,onValidationError:u,onDragStateChange:d}=e,[f,p]=o(v),m=a(null),h=a(0),g=t(e=>{p(t=>({...t,...e}))},[]),_=t(e=>{let t=[];r&&e.length>r&&t.push(`Maximum ${r} files allowed. You selected ${e.length} files.`);for(let r of e){if(i&&r.size>i){let e=(i/(1024*1024)).toFixed(1),n=(r.size/(1024*1024)).toFixed(1);t.push(`File "${r.name}" (${n}MB) exceeds maximum size of ${e}MB.`)}n&&n.length>0&&(n.some(e=>{if(e.startsWith(`.`))return r.name.toLowerCase().endsWith(e.toLowerCase());if(e.endsWith(`/*`)){let t=e.slice(0,-2);return r.type.startsWith(t)}else return r.type===e})||t.push(`File "${r.name}" type "${r.type}" is not accepted. Accepted types: ${n.join(`, `)}.`))}if(c){let n=c(e);n&&t.push(...n)}return t},[n,r,i,c]),y=t(e=>{let t=Array.from(e),n=_(t);n.length>0?(g({errors:n,isValid:!1}),u?.(n)):(g({errors:[],isValid:!0}),l?.(t))},[_,g,l,u]),b=t(e=>{let t=[];if(e.items)for(let n=0;n<e.items.length;n++){let r=e.items[n];if(r&&r.kind===`file`){let e=r.getAsFile();e&&t.push(e)}}else for(let n=0;n<e.files.length;n++){let r=e.files[n];r&&t.push(r)}return t},[]),x=t(e=>{e.preventDefault(),e.stopPropagation(),h.current++,h.current===1&&(g({isDragging:!0,isOver:!0}),d?.(!0))},[g,d]),S=t(e=>{e.preventDefault(),e.stopPropagation(),e.dataTransfer&&(e.dataTransfer.dropEffect=`copy`)},[]),C=t(e=>{e.preventDefault(),e.stopPropagation(),h.current--,h.current===0&&(g({isDragging:!1,isOver:!1,errors:[]}),d?.(!1))},[g,d]),w=t(e=>{if(e.preventDefault(),e.stopPropagation(),h.current=0,g({isDragging:!1,isOver:!1}),d?.(!1),e.dataTransfer){let t=b(e.dataTransfer);t.length>0&&y(t)}},[g,d,b,y]),T=t(()=>{m.current?.click()},[]),E=t(e=>{e.target.files&&e.target.files.length>0&&y(Array.from(e.target.files)),e.target.value=``},[y]),D=t(()=>{p(v),h.current=0},[]);return{state:f,dragHandlers:{onDragEnter:x,onDragOver:S,onDragLeave:C,onDrop:w},inputProps:{type:`file`,multiple:s,accept:n?.join(`, `),onChange:E,style:{display:`none`},ref:m},openFilePicker:T,processFiles:y,reset:D}}function b(e){let t=e;return t.eventType===d.FlowStart||t.eventType===d.FlowEnd||t.eventType===d.FlowError||t.eventType===d.NodeStart||t.eventType===d.NodeEnd||t.eventType===d.NodePause||t.eventType===d.NodeResume||t.eventType===d.NodeError}const x={status:`idle`,progress:0,bytesUploaded:0,totalBytes:null,error:null,result:null,jobId:null,flowStarted:!1,currentNodeName:null,currentNodeType:null,flowOutputs:null};function S(e){let n=g(),[i,s]=o(x),c=a(null);return r(()=>(c.current=new l(async(e,t,r)=>{let i=await n.client.uploadWithFlow(e,t,{onJobStart:r.onJobStart,onProgress:r.onProgress,onChunkComplete:r.onChunkComplete,onSuccess:r.onSuccess,onError:r.onError,onShouldRetry:r.onShouldRetry});return{abort:async()=>{await i.abort()},pause:async()=>{await i.pause()}}},{onStateChange:s,onProgress:e.onProgress,onChunkComplete:e.onChunkComplete,onFlowComplete:e.onFlowComplete,onSuccess:e.onSuccess,onError:e.onError,onAbort:e.onAbort},e),()=>{c.current?.cleanup()}),[n]),r(()=>n.subscribeToEvents(e=>{if(b(e)){console.log(`[useFlowUpload] Flow event received:`,e.eventType,`jobId:`,e.jobId,`current:`,c.current?.getJobId()),c.current?.handleFlowEvent(e);return}let t=e;if(t.type===f.UPLOAD_PROGRESS&&t.flow?.jobId===c.current?.getJobId()&&t.data){let{progress:e,total:n}=t.data;c.current?.handleUploadProgress(t.data.id,e,n)}}),[n]),{state:i,upload:t(async e=>{await c.current?.upload(e)},[]),abort:t(()=>{c.current?.abort()},[]),pause:t(()=>{c.current?.pause()},[]),reset:t(()=>{c.current?.reset()},[]),isUploading:i.status===`uploading`||i.status===`processing`,isUploadingFile:i.status===`uploading`,isProcessing:i.status===`processing`}}function C(e={}){let n=g(),{maxConcurrent:r=3}=e,[i,s]=o([]),c=a([]),l=a(0),u=a(new Set),d=a(new Map);c.current=i;let f=t(()=>`upload-${Date.now()}-${l.current++}`,[]),p=t((e,t)=>{s(n=>{let r=n.map(n=>n.id===e?{...n,state:{...n.state,...t}}:n);return c.current=r,r})},[]),m=t(()=>{let t=c.current;if(t.every(e=>[`success`,`error`,`aborted`].includes(e.state.status))&&t.length>0){let n=t.filter(e=>e.state.status===`success`),r=t.filter(e=>[`error`,`aborted`].includes(e.state.status));e.onComplete?.({successful:n,failed:r,total:t.length})}},[e]),h=t(()=>{if(u.current.size>=r)return;let t=c.current.find(e=>e.state.status===`idle`&&!u.current.has(e.id));t&&(async()=>{u.current.add(t.id),e.onUploadStart?.(t),p(t.id,{status:`uploading`});try{let r=await n.client.upload(t.file,{metadata:e.metadata,uploadLengthDeferred:e.uploadLengthDeferred,uploadSize:e.uploadSize,onProgress:(n,r,i)=>{let a=i?Math.round(r/i*100):0;p(t.id,{progress:a,bytesUploaded:r,totalBytes:i}),e.onUploadProgress?.(t,a,r,i)},onChunkComplete:()=>{},onSuccess:n=>{p(t.id,{status:`success`,result:n,progress:100});let r={...t,state:{...t.state,status:`success`,result:n}};e.onUploadSuccess?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()},onError:n=>{p(t.id,{status:`error`,error:n});let r={...t,state:{...t.state,status:`error`,error:n}};e.onUploadError?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()},onShouldRetry:e.onShouldRetry});d.current.set(t.id,r)}catch(n){p(t.id,{status:`error`,error:n});let r={...t,state:{...t.state,status:`error`,error:n}};e.onUploadError?.(r,n),u.current.delete(t.id),d.current.delete(t.id),h(),m()}})()},[r,n,e,p,m]),_={total:i.length,completed:i.filter(e=>[`success`,`error`,`aborted`].includes(e.state.status)).length,successful:i.filter(e=>e.state.status===`success`).length,failed:i.filter(e=>[`error`,`aborted`].includes(e.state.status)).length,uploading:i.filter(e=>e.state.status===`uploading`).length,progress:i.length>0?Math.round(i.reduce((e,t)=>e+t.state.progress,0)/i.length):0,totalBytesUploaded:i.reduce((e,t)=>e+t.state.bytesUploaded,0),totalBytes:i.reduce((e,t)=>e+(t.state.totalBytes||0),0),isUploading:i.some(e=>e.state.status===`uploading`),isComplete:i.length>0&&i.every(e=>[`success`,`error`,`aborted`].includes(e.state.status))},v=t(e=>{let t=e.map(e=>({id:f(),file:e,state:{status:`idle`,progress:0,bytesUploaded:0,totalBytes:e instanceof File?e.size:null,error:null,result:null}}));console.log(`addFiles: Adding`,t.length,`files`);let n=[...c.current,...t];c.current=n,console.log(`addFiles: Updated itemsRef.current to`,n.length,`items`),s(n)},[f]),y=t(e=>{let t=c.current.find(t=>t.id===e);if(t&&t.state.status===`uploading`){let t=d.current.get(e);t&&(t.abort(),d.current.delete(e))}s(t=>{let n=t.filter(t=>t.id!==e);return c.current=n,n}),u.current.delete(e)},[]),b=t(e=>{let t=c.current.find(t=>t.id===e);if(t&&t.state.status===`uploading`){let t=d.current.get(e);t&&(t.abort(),d.current.delete(e)),u.current.delete(e),s(t=>{let n=t.map(t=>t.id===e?{...t,state:{...t.state,status:`aborted`}}:t);return c.current=n,n}),h()}},[h]),x=t(e=>{let t=c.current.find(t=>t.id===e);t&&[`error`,`aborted`].includes(t.state.status)&&(s(t=>{let n=t.map(t=>t.id===e?{...t,state:{...t.state,status:`idle`,error:null}}:t);return c.current=n,n}),setTimeout(()=>h(),0))},[h]),S=t(()=>{let e=c.current;console.log(`Starting all uploads`,e);let t=e.filter(e=>e.state.status===`idle`),n=r-u.current.size,i=t.slice(0,n);for(let e of i)console.log(`Starting next upload`,e),h()},[r,h]),C=t(()=>{c.current.filter(e=>e.state.status===`uploading`).forEach(e=>{let t=d.current.get(e.id);t&&(t.abort(),d.current.delete(e.id))}),u.current.clear(),s(e=>{let t=e.map(e=>e.state.status===`uploading`?{...e,state:{...e.state,status:`aborted`}}:e);return c.current=t,t})},[]);return{state:_,items:i,addFiles:v,removeItem:y,removeFile:y,startAll:S,abortUpload:b,abortAll:C,retryUpload:x,retryFailed:t(()=>{let e=c.current.filter(e=>[`error`,`aborted`].includes(e.state.status));e.length>0&&(s(t=>{let n=t.map(t=>e.some(e=>e.id===t.id)?{...t,state:{...t.state,status:`idle`,error:null}}:t);return c.current=n,n}),setTimeout(S,0))},[S]),clearCompleted:t(()=>{s(e=>{let t=e.filter(e=>![`success`,`error`,`aborted`].includes(e.state.status));return c.current=t,t})},[]),clearAll:t(()=>{C(),s([]),c.current=[],u.current.clear()},[C]),getItemsByStatus:t(e=>c.current.filter(t=>t.state.status===e),[]),metrics:{getInsights:()=>n.client.getChunkingInsights(),exportMetrics:()=>n.client.exportMetrics(),getNetworkMetrics:()=>n.client.getNetworkMetrics(),getNetworkCondition:()=>n.client.getNetworkCondition(),resetMetrics:()=>n.client.resetMetrics()}}}const w={status:`idle`,progress:0,bytesUploaded:0,totalBytes:null,error:null,result:null};function T(e={}){let n=g(),[i,s]=o(w),c=a(null);return r(()=>(c.current=new u((e,t)=>n.client.upload(e,t),{onStateChange:s,onProgress:e.onProgress,onChunkComplete:e.onChunkComplete,onSuccess:e.onSuccess,onError:e.onError,onAbort:e.onAbort},{metadata:e.metadata,uploadLengthDeferred:e.uploadLengthDeferred,uploadSize:e.uploadSize,onShouldRetry:e.onShouldRetry}),()=>{c.current?.cleanup()}),[n,e]),{state:i,upload:t(e=>{c.current?.upload(e)},[]),abort:t(()=>{c.current?.abort()},[]),reset:t(()=>{c.current?.reset()},[]),retry:t(()=>{c.current?.retry()},[]),isUploading:i.status===`uploading`,canRetry:c.current?.canRetry()??!1,metrics:{getInsights:()=>n.client.getChunkingInsights(),exportMetrics:()=>n.client.exportMetrics(),getNetworkMetrics:()=>n.client.getNetworkMetrics(),getNetworkCondition:()=>n.client.getNetworkCondition(),resetMetrics:()=>n.client.resetMetrics()}}}export{_ as a,p as c,y as i,C as n,h as o,S as r,g as s,T as t};
|
|
2
|
+
//# sourceMappingURL=use-upload-CbeBq3kV.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-upload-xcqz090n.mjs","names":["items","newItems: FlowUploadItem<BrowserUploadInput>[]","state: MultiFlowUploadState<BrowserUploadInput>","initialState: DragDropState","initialState","errors: string[]","files: File[]","initialState: FlowUploadState","initialState","state","state: MultiUploadState","newItems: UploadItem[]","item","initialState: UploadState"],"sources":["../src/hooks/use-uploadista-client.ts","../src/components/uploadista-provider.tsx","../src/hooks/use-multi-flow-upload.ts","../src/hooks/use-drag-drop.ts","../src/hooks/use-flow-upload.ts","../src/hooks/use-multi-upload.ts","../src/hooks/use-upload.ts"],"sourcesContent":["import {\n createUploadistaClient,\n type UploadistaClientOptions,\n} from \"@uploadista/client-browser\";\nimport { useMemo, useRef } from \"react\";\n\n/**\n * Configuration options for the uploadista client hook.\n * Extends the base client options with React-specific behavior.\n *\n * @property onEvent - Global event handler for all upload and flow events\n * @property baseUrl - API base URL for uploads\n * @property storageId - Default storage identifier\n * @property chunkSize - Size of upload chunks in bytes\n * @property storeFingerprintForResuming - Enable resumable uploads\n * @property retryDelays - Array of retry delays in milliseconds\n * @property parallelUploads - Maximum number of parallel uploads\n * @property uploadStrategy - Upload strategy (sequential, parallel, adaptive)\n * @property smartChunking - Enable dynamic chunk size adjustment\n * @property networkMonitoring - Enable network condition monitoring\n */\nexport interface UseUploadistaClientOptions extends UploadistaClientOptions {\n /**\n * Global event handler for all upload and flow events from this client\n */\n onEvent?: UploadistaClientOptions[\"onEvent\"];\n}\n\n/**\n * Return value from the useUploadistaClient hook.\n *\n * @property client - Configured uploadista client instance (stable across re-renders)\n * @property config - Current client configuration options\n */\nexport interface UseUploadistaClientReturn {\n /**\n * The uploadista client instance\n */\n client: ReturnType<typeof createUploadistaClient>;\n\n /**\n * Current configuration of the client\n */\n config: UseUploadistaClientOptions;\n}\n\n/**\n * React hook for creating and managing an uploadista client instance.\n * The client instance is memoized and stable across re-renders, only being\n * recreated when configuration options change.\n *\n * This hook is typically used internally by UploadistaProvider, but can be\n * used directly for advanced use cases requiring multiple client instances.\n *\n * @param options - Upload client configuration options\n * @returns Object containing the stable client instance and current configuration\n *\n * @example\n * ```tsx\n * // Basic client setup\n * function MyUploadComponent() {\n * const { client, config } = useUploadistaClient({\n * baseUrl: 'https://api.example.com',\n * storageId: 'default-storage',\n * chunkSize: 1024 * 1024, // 1MB chunks\n * storeFingerprintForResuming: true,\n * onEvent: (event) => {\n * console.log('Upload event:', event);\n * }\n * });\n *\n * // Use client directly\n * const handleUpload = async (file: File) => {\n * await client.upload(file, {\n * onSuccess: (result) => console.log('Uploaded:', result),\n * onError: (error) => console.error('Failed:', error),\n * });\n * };\n *\n * return <FileUploader onUpload={handleUpload} />;\n * }\n *\n * // Advanced: Multiple clients with different configurations\n * function MultiClientComponent() {\n * // Client for image uploads\n * const imageClient = useUploadistaClient({\n * baseUrl: 'https://images.example.com',\n * storageId: 'images',\n * chunkSize: 2 * 1024 * 1024, // 2MB for images\n * });\n *\n * // Client for document uploads\n * const docClient = useUploadistaClient({\n * baseUrl: 'https://docs.example.com',\n * storageId: 'documents',\n * chunkSize: 512 * 1024, // 512KB for documents\n * });\n *\n * return (\n * <div>\n * <ImageUploader client={imageClient.client} />\n * <DocumentUploader client={docClient.client} />\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link UploadistaProvider} for the recommended way to provide client context\n */\nexport function useUploadistaClient(\n options: UseUploadistaClientOptions,\n): UseUploadistaClientReturn {\n // Store the options in a ref to enable stable dependency checking\n const optionsRef = useRef<UseUploadistaClientOptions>(options);\n\n // Update ref on each render but only create new client when essential deps change\n optionsRef.current = options;\n\n // Create client instance with stable identity\n const client = useMemo(() => {\n return createUploadistaClient({\n baseUrl: options.baseUrl,\n storageId: options.storageId,\n uploadistaBasePath: options.uploadistaBasePath,\n chunkSize: options.chunkSize,\n storeFingerprintForResuming: options.storeFingerprintForResuming,\n retryDelays: options.retryDelays,\n parallelUploads: options.parallelUploads,\n parallelChunkSize: options.parallelChunkSize,\n uploadStrategy: options.uploadStrategy,\n smartChunking: options.smartChunking,\n networkMonitoring: options.networkMonitoring,\n uploadMetrics: options.uploadMetrics,\n connectionPooling: options.connectionPooling,\n // logger: options.logger,\n auth: options.auth,\n onEvent: options.onEvent,\n });\n }, [options]);\n\n return {\n client,\n config: options,\n };\n}\n","\"use client\";\nimport type { UploadistaEvent } from \"@uploadista/client-browser\";\nimport type React from \"react\";\nimport { createContext, useCallback, useContext, useMemo, useRef } from \"react\";\nimport {\n type UseUploadistaClientOptions,\n type UseUploadistaClientReturn,\n useUploadistaClient,\n} from \"../hooks/use-uploadista-client\";\n\n/**\n * Props for the UploadistaProvider component.\n * Combines client configuration options with React children.\n *\n * @property children - React components that will have access to the upload client context\n * @property baseUrl - API base URL for uploads\n * @property storageId - Default storage identifier\n * @property chunkSize - Upload chunk size in bytes\n * @property onEvent - Global event handler for all upload events\n * @property ... - All other UploadistaClientOptions\n */\nexport interface UploadistaProviderProps extends UseUploadistaClientOptions {\n /**\n * Children components that will have access to the upload client\n */\n children: React.ReactNode;\n}\n\ntype UploadistaContextValue = UseUploadistaClientReturn & {\n /**\n * Subscribe to events (used internally by hooks)\n * @internal\n */\n subscribeToEvents: (handler: (event: UploadistaEvent) => void) => () => void;\n};\n\nconst UploadistaContext = createContext<UploadistaContextValue | null>(null);\n\n/**\n * Context provider that provides uploadista client functionality to child components.\n * This eliminates the need to pass upload client configuration down through props\n * and ensures a single, shared upload client instance across your application.\n *\n * @param props - Upload client options and children\n * @returns Provider component with upload client context\n *\n * @example\n * ```tsx\n * // Wrap your app with the upload provider\n * function App() {\n * return (\n * <UploadistaProvider\n * baseUrl=\"https://api.example.com\"\n * storageId=\"my-storage\"\n * chunkSize={1024 * 1024} // 1MB chunks\n * onEvent={(event) => {\n * console.log('Global upload event:', event);\n * }}\n * >\n * <UploadInterface />\n * </UploadistaProvider>\n * );\n * }\n *\n * // Use the upload client in any child component\n * function UploadInterface() {\n * const uploadClient = useUploadistaContext();\n * const upload = useUpload(uploadClient);\n * const dragDrop = useDragDrop({\n * onFilesReceived: (files) => {\n * files.forEach(file => upload.upload(file));\n * }\n * });\n *\n * return (\n * <div {...dragDrop.dragHandlers}>\n * <p>Drop files here to upload</p>\n * {upload.isUploading && <p>Progress: {upload.state.progress}%</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function UploadistaProvider({\n children,\n ...options\n}: UploadistaProviderProps) {\n const eventSubscribersRef = useRef<Set<(event: UploadistaEvent) => void>>(\n new Set(),\n );\n\n // Wrap the original onEvent to broadcast to subscribers\n const wrappedOnEvent = useCallback(\n (event: UploadistaEvent) => {\n console.log(\"[UploadistaProvider] Received event:\", event);\n\n // Call original handler if provided\n options.onEvent?.(event);\n\n // Broadcast to all subscribers\n console.log(\n \"[UploadistaProvider] Broadcasting to\",\n eventSubscribersRef.current.size,\n \"subscribers\",\n );\n eventSubscribersRef.current.forEach((handler) => {\n try {\n handler(event);\n } catch (err) {\n console.error(\"Error in event subscriber:\", err);\n }\n });\n },\n [options.onEvent],\n );\n\n const uploadClient = useUploadistaClient({\n ...options,\n onEvent: wrappedOnEvent,\n });\n\n const subscribeToEvents = useCallback(\n (handler: (event: UploadistaEvent) => void) => {\n eventSubscribersRef.current.add(handler);\n return () => {\n eventSubscribersRef.current.delete(handler);\n };\n },\n [],\n );\n\n // Memoize the context value to prevent unnecessary re-renders\n const contextValue = useMemo(\n () => ({\n ...uploadClient,\n subscribeToEvents,\n }),\n [uploadClient, subscribeToEvents],\n );\n\n return (\n <UploadistaContext.Provider value={contextValue}>\n {children}\n </UploadistaContext.Provider>\n );\n}\n\n/**\n * Hook to access the uploadista client from the UploadistaProvider context.\n * Must be used within an UploadistaProvider component.\n *\n * @returns Upload client instance from context\n * @throws Error if used outside of UploadistaProvider\n *\n * @example\n * ```tsx\n * function FileUploader() {\n * const uploadClient = useUploadistaContext();\n * const upload = useUpload(uploadClient);\n *\n * return (\n * <button\n * onClick={() => {\n * const input = document.createElement('input');\n * input.type = 'file';\n * input.onchange = (e) => {\n * const file = (e.target as HTMLInputElement).files?.[0];\n * if (file) upload.upload(file);\n * };\n * input.click();\n * }}\n * >\n * Upload File\n * </button>\n * );\n * }\n * ```\n */\nexport function useUploadistaContext(): UploadistaContextValue {\n const context = useContext(UploadistaContext);\n\n if (context === null) {\n throw new Error(\n \"useUploadistaContext must be used within an UploadistaProvider. \" +\n \"Make sure to wrap your component tree with <UploadistaProvider>.\",\n );\n }\n\n return context;\n}\n","import type {\n BrowserUploadInput,\n FlowUploadItem,\n MultiFlowUploadOptions,\n MultiFlowUploadState,\n} from \"@uploadista/client-browser\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n/**\n * Return value from the useMultiFlowUpload hook with batch upload control methods.\n *\n * @property state - Aggregated state across all flow upload items\n * @property addFiles - Add new files to the upload queue\n * @property removeFile - Remove a file from the queue (aborts if uploading)\n * @property startUpload - Begin uploading all pending files\n * @property abortUpload - Cancel a specific upload by its ID\n * @property abortAll - Cancel all active uploads\n * @property clear - Remove all items and abort active uploads\n * @property retryUpload - Retry a specific failed upload\n * @property isUploading - True when any uploads are in progress\n */\nexport interface UseMultiFlowUploadReturn {\n /**\n * Current upload state\n */\n state: MultiFlowUploadState<BrowserUploadInput>;\n\n /**\n * Add files to upload queue\n */\n addFiles: (files: File[] | FileList) => void;\n\n /**\n * Remove a file from the queue\n */\n removeFile: (id: string) => void;\n\n /**\n * Start uploading all pending files\n */\n startUpload: () => void;\n\n /**\n * Abort a specific upload by ID\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all active uploads\n */\n abortAll: () => void;\n\n /**\n * Clear all items (aborts any active uploads first)\n */\n clear: () => void;\n\n /**\n * Retry a specific failed upload by ID\n */\n retryUpload: (id: string) => void;\n\n /**\n * Whether uploads are in progress\n */\n isUploading: boolean;\n}\n\n/**\n * React hook for uploading multiple files through a flow with concurrent upload management.\n * Processes each file through the specified flow while respecting concurrency limits.\n *\n * Each file is uploaded and processed independently through the flow, with automatic\n * queue management. Failed uploads can be retried individually, and uploads can be\n * aborted at any time.\n *\n * Must be used within an UploadistaProvider. Flow events for each upload are automatically\n * tracked and synchronized.\n *\n * @param options - Multi-flow upload configuration including flow config and concurrency settings\n * @returns Multi-flow upload state and control methods\n *\n * @example\n * ```tsx\n * // Batch image upload with progress tracking\n * function BatchImageUploader() {\n * const multiFlowUpload = useMultiFlowUpload({\n * flowConfig: {\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * },\n * maxConcurrent: 3, // Process 3 files at a time\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} uploaded successfully`);\n * },\n * onItemError: (item, error) => {\n * console.error(`${item.file.name} failed:`, error);\n * },\n * onComplete: (items) => {\n * const successful = items.filter(i => i.status === 'success');\n * const failed = items.filter(i => i.status === 'error');\n * console.log(`Batch complete: ${successful.length} successful, ${failed.length} failed`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * accept=\"image/*\"\n * onChange={(e) => {\n * if (e.target.files) {\n * multiFlowUpload.addFiles(e.target.files);\n * multiFlowUpload.startUpload();\n * }\n * }}\n * />\n *\n * <div>\n * <p>Overall Progress: {multiFlowUpload.state.totalProgress}%</p>\n * <p>\n * {multiFlowUpload.state.activeUploads} uploading,\n * {multiFlowUpload.state.completedUploads} completed,\n * {multiFlowUpload.state.failedUploads} failed\n * </p>\n * </div>\n *\n * <div>\n * <button onClick={multiFlowUpload.startUpload} disabled={multiFlowUpload.isUploading}>\n * Start All\n * </button>\n * <button onClick={multiFlowUpload.abortAll} disabled={!multiFlowUpload.isUploading}>\n * Cancel All\n * </button>\n * <button onClick={multiFlowUpload.clear}>\n * Clear List\n * </button>\n * </div>\n *\n * {multiFlowUpload.state.items.map((item) => (\n * <div key={item.id} style={{\n * border: '1px solid #ccc',\n * padding: '1rem',\n * marginBottom: '0.5rem'\n * }}>\n * <div>{item.file instanceof File ? item.file.name : 'File'}</div>\n * <div>Status: {item.status}</div>\n *\n * {item.status === \"uploading\" && (\n * <div>\n * <progress value={item.progress} max={100} />\n * <span>{item.progress}%</span>\n * <button onClick={() => multiFlowUpload.abortUpload(item.id)}>\n * Cancel\n * </button>\n * </div>\n * )}\n *\n * {item.status === \"error\" && (\n * <div>\n * <p style={{ color: 'red' }}>{item.error?.message}</p>\n * <button onClick={() => multiFlowUpload.retryUpload(item.id)}>\n * Retry\n * </button>\n * <button onClick={() => multiFlowUpload.removeFile(item.id)}>\n * Remove\n * </button>\n * </div>\n * )}\n *\n * {item.status === \"success\" && (\n * <div>\n * <p style={{ color: 'green' }}>✓ Upload complete</p>\n * <button onClick={() => multiFlowUpload.removeFile(item.id)}>\n * Remove\n * </button>\n * </div>\n * )}\n * </div>\n * ))}\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link useFlowUpload} for single file flow uploads\n * @see {@link useMultiUpload} for multi-file uploads without flow processing\n */\nexport function useMultiFlowUpload(\n options: MultiFlowUploadOptions<BrowserUploadInput>,\n): UseMultiFlowUploadReturn {\n const client = useUploadistaContext();\n const [items, setItems] = useState<FlowUploadItem<BrowserUploadInput>[]>([]);\n const abortFnsRef = useRef<Map<string, () => void>>(new Map());\n const queueRef = useRef<string[]>([]);\n const activeCountRef = useRef(0);\n\n const maxConcurrent = options.maxConcurrent ?? 3;\n\n const calculateTotalProgress = useCallback(\n (items: FlowUploadItem<BrowserUploadInput>[]) => {\n if (items.length === 0) return 0;\n const totalProgress = items.reduce((sum, item) => sum + item.progress, 0);\n return Math.round(totalProgress / items.length);\n },\n [],\n );\n\n const processQueue = useCallback(async () => {\n if (\n activeCountRef.current >= maxConcurrent ||\n queueRef.current.length === 0\n ) {\n return;\n }\n\n const itemId = queueRef.current.shift();\n if (!itemId) return;\n\n const item = items.find((i) => i.id === itemId);\n if (!item || item.status !== \"pending\") {\n processQueue();\n return;\n }\n\n activeCountRef.current++;\n\n setItems((prev) =>\n prev.map((i) =>\n i.id === itemId ? { ...i, status: \"uploading\" as const } : i,\n ),\n );\n\n try {\n const { abort, jobId } = await client.client.uploadWithFlow(\n item.file,\n options.flowConfig,\n {\n onJobStart: (jobId: string) => {\n setItems((prev) =>\n prev.map((i) => (i.id === itemId ? { ...i, jobId } : i)),\n );\n },\n onProgress: (\n _uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => {\n const progress = totalBytes\n ? Math.round((bytesUploaded / totalBytes) * 100)\n : 0;\n\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId\n ? {\n ...i,\n progress,\n bytesUploaded,\n totalBytes: totalBytes || 0,\n }\n : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemProgress?.(updatedItem);\n }\n return updated;\n });\n },\n onSuccess: (result: UploadFile) => {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId\n ? { ...i, status: \"success\" as const, result, progress: 100 }\n : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemSuccess?.(updatedItem);\n }\n\n // Check if all uploads are complete\n const allComplete = updated.every(\n (i) =>\n i.status === \"success\" ||\n i.status === \"error\" ||\n i.status === \"aborted\",\n );\n if (allComplete) {\n options.onComplete?.(updated);\n }\n\n return updated;\n });\n\n abortFnsRef.current.delete(itemId);\n activeCountRef.current--;\n processQueue();\n },\n onError: (error: Error) => {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId ? { ...i, status: \"error\" as const, error } : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemError?.(updatedItem, error);\n }\n\n // Check if all uploads are complete\n const allComplete = updated.every(\n (i) =>\n i.status === \"success\" ||\n i.status === \"error\" ||\n i.status === \"aborted\",\n );\n if (allComplete) {\n options.onComplete?.(updated);\n }\n\n return updated;\n });\n\n abortFnsRef.current.delete(itemId);\n activeCountRef.current--;\n processQueue();\n },\n onShouldRetry: options.onShouldRetry,\n },\n );\n\n abortFnsRef.current.set(itemId, abort);\n\n setItems((prev) =>\n prev.map((i) => (i.id === itemId ? { ...i, jobId } : i)),\n );\n } catch (error) {\n setItems((prev) =>\n prev.map((i) =>\n i.id === itemId\n ? { ...i, status: \"error\" as const, error: error as Error }\n : i,\n ),\n );\n\n activeCountRef.current--;\n processQueue();\n }\n }, [client, items, maxConcurrent, options]);\n\n const addFiles = useCallback((files: File[] | FileList) => {\n const fileArray = Array.from(files);\n const newItems: FlowUploadItem<BrowserUploadInput>[] = fileArray.map(\n (file) => ({\n id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n file,\n status: \"pending\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: file.size,\n error: null,\n result: null,\n jobId: null,\n }),\n );\n\n setItems((prev) => [...prev, ...newItems]);\n }, []);\n\n const removeFile = useCallback((id: string) => {\n const abortFn = abortFnsRef.current.get(id);\n if (abortFn) {\n abortFn();\n abortFnsRef.current.delete(id);\n }\n\n setItems((prev) => prev.filter((item) => item.id !== id));\n queueRef.current = queueRef.current.filter((queueId) => queueId !== id);\n }, []);\n\n const startUpload = useCallback(() => {\n const pendingItems = items.filter((item) => item.status === \"pending\");\n queueRef.current.push(...pendingItems.map((item) => item.id));\n\n for (let i = 0; i < maxConcurrent; i++) {\n processQueue();\n }\n }, [items, maxConcurrent, processQueue]);\n\n const abortUpload = useCallback(\n (id: string) => {\n const abortFn = abortFnsRef.current.get(id);\n if (abortFn) {\n abortFn();\n abortFnsRef.current.delete(id);\n\n setItems((prev) =>\n prev.map((item) =>\n item.id === id ? { ...item, status: \"aborted\" as const } : item,\n ),\n );\n\n activeCountRef.current--;\n processQueue();\n }\n },\n [processQueue],\n );\n\n const abortAll = useCallback(() => {\n for (const abortFn of abortFnsRef.current.values()) {\n abortFn();\n }\n abortFnsRef.current.clear();\n queueRef.current = [];\n activeCountRef.current = 0;\n\n setItems((prev) =>\n prev.map((item) =>\n item.status === \"uploading\"\n ? { ...item, status: \"aborted\" as const }\n : item,\n ),\n );\n }, []);\n\n const clear = useCallback(() => {\n abortAll();\n setItems([]);\n }, [abortAll]);\n\n const retryUpload = useCallback(\n (id: string) => {\n setItems((prev) =>\n prev.map((item) =>\n item.id === id\n ? {\n ...item,\n status: \"pending\" as const,\n progress: 0,\n bytesUploaded: 0,\n error: null,\n }\n : item,\n ),\n );\n\n queueRef.current.push(id);\n processQueue();\n },\n [processQueue],\n );\n\n const state: MultiFlowUploadState<BrowserUploadInput> = {\n items,\n totalProgress: calculateTotalProgress(items),\n activeUploads: items.filter((item) => item.status === \"uploading\").length,\n completedUploads: items.filter((item) => item.status === \"success\").length,\n failedUploads: items.filter((item) => item.status === \"error\").length,\n };\n\n return {\n state,\n addFiles,\n removeFile,\n startUpload,\n abortUpload,\n abortAll,\n clear,\n retryUpload,\n isUploading: state.activeUploads > 0,\n };\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface DragDropOptions {\n /**\n * Accept specific file types (MIME types or file extensions)\n */\n accept?: string[];\n\n /**\n * Maximum number of files allowed\n */\n maxFiles?: number;\n\n /**\n * Maximum file size in bytes\n */\n maxFileSize?: number;\n\n /**\n * Whether to allow multiple files\n */\n multiple?: boolean;\n\n /**\n * Custom validation function for files\n */\n validator?: (files: File[]) => string[] | null;\n\n /**\n * Called when files are dropped or selected\n */\n onFilesReceived?: (files: File[]) => void;\n\n /**\n * Called when validation fails\n */\n onValidationError?: (errors: string[]) => void;\n\n /**\n * Called when drag state changes\n */\n onDragStateChange?: (isDragging: boolean) => void;\n}\n\nexport interface DragDropState {\n /**\n * Whether files are currently being dragged over the drop zone\n */\n isDragging: boolean;\n\n /**\n * Whether the drag is currently over the drop zone\n */\n isOver: boolean;\n\n /**\n * Whether the dragged items are valid files\n */\n isValid: boolean;\n\n /**\n * Current validation errors\n */\n errors: string[];\n}\n\nexport interface UseDragDropReturn {\n /**\n * Current drag and drop state\n */\n state: DragDropState;\n\n /**\n * Event handlers for the drop zone element\n */\n dragHandlers: {\n onDragEnter: (event: React.DragEvent) => void;\n onDragOver: (event: React.DragEvent) => void;\n onDragLeave: (event: React.DragEvent) => void;\n onDrop: (event: React.DragEvent) => void;\n };\n\n /**\n * Props for a file input element\n */\n inputProps: {\n type: \"file\";\n multiple: boolean;\n accept?: string;\n onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n style: { display: \"none\" };\n };\n\n /**\n * Open file picker dialog\n */\n openFilePicker: () => void;\n\n /**\n * Manually process files (useful for programmatic file handling)\n */\n processFiles: (files: File[]) => void;\n\n /**\n * Reset drag state\n */\n reset: () => void;\n}\n\nconst initialState: DragDropState = {\n isDragging: false,\n isOver: false,\n isValid: true,\n errors: [],\n};\n\n/**\n * React hook for handling drag and drop file uploads with validation.\n * Provides drag state management, file validation, and file picker integration.\n *\n * @param options - Configuration and event handlers\n * @returns Drag and drop state and handlers\n *\n * @example\n * ```tsx\n * const dragDrop = useDragDrop({\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * maxFileSize: 10 * 1024 * 1024, // 10MB\n * multiple: true,\n * onFilesReceived: (files) => {\n * console.log('Received files:', files);\n * // Process files with upload hooks\n * },\n * onValidationError: (errors) => {\n * console.error('Validation errors:', errors);\n * },\n * });\n *\n * return (\n * <div>\n * <div\n * {...dragDrop.dragHandlers}\n * style={{\n * border: dragDrop.state.isDragging ? '2px dashed #007bff' : '2px dashed #ccc',\n * backgroundColor: dragDrop.state.isOver ? '#f8f9fa' : 'transparent',\n * padding: '2rem',\n * textAlign: 'center',\n * cursor: 'pointer',\n * }}\n * onClick={dragDrop.openFilePicker}\n * >\n * {dragDrop.state.isDragging ? (\n * <p>Drop files here...</p>\n * ) : (\n * <p>Drag files here or click to select</p>\n * )}\n *\n * {dragDrop.state.errors.length > 0 && (\n * <div style={{ color: 'red', marginTop: '1rem' }}>\n * {dragDrop.state.errors.map((error, index) => (\n * <p key={index}>{error}</p>\n * ))}\n * </div>\n * )}\n * </div>\n *\n * <input {...dragDrop.inputProps} />\n * </div>\n * );\n * ```\n */\nexport function useDragDrop(options: DragDropOptions = {}): UseDragDropReturn {\n const {\n accept,\n maxFiles,\n maxFileSize,\n multiple = true,\n validator,\n onFilesReceived,\n onValidationError,\n onDragStateChange,\n } = options;\n\n const [state, setState] = useState<DragDropState>(initialState);\n const inputRef = useRef<HTMLInputElement>(null);\n const dragCounterRef = useRef(0);\n\n const updateState = useCallback((update: Partial<DragDropState>) => {\n setState((prev) => ({ ...prev, ...update }));\n }, []);\n\n const validateFiles = useCallback(\n (files: File[]): string[] => {\n const errors: string[] = [];\n\n // Check file count\n if (maxFiles && files.length > maxFiles) {\n errors.push(\n `Maximum ${maxFiles} files allowed. You selected ${files.length} files.`,\n );\n }\n\n // Check individual files\n for (const file of files) {\n // Check file size\n if (maxFileSize && file.size > maxFileSize) {\n const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(1);\n const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);\n errors.push(\n `File \"${file.name}\" (${fileSizeMB}MB) exceeds maximum size of ${maxSizeMB}MB.`,\n );\n }\n\n // Check file type\n if (accept && accept.length > 0) {\n const isAccepted = accept.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n } else {\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n } else {\n return file.type === acceptType;\n }\n }\n });\n\n if (!isAccepted) {\n errors.push(\n `File \"${file.name}\" type \"${file.type}\" is not accepted. Accepted types: ${accept.join(\", \")}.`,\n );\n }\n }\n }\n\n // Run custom validator\n if (validator) {\n const customErrors = validator(files);\n if (customErrors) {\n errors.push(...customErrors);\n }\n }\n\n return errors;\n },\n [accept, maxFiles, maxFileSize, validator],\n );\n\n const processFiles = useCallback(\n (files: File[]) => {\n const fileArray = Array.from(files);\n const errors = validateFiles(fileArray);\n\n if (errors.length > 0) {\n updateState({ errors, isValid: false });\n onValidationError?.(errors);\n } else {\n updateState({ errors: [], isValid: true });\n onFilesReceived?.(fileArray);\n }\n },\n [validateFiles, updateState, onFilesReceived, onValidationError],\n );\n\n const getFilesFromDataTransfer = useCallback(\n (dataTransfer: DataTransfer): File[] => {\n const files: File[] = [];\n\n if (dataTransfer.items) {\n // Use DataTransferItemList interface\n for (let i = 0; i < dataTransfer.items.length; i++) {\n const item = dataTransfer.items[i];\n if (item && item.kind === \"file\") {\n const file = item.getAsFile();\n if (file) {\n files.push(file);\n }\n }\n }\n } else {\n // Fallback to DataTransfer.files\n for (let i = 0; i < dataTransfer.files.length; i++) {\n const file = dataTransfer.files[i];\n if (file) {\n files.push(file);\n }\n }\n }\n\n return files;\n },\n [],\n );\n\n const onDragEnter = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current++;\n\n if (dragCounterRef.current === 1) {\n updateState({ isDragging: true, isOver: true });\n onDragStateChange?.(true);\n }\n },\n [updateState, onDragStateChange],\n );\n\n const onDragOver = useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n // Set dropEffect to indicate what operation is allowed\n if (event.dataTransfer) {\n event.dataTransfer.dropEffect = \"copy\";\n }\n }, []);\n\n const onDragLeave = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current--;\n\n if (dragCounterRef.current === 0) {\n updateState({ isDragging: false, isOver: false, errors: [] });\n onDragStateChange?.(false);\n }\n },\n [updateState, onDragStateChange],\n );\n\n const onDrop = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current = 0;\n updateState({ isDragging: false, isOver: false });\n onDragStateChange?.(false);\n\n if (event.dataTransfer) {\n const files = getFilesFromDataTransfer(event.dataTransfer);\n if (files.length > 0) {\n processFiles(files);\n }\n }\n },\n [updateState, onDragStateChange, getFilesFromDataTransfer, processFiles],\n );\n\n const openFilePicker = useCallback(() => {\n inputRef.current?.click();\n }, []);\n\n const onInputChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n if (event.target.files && event.target.files.length > 0) {\n const files = Array.from(event.target.files);\n processFiles(files);\n }\n\n // Reset input value to allow selecting the same files again\n event.target.value = \"\";\n },\n [processFiles],\n );\n\n const reset = useCallback(() => {\n setState(initialState);\n dragCounterRef.current = 0;\n }, []);\n\n const dragHandlers = {\n onDragEnter,\n onDragOver,\n onDragLeave,\n onDrop,\n };\n\n const inputProps = {\n type: \"file\" as const,\n multiple,\n accept: accept?.join(\", \"),\n onChange: onInputChange,\n style: { display: \"none\" as const },\n ref: inputRef,\n };\n\n return {\n state,\n dragHandlers,\n inputProps,\n openFilePicker,\n processFiles,\n reset,\n };\n}\n","import type {\n FlowUploadOptions,\n UploadistaEvent,\n} from \"@uploadista/client-browser\";\nimport {\n FlowManager,\n type FlowUploadState,\n type FlowUploadStatus,\n type InternalFlowUploadOptions,\n} from \"@uploadista/client-core\";\nimport { EventType, type FlowEvent } from \"@uploadista/core/flow\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { UploadEventType } from \"@uploadista/core/types\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n/**\n * Type guard to check if an event is a flow event\n */\nfunction isFlowEvent(event: UploadistaEvent): event is FlowEvent {\n // FlowEvent has eventType, not type\n const flowEvent = event as FlowEvent;\n return (\n flowEvent.eventType === EventType.FlowStart ||\n flowEvent.eventType === EventType.FlowEnd ||\n flowEvent.eventType === EventType.FlowError ||\n flowEvent.eventType === EventType.NodeStart ||\n flowEvent.eventType === EventType.NodeEnd ||\n flowEvent.eventType === EventType.NodePause ||\n flowEvent.eventType === EventType.NodeResume ||\n flowEvent.eventType === EventType.NodeError\n );\n}\n\n// Re-export types from core for convenience\nexport type { FlowUploadState, FlowUploadStatus };\n\n/**\n * Return value from the useFlowUpload hook with upload control methods and state.\n *\n * @template TOutput - Type of the final output from the flow (defaults to UploadFile)\n *\n * @property state - Complete flow upload state with progress and outputs\n * @property upload - Function to initiate file upload through the flow\n * @property abort - Cancel the current upload and flow execution\n * @property pause - Pause the current upload\n * @property reset - Reset state to idle (clears all data)\n * @property isUploading - True when upload or processing is active\n * @property isUploadingFile - True only during file upload phase\n * @property isProcessing - True only during flow processing phase\n */\nexport interface UseFlowUploadReturn<TOutput = UploadFile> {\n /**\n * Current upload state\n */\n state: FlowUploadState<TOutput>;\n\n /**\n * Upload a file through the flow\n */\n upload: (file: File | Blob) => Promise<void>;\n\n /**\n * Abort the current upload\n */\n abort: () => void;\n\n /**\n * Pause the current upload\n */\n pause: () => void;\n\n /**\n * Reset the upload state\n */\n reset: () => void;\n\n /**\n * Whether an upload or flow execution is in progress (uploading OR processing)\n */\n isUploading: boolean;\n\n /**\n * Whether the file is currently being uploaded (chunks being sent)\n */\n isUploadingFile: boolean;\n\n /**\n * Whether the flow is currently processing (after upload completes)\n */\n isProcessing: boolean;\n}\n\nconst initialState: FlowUploadState = {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: null,\n error: null,\n result: null,\n jobId: null,\n flowStarted: false,\n currentNodeName: null,\n currentNodeType: null,\n flowOutputs: null,\n};\n\n/**\n * React hook for uploading files through a flow with automatic flow execution.\n * Handles both the file upload phase and the flow processing phase, providing\n * real-time progress updates and flow node execution tracking.\n *\n * The flow engine processes the uploaded file through a DAG of nodes, which can\n * perform operations like image optimization, storage saving, webhooks, etc.\n *\n * Must be used within an UploadistaProvider. Flow events (node start/end, flow complete)\n * are automatically subscribed through the provider context.\n *\n * @template TOutput - Type of the final result from the flow (defaults to UploadFile)\n * @param options - Flow upload configuration including flow ID and event handlers\n * @returns Flow upload state and control methods\n *\n * @example\n * ```tsx\n * // Basic flow upload with progress tracking\n * function ImageUploader() {\n * const flowUpload = useFlowUpload({\n * flowConfig: {\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized-output\", // Optional: specify which output to use\n * },\n * onSuccess: (result) => {\n * console.log(\"Image optimized and saved:\", result);\n * },\n * onFlowComplete: (outputs) => {\n * console.log(\"All flow outputs:\", outputs);\n * // outputs might include: { thumbnail: {...}, optimized: {...}, original: {...} }\n * },\n * onError: (error) => {\n * console.error(\"Upload or processing failed:\", error);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * accept=\"image/*\"\n * onChange={(e) => {\n * const file = e.target.files?.[0];\n * if (file) flowUpload.upload(file);\n * }}\n * />\n *\n * {flowUpload.isUploadingFile && (\n * <div>Uploading... {flowUpload.state.progress}%</div>\n * )}\n *\n * {flowUpload.isProcessing && (\n * <div>\n * Processing...\n * {flowUpload.state.currentNodeName && (\n * <span>Current step: {flowUpload.state.currentNodeName}</span>\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"success\" && (\n * <div>\n * <p>Upload complete!</p>\n * {flowUpload.state.result && (\n * <img src={flowUpload.state.result.url} alt=\"Uploaded\" />\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"error\" && (\n * <div>\n * <p>Error: {flowUpload.state.error?.message}</p>\n * <button onClick={flowUpload.reset}>Try Again</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploading && (\n * <button onClick={flowUpload.abort}>Cancel</button>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link useMultiFlowUpload} for uploading multiple files through a flow\n * @see {@link useUpload} for simple uploads without flow processing\n */\nexport function useFlowUpload<TOutput = UploadFile>(\n options: FlowUploadOptions<TOutput>,\n): UseFlowUploadReturn<TOutput> {\n // Get client from context\n const client = useUploadistaContext();\n const [state, setState] = useState<FlowUploadState<TOutput>>(\n initialState as FlowUploadState<TOutput>,\n );\n const managerRef = useRef<FlowManager<File | Blob, TOutput> | null>(null);\n\n // Create FlowManager instance once (only recreate if client changes)\n // Note: We don't include options in deps to avoid recreating the manager on every render\n // The manager will use the latest options values through closures\n useEffect(() => {\n managerRef.current = new FlowManager(\n async (\n file: File | Blob,\n flowConfig: {\n flowId: string;\n storageId: string;\n outputNodeId?: string;\n metadata?: Record<string, string>;\n },\n internalOptions: InternalFlowUploadOptions,\n ) => {\n const result = await client.client.uploadWithFlow(file, flowConfig, {\n onJobStart: internalOptions.onJobStart,\n onProgress: internalOptions.onProgress,\n onChunkComplete: internalOptions.onChunkComplete,\n onSuccess: internalOptions.onSuccess,\n onError: internalOptions.onError,\n onShouldRetry: internalOptions.onShouldRetry,\n });\n // Return only abort and pause (ignore jobId and return value)\n return {\n abort: async () => {\n await result.abort();\n },\n pause: async () => {\n await result.pause();\n // Ignore the FlowJob return value\n },\n };\n },\n {\n onStateChange: setState,\n onProgress: options.onProgress,\n onChunkComplete: options.onChunkComplete,\n onFlowComplete: options.onFlowComplete,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onAbort: options.onAbort,\n },\n options,\n );\n\n return () => {\n managerRef.current?.cleanup();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client]);\n\n // Subscribe to events and forward them to the manager\n useEffect(() => {\n const unsubscribe = client.subscribeToEvents((event: UploadistaEvent) => {\n // Handle flow events\n if (isFlowEvent(event)) {\n managerRef.current?.handleFlowEvent(event);\n return;\n }\n\n // Handle upload progress events for this job's upload\n const uploadEvent = event as {\n type: string;\n data?: { id: string; progress: number; total: number };\n flow?: { jobId: string };\n };\n\n if (\n uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&\n uploadEvent.flow?.jobId === managerRef.current?.getJobId() &&\n uploadEvent.data\n ) {\n const { progress: bytesUploaded, total: totalBytes } = uploadEvent.data;\n\n managerRef.current?.handleUploadProgress(\n uploadEvent.data.id,\n bytesUploaded,\n totalBytes,\n );\n }\n });\n\n return unsubscribe;\n }, [client]);\n\n // Wrap manager methods with useCallback\n const upload = useCallback(async (file: File | Blob) => {\n await managerRef.current?.upload(file);\n }, []);\n\n const abort = useCallback(() => {\n managerRef.current?.abort();\n }, []);\n\n const pause = useCallback(() => {\n managerRef.current?.pause();\n }, []);\n\n const reset = useCallback(() => {\n managerRef.current?.reset();\n }, []);\n\n // Derive computed values from state (reactive to state changes)\n const isUploading =\n state.status === \"uploading\" || state.status === \"processing\";\n const isUploadingFile = state.status === \"uploading\";\n const isProcessing = state.status === \"processing\";\n\n return {\n state,\n upload,\n abort,\n pause,\n reset,\n isUploading,\n isUploadingFile,\n isProcessing,\n };\n}\n","import type { BrowserUploadInput } from \"@uploadista/client-browser\";\nimport type { UploadMetrics } from \"@uploadista/client-core\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\nimport type { UploadState, UploadStatus, UseUploadOptions } from \"./use-upload\";\n\nexport interface UploadItem {\n id: string;\n file: BrowserUploadInput;\n state: UploadState;\n}\n\nexport interface MultiUploadOptions\n extends Omit<UseUploadOptions, \"onSuccess\" | \"onError\" | \"onProgress\"> {\n /**\n * Maximum number of concurrent uploads\n */\n maxConcurrent?: number;\n\n /**\n * Called when an individual file upload starts\n */\n onUploadStart?: (item: UploadItem) => void;\n\n /**\n * Called when an individual file upload progresses\n */\n onUploadProgress?: (\n item: UploadItem,\n progress: number,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => void;\n\n /**\n * Called when an individual file upload succeeds\n */\n onUploadSuccess?: (item: UploadItem, result: UploadFile) => void;\n\n /**\n * Called when an individual file upload fails\n */\n onUploadError?: (item: UploadItem, error: Error) => void;\n\n /**\n * Called when all uploads complete (successfully or with errors)\n */\n onComplete?: (results: {\n successful: UploadItem[];\n failed: UploadItem[];\n total: number;\n }) => void;\n}\n\nexport interface MultiUploadState {\n /**\n * Total number of uploads\n */\n total: number;\n\n /**\n * Number of completed uploads (successful + failed)\n */\n completed: number;\n\n /**\n * Number of successful uploads\n */\n successful: number;\n\n /**\n * Number of failed uploads\n */\n failed: number;\n\n /**\n * Number of currently uploading files\n */\n uploading: number;\n\n /**\n * Overall progress as a percentage (0-100)\n */\n progress: number;\n\n /**\n * Total bytes uploaded across all files\n */\n totalBytesUploaded: number;\n\n /**\n * Total bytes to upload across all files\n */\n totalBytes: number;\n\n /**\n * Whether any uploads are currently active\n */\n isUploading: boolean;\n\n /**\n * Whether all uploads have completed\n */\n isComplete: boolean;\n}\n\nexport interface UseMultiUploadReturn {\n /**\n * Current multi-upload state\n */\n state: MultiUploadState;\n\n /**\n * Array of all upload items\n */\n items: UploadItem[];\n\n /**\n * Add files to the upload queue\n */\n addFiles: (files: BrowserUploadInput[]) => void;\n\n /**\n * Remove an item from the queue (only if not currently uploading)\n */\n removeItem: (id: string) => void;\n\n /**\n * Remove a file from the queue (alias for removeItem)\n */\n removeFile: (id: string) => void;\n\n /**\n * Start all pending uploads\n */\n startAll: () => void;\n\n /**\n * Abort a specific upload by ID\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all active uploads\n */\n abortAll: () => void;\n\n /**\n * Retry a specific failed upload by ID\n */\n retryUpload: (id: string) => void;\n\n /**\n * Retry all failed uploads\n */\n retryFailed: () => void;\n\n /**\n * Clear all completed uploads (successful and failed)\n */\n clearCompleted: () => void;\n\n /**\n * Clear all items\n */\n clearAll: () => void;\n\n /**\n * Get items by status\n */\n getItemsByStatus: (status: UploadStatus) => UploadItem[];\n\n /**\n * Aggregated upload metrics and performance insights from the client\n */\n metrics: UploadMetrics;\n}\n\n/**\n * React hook for managing multiple file uploads with queue management,\n * concurrent upload limits, and batch operations.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param options - Multi-upload configuration and event handlers\n * @returns Multi-upload state and control methods\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const multiUpload = useMultiUpload({\n * maxConcurrent: 3,\n * onUploadSuccess: (item, result) => {\n * console.log(`${item.file.name} uploaded successfully`);\n * },\n * onComplete: (results) => {\n * console.log(`Upload batch complete: ${results.successful.length}/${results.total} successful`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * onChange={(e) => {\n * if (e.target.files) {\n * multiUpload.addFiles(Array.from(e.target.files));\n * }\n * }}\n * />\n *\n * <div>Progress: {multiUpload.state.progress}%</div>\n * <div>\n * {multiUpload.state.uploading} uploading, {multiUpload.state.successful} successful,\n * {multiUpload.state.failed} failed\n * </div>\n *\n * <button onClick={multiUpload.startAll} disabled={multiUpload.state.isUploading}>\n * Start All\n * </button>\n * <button onClick={multiUpload.abortAll} disabled={!multiUpload.state.isUploading}>\n * Abort All\n * </button>\n * <button onClick={multiUpload.retryFailed} disabled={multiUpload.state.failed === 0}>\n * Retry Failed\n * </button>\n *\n * {multiUpload.items.map((item) => (\n * <div key={item.id}>\n * {item.file.name}: {item.state.status} ({item.state.progress}%)\n * </div>\n * ))}\n * </div>\n * );\n * }\n * ```\n */\n\nexport function useMultiUpload(\n options: MultiUploadOptions = {},\n): UseMultiUploadReturn {\n const uploadClient = useUploadistaContext();\n const { maxConcurrent = 3 } = options;\n const [items, setItems] = useState<UploadItem[]>([]);\n const itemsRef = useRef<UploadItem[]>([]);\n const nextIdRef = useRef(0);\n const activeUploadsRef = useRef(new Set<string>());\n\n // Store abort controllers for each upload\n const abortControllersRef = useRef<Map<string, { abort: () => void }>>(\n new Map(),\n );\n\n // Keep ref in sync with state (also updated synchronously in setItems callbacks)\n itemsRef.current = items;\n\n // Generate a unique ID for each upload item\n const generateId = useCallback(() => {\n return `upload-${Date.now()}-${nextIdRef.current++}`;\n }, []);\n\n // State update callback for individual uploads\n const onStateUpdate = useCallback(\n (id: string, state: Partial<UploadState>) => {\n setItems((prev) => {\n const updated = prev.map((item) =>\n item.id === id\n ? { ...item, state: { ...item.state, ...state } }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n },\n [],\n );\n\n // Check if all uploads are complete and trigger completion callback\n const checkForCompletion = useCallback(() => {\n const currentItems = itemsRef.current;\n const allComplete = currentItems.every((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n );\n\n if (allComplete && currentItems.length > 0) {\n const successful = currentItems.filter(\n (item) => item.state.status === \"success\",\n );\n const failed = currentItems.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n );\n\n options.onComplete?.({\n successful,\n failed,\n total: currentItems.length,\n });\n }\n }, [options]);\n\n // Start the next available upload if we have capacity\n const startNextUpload = useCallback(() => {\n if (activeUploadsRef.current.size >= maxConcurrent) {\n return;\n }\n\n const currentItems = itemsRef.current;\n const nextItem = currentItems.find(\n (item) =>\n item.state.status === \"idle\" && !activeUploadsRef.current.has(item.id),\n );\n\n if (!nextItem) {\n return;\n }\n\n // Perform upload inline to avoid circular dependency\n const performUploadInline = async () => {\n activeUploadsRef.current.add(nextItem.id);\n options.onUploadStart?.(nextItem);\n\n // Update state to uploading\n onStateUpdate(nextItem.id, { status: \"uploading\" });\n\n try {\n const controller = await uploadClient.client.upload(nextItem.file, {\n metadata: options.metadata,\n uploadLengthDeferred: options.uploadLengthDeferred,\n uploadSize: options.uploadSize,\n\n onProgress: (\n _uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => {\n const progress = totalBytes\n ? Math.round((bytesUploaded / totalBytes) * 100)\n : 0;\n\n onStateUpdate(nextItem.id, {\n progress,\n bytesUploaded,\n totalBytes,\n });\n\n options.onUploadProgress?.(\n nextItem,\n progress,\n bytesUploaded,\n totalBytes,\n );\n },\n\n onChunkComplete: () => {\n // Optional: could expose this as an option\n },\n\n onSuccess: (result: UploadFile) => {\n onStateUpdate(nextItem.id, {\n status: \"success\",\n result,\n progress: 100,\n });\n\n const updatedItem = {\n ...nextItem,\n state: { ...nextItem.state, status: \"success\" as const, result },\n };\n options.onUploadSuccess?.(updatedItem, result);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n },\n\n onError: (error: Error) => {\n onStateUpdate(nextItem.id, {\n status: \"error\",\n error,\n });\n\n const updatedItem = {\n ...nextItem,\n state: { ...nextItem.state, status: \"error\" as const, error },\n };\n options.onUploadError?.(updatedItem, error);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n },\n\n onShouldRetry: options.onShouldRetry,\n });\n\n // Store abort controller\n abortControllersRef.current.set(nextItem.id, controller);\n } catch (error) {\n onStateUpdate(nextItem.id, {\n status: \"error\",\n error: error as Error,\n });\n\n const updatedItem = {\n ...nextItem,\n state: {\n ...nextItem.state,\n status: \"error\" as const,\n error: error as Error,\n },\n };\n options.onUploadError?.(updatedItem, error as Error);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n }\n };\n\n performUploadInline();\n }, [maxConcurrent, uploadClient, options, onStateUpdate, checkForCompletion]);\n\n // Calculate overall state\n const state: MultiUploadState = {\n total: items.length,\n completed: items.filter((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n ).length,\n successful: items.filter((item) => item.state.status === \"success\").length,\n failed: items.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n ).length,\n uploading: items.filter((item) => item.state.status === \"uploading\").length,\n progress:\n items.length > 0\n ? Math.round(\n items.reduce((sum, item) => sum + item.state.progress, 0) /\n items.length,\n )\n : 0,\n totalBytesUploaded: items.reduce(\n (sum, item) => sum + item.state.bytesUploaded,\n 0,\n ),\n totalBytes: items.reduce(\n (sum, item) => sum + (item.state.totalBytes || 0),\n 0,\n ),\n isUploading: items.some((item) => item.state.status === \"uploading\"),\n isComplete:\n items.length > 0 &&\n items.every((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n ),\n };\n\n const addFiles = useCallback(\n (files: BrowserUploadInput[]) => {\n const newItems: UploadItem[] = files.map((file) => {\n const id = generateId();\n return {\n id,\n file,\n state: {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: file instanceof File ? file.size : null,\n error: null,\n result: null,\n },\n };\n });\n\n console.log(\"addFiles: Adding\", newItems.length, \"files\");\n\n // Update ref synchronously BEFORE setItems\n const updated = [...itemsRef.current, ...newItems];\n itemsRef.current = updated;\n console.log(\n \"addFiles: Updated itemsRef.current to\",\n updated.length,\n \"items\",\n );\n\n setItems(updated);\n },\n [generateId],\n );\n\n const removeItem = useCallback((id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && item.state.status === \"uploading\") {\n // Abort before removing\n const controller = abortControllersRef.current.get(id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(id);\n }\n }\n\n setItems((prev) => {\n const updated = prev.filter((item) => item.id !== id);\n itemsRef.current = updated;\n return updated;\n });\n activeUploadsRef.current.delete(id);\n }, []);\n\n const abortUpload = useCallback(\n (id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && item.state.status === \"uploading\") {\n const controller = abortControllersRef.current.get(id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(id);\n }\n\n activeUploadsRef.current.delete(id);\n\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === id\n ? { ...i, state: { ...i.state, status: \"aborted\" as const } }\n : i,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Try to start next upload in queue\n startNextUpload();\n }\n },\n [startNextUpload],\n );\n\n const retryUpload = useCallback(\n (id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && [\"error\", \"aborted\"].includes(item.state.status)) {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === id\n ? {\n ...i,\n state: { ...i.state, status: \"idle\" as const, error: null },\n }\n : i,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Auto-start the upload\n setTimeout(() => startNextUpload(), 0);\n }\n },\n [startNextUpload],\n );\n\n const startAll = useCallback(() => {\n const currentItems = itemsRef.current;\n console.log(\"Starting all uploads\", currentItems);\n // Start as many uploads as we can up to the concurrent limit\n const idleItems = currentItems.filter(\n (item) => item.state.status === \"idle\",\n );\n const slotsAvailable = maxConcurrent - activeUploadsRef.current.size;\n const itemsToStart = idleItems.slice(0, slotsAvailable);\n\n for (const item of itemsToStart) {\n console.log(\"Starting next upload\", item);\n startNextUpload();\n }\n }, [maxConcurrent, startNextUpload]);\n\n const abortAll = useCallback(() => {\n const currentItems = itemsRef.current;\n currentItems\n .filter((item) => item.state.status === \"uploading\")\n .forEach((item) => {\n const controller = abortControllersRef.current.get(item.id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(item.id);\n }\n });\n\n activeUploadsRef.current.clear();\n\n // Update all uploading items to aborted status\n setItems((prev) => {\n const updated = prev.map((item) =>\n item.state.status === \"uploading\"\n ? { ...item, state: { ...item.state, status: \"aborted\" as const } }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n }, []);\n\n const retryFailed = useCallback(() => {\n const currentItems = itemsRef.current;\n const failedItems = currentItems.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n );\n\n if (failedItems.length > 0) {\n setItems((prev) => {\n const updated = prev.map((item) =>\n failedItems.some((f) => f.id === item.id)\n ? {\n ...item,\n state: { ...item.state, status: \"idle\" as const, error: null },\n }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Auto-start uploads if we have capacity\n setTimeout(startAll, 0);\n }\n }, [startAll]);\n\n const clearCompleted = useCallback(() => {\n setItems((prev) => {\n const updated = prev.filter(\n (item) => ![\"success\", \"error\", \"aborted\"].includes(item.state.status),\n );\n itemsRef.current = updated;\n return updated;\n });\n }, []);\n\n const clearAll = useCallback(() => {\n abortAll();\n setItems([]);\n itemsRef.current = [];\n activeUploadsRef.current.clear();\n }, [abortAll]);\n\n const getItemsByStatus = useCallback((status: UploadStatus) => {\n return itemsRef.current.filter((item) => item.state.status === status);\n }, []);\n\n // Create aggregated metrics object that delegates to the upload client\n const metrics: UploadMetrics = {\n getInsights: () => uploadClient.client.getChunkingInsights(),\n exportMetrics: () => uploadClient.client.exportMetrics(),\n getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),\n getNetworkCondition: () => uploadClient.client.getNetworkCondition(),\n resetMetrics: () => uploadClient.client.resetMetrics(),\n };\n\n return {\n state,\n items,\n addFiles,\n removeItem,\n removeFile: removeItem, // Alias for consistency with MultiUploadExample\n startAll,\n abortUpload,\n abortAll,\n retryUpload,\n retryFailed,\n clearCompleted,\n clearAll,\n getItemsByStatus,\n metrics,\n };\n}\n","import type { BrowserUploadInput } from \"@uploadista/client-browser\";\nimport type { UploadMetrics, UploadOptions } from \"@uploadista/client-core\";\nimport {\n UploadManager,\n type UploadState,\n type UploadStatus,\n} from \"@uploadista/client-core\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n// Re-export types from core for convenience\nexport type { UploadState, UploadStatus };\n\nexport interface UseUploadOptions {\n /**\n * Upload metadata to attach to the file\n */\n metadata?: Record<string, string>;\n\n /**\n * Whether to defer the upload size calculation\n */\n uploadLengthDeferred?: boolean;\n\n /**\n * Manual upload size override\n */\n uploadSize?: number;\n\n /**\n * Called when upload progress updates\n *\n * @param uploadId - The unique identifier for this upload\n * @param bytesUploaded - Number of bytes uploaded\n * @param totalBytes - Total bytes to upload, null if unknown\n */\n onProgress?: (\n uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => void;\n\n /**\n * Called when a chunk completes\n *\n * @param chunkSize - Size of the completed chunk\n * @param bytesAccepted - Total bytes accepted so far\n * @param bytesTotal - Total bytes to upload, null if unknown\n */\n onChunkComplete?: (\n chunkSize: number,\n bytesAccepted: number,\n bytesTotal: number | null,\n ) => void;\n\n /**\n * Called when upload succeeds\n *\n * @param result - The uploaded file result\n */\n onSuccess?: (result: UploadFile) => void;\n\n /**\n * Called when upload fails\n *\n * @param error - The error that caused the failure\n */\n onError?: (error: Error) => void;\n\n /**\n * Called when upload is aborted\n */\n onAbort?: () => void;\n\n /**\n * Custom retry logic\n *\n * @param error - The error that triggered the retry check\n * @param retryAttempt - The current retry attempt number\n * @returns true to retry, false to fail\n */\n onShouldRetry?: (error: Error, retryAttempt: number) => boolean;\n}\n\nexport interface UseUploadReturn {\n /**\n * Current upload state\n */\n state: UploadState;\n\n /**\n * Start uploading a file\n */\n upload: (file: BrowserUploadInput) => void;\n\n /**\n * Abort the current upload\n */\n abort: () => void;\n\n /**\n * Reset the upload state to idle\n */\n reset: () => void;\n\n /**\n * Retry the last failed upload\n */\n retry: () => void;\n\n /**\n * Whether an upload is currently active\n */\n isUploading: boolean;\n\n /**\n * Whether the upload can be retried\n */\n canRetry: boolean;\n\n /**\n * Upload metrics and performance insights from the client\n */\n metrics: UploadMetrics;\n}\n\n/**\n * React hook for managing individual file uploads with full state management.\n * Provides upload progress tracking, error handling, abort functionality, and retry logic.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param options - Upload configuration and event handlers\n * @returns Upload state and control methods\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const upload = useUpload({\n * onSuccess: (result) => console.log('Upload complete:', result),\n * onError: (error) => console.error('Upload failed:', error),\n * onProgress: (uploadId, bytesUploaded, totalBytes) => {\n * const progress = totalBytes ? Math.round((bytesUploaded / totalBytes) * 100) : 0;\n * console.log(`Upload ${uploadId}: ${progress}% (${bytesUploaded}/${totalBytes} bytes)`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * onChange={(e) => {\n * const file = e.target.files?.[0];\n * if (file) upload.upload(file);\n * }}\n * />\n * {upload.isUploading && <div>Progress: {upload.state.progress}%</div>}\n * {upload.state.error && <div>Error: {upload.state.error.message}</div>}\n * {upload.canRetry && <button onClick={upload.retry}>Retry</button>}\n * <button onClick={upload.abort} disabled={!upload.isUploading}>Abort</button>\n * </div>\n * );\n * }\n * ```\n */\nconst initialState: UploadState = {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: null,\n error: null,\n result: null,\n};\n\nexport function useUpload(options: UseUploadOptions = {}): UseUploadReturn {\n const uploadClient = useUploadistaContext();\n const [state, setState] = useState<UploadState>(initialState);\n const managerRef = useRef<UploadManager<\n BrowserUploadInput,\n UploadOptions\n > | null>(null);\n\n // Create UploadManager instance\n useEffect(() => {\n managerRef.current = new UploadManager(\n (file: BrowserUploadInput, opts: UploadOptions) =>\n uploadClient.client.upload(file, opts),\n {\n onStateChange: setState,\n onProgress: options.onProgress,\n onChunkComplete: options.onChunkComplete,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onAbort: options.onAbort,\n },\n {\n metadata: options.metadata,\n uploadLengthDeferred: options.uploadLengthDeferred,\n uploadSize: options.uploadSize,\n onShouldRetry: options.onShouldRetry,\n },\n );\n\n return () => {\n managerRef.current?.cleanup();\n };\n }, [uploadClient, options]);\n\n // Wrap manager methods with useCallback\n const upload = useCallback((file: BrowserUploadInput) => {\n managerRef.current?.upload(file);\n }, []);\n\n const abort = useCallback(() => {\n managerRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n managerRef.current?.reset();\n }, []);\n\n const retry = useCallback(() => {\n managerRef.current?.retry();\n }, []);\n\n // Derive computed values from state\n const isUploading = state.status === \"uploading\";\n const canRetry = managerRef.current?.canRetry() ?? false;\n\n // Create metrics object that delegates to the upload client\n const metrics: UploadMetrics = {\n getInsights: () => uploadClient.client.getChunkingInsights(),\n exportMetrics: () => uploadClient.client.exportMetrics(),\n getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),\n getNetworkCondition: () => uploadClient.client.getNetworkCondition(),\n resetMetrics: () => uploadClient.client.resetMetrics(),\n };\n\n return {\n state,\n upload,\n abort,\n reset,\n retry,\n isUploading,\n canRetry,\n metrics,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-upload-CbeBq3kV.mjs","names":["items","newItems: FlowUploadItem<BrowserUploadInput>[]","state: MultiFlowUploadState<BrowserUploadInput>","initialState: DragDropState","initialState","errors: string[]","files: File[]","initialState: FlowUploadState","initialState","state","state: MultiUploadState","newItems: UploadItem[]","item","initialState: UploadState"],"sources":["../src/hooks/use-uploadista-client.ts","../src/components/uploadista-provider.tsx","../src/hooks/use-multi-flow-upload.ts","../src/hooks/use-drag-drop.ts","../src/hooks/use-flow-upload.ts","../src/hooks/use-multi-upload.ts","../src/hooks/use-upload.ts"],"sourcesContent":["import {\n createUploadistaClient,\n type UploadistaClientOptions,\n} from \"@uploadista/client-browser\";\nimport { useMemo, useRef } from \"react\";\n\n/**\n * Configuration options for the uploadista client hook.\n * Extends the base client options with React-specific behavior.\n *\n * @property onEvent - Global event handler for all upload and flow events\n * @property baseUrl - API base URL for uploads\n * @property storageId - Default storage identifier\n * @property chunkSize - Size of upload chunks in bytes\n * @property storeFingerprintForResuming - Enable resumable uploads\n * @property retryDelays - Array of retry delays in milliseconds\n * @property parallelUploads - Maximum number of parallel uploads\n * @property uploadStrategy - Upload strategy (sequential, parallel, adaptive)\n * @property smartChunking - Enable dynamic chunk size adjustment\n * @property networkMonitoring - Enable network condition monitoring\n */\nexport interface UseUploadistaClientOptions extends UploadistaClientOptions {\n /**\n * Global event handler for all upload and flow events from this client\n */\n onEvent?: UploadistaClientOptions[\"onEvent\"];\n}\n\n/**\n * Return value from the useUploadistaClient hook.\n *\n * @property client - Configured uploadista client instance (stable across re-renders)\n * @property config - Current client configuration options\n */\nexport interface UseUploadistaClientReturn {\n /**\n * The uploadista client instance\n */\n client: ReturnType<typeof createUploadistaClient>;\n\n /**\n * Current configuration of the client\n */\n config: UseUploadistaClientOptions;\n}\n\n/**\n * React hook for creating and managing an uploadista client instance.\n * The client instance is memoized and stable across re-renders, only being\n * recreated when configuration options change.\n *\n * This hook is typically used internally by UploadistaProvider, but can be\n * used directly for advanced use cases requiring multiple client instances.\n *\n * @param options - Upload client configuration options\n * @returns Object containing the stable client instance and current configuration\n *\n * @example\n * ```tsx\n * // Basic client setup\n * function MyUploadComponent() {\n * const { client, config } = useUploadistaClient({\n * baseUrl: 'https://api.example.com',\n * storageId: 'default-storage',\n * chunkSize: 1024 * 1024, // 1MB chunks\n * storeFingerprintForResuming: true,\n * onEvent: (event) => {\n * console.log('Upload event:', event);\n * }\n * });\n *\n * // Use client directly\n * const handleUpload = async (file: File) => {\n * await client.upload(file, {\n * onSuccess: (result) => console.log('Uploaded:', result),\n * onError: (error) => console.error('Failed:', error),\n * });\n * };\n *\n * return <FileUploader onUpload={handleUpload} />;\n * }\n *\n * // Advanced: Multiple clients with different configurations\n * function MultiClientComponent() {\n * // Client for image uploads\n * const imageClient = useUploadistaClient({\n * baseUrl: 'https://images.example.com',\n * storageId: 'images',\n * chunkSize: 2 * 1024 * 1024, // 2MB for images\n * });\n *\n * // Client for document uploads\n * const docClient = useUploadistaClient({\n * baseUrl: 'https://docs.example.com',\n * storageId: 'documents',\n * chunkSize: 512 * 1024, // 512KB for documents\n * });\n *\n * return (\n * <div>\n * <ImageUploader client={imageClient.client} />\n * <DocumentUploader client={docClient.client} />\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link UploadistaProvider} for the recommended way to provide client context\n */\nexport function useUploadistaClient(\n options: UseUploadistaClientOptions,\n): UseUploadistaClientReturn {\n // Store the options in a ref to enable stable dependency checking\n const optionsRef = useRef<UseUploadistaClientOptions>(options);\n\n // Update ref on each render but only create new client when essential deps change\n optionsRef.current = options;\n\n // Create client instance with stable identity\n const client = useMemo(() => {\n return createUploadistaClient({\n baseUrl: options.baseUrl,\n storageId: options.storageId,\n uploadistaBasePath: options.uploadistaBasePath,\n chunkSize: options.chunkSize,\n storeFingerprintForResuming: options.storeFingerprintForResuming,\n retryDelays: options.retryDelays,\n parallelUploads: options.parallelUploads,\n parallelChunkSize: options.parallelChunkSize,\n uploadStrategy: options.uploadStrategy,\n smartChunking: options.smartChunking,\n networkMonitoring: options.networkMonitoring,\n uploadMetrics: options.uploadMetrics,\n connectionPooling: options.connectionPooling,\n // logger: options.logger,\n auth: options.auth,\n onEvent: options.onEvent,\n });\n }, [options]);\n\n return {\n client,\n config: options,\n };\n}\n","\"use client\";\nimport type { UploadistaEvent } from \"@uploadista/client-browser\";\nimport type React from \"react\";\nimport { createContext, useCallback, useContext, useMemo, useRef } from \"react\";\nimport {\n type UseUploadistaClientOptions,\n type UseUploadistaClientReturn,\n useUploadistaClient,\n} from \"../hooks/use-uploadista-client\";\n\n/**\n * Props for the UploadistaProvider component.\n * Combines client configuration options with React children.\n *\n * @property children - React components that will have access to the upload client context\n * @property baseUrl - API base URL for uploads\n * @property storageId - Default storage identifier\n * @property chunkSize - Upload chunk size in bytes\n * @property onEvent - Global event handler for all upload events\n * @property ... - All other UploadistaClientOptions\n */\nexport interface UploadistaProviderProps extends UseUploadistaClientOptions {\n /**\n * Children components that will have access to the upload client\n */\n children: React.ReactNode;\n}\n\ntype UploadistaContextValue = UseUploadistaClientReturn & {\n /**\n * Subscribe to events (used internally by hooks)\n * @internal\n */\n subscribeToEvents: (handler: (event: UploadistaEvent) => void) => () => void;\n};\n\nconst UploadistaContext = createContext<UploadistaContextValue | null>(null);\n\n/**\n * Context provider that provides uploadista client functionality to child components.\n * This eliminates the need to pass upload client configuration down through props\n * and ensures a single, shared upload client instance across your application.\n *\n * @param props - Upload client options and children\n * @returns Provider component with upload client context\n *\n * @example\n * ```tsx\n * // Wrap your app with the upload provider\n * function App() {\n * return (\n * <UploadistaProvider\n * baseUrl=\"https://api.example.com\"\n * storageId=\"my-storage\"\n * chunkSize={1024 * 1024} // 1MB chunks\n * onEvent={(event) => {\n * console.log('Global upload event:', event);\n * }}\n * >\n * <UploadInterface />\n * </UploadistaProvider>\n * );\n * }\n *\n * // Use the upload client in any child component\n * function UploadInterface() {\n * const uploadClient = useUploadistaContext();\n * const upload = useUpload(uploadClient);\n * const dragDrop = useDragDrop({\n * onFilesReceived: (files) => {\n * files.forEach(file => upload.upload(file));\n * }\n * });\n *\n * return (\n * <div {...dragDrop.dragHandlers}>\n * <p>Drop files here to upload</p>\n * {upload.isUploading && <p>Progress: {upload.state.progress}%</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function UploadistaProvider({\n children,\n ...options\n}: UploadistaProviderProps) {\n const eventSubscribersRef = useRef<Set<(event: UploadistaEvent) => void>>(\n new Set(),\n );\n\n // Wrap the original onEvent to broadcast to subscribers\n const wrappedOnEvent = useCallback(\n (event: UploadistaEvent) => {\n console.log(\"[UploadistaProvider] Received event:\", event);\n\n // Call original handler if provided\n options.onEvent?.(event);\n\n // Broadcast to all subscribers\n console.log(\n \"[UploadistaProvider] Broadcasting to\",\n eventSubscribersRef.current.size,\n \"subscribers\",\n );\n eventSubscribersRef.current.forEach((handler) => {\n try {\n handler(event);\n } catch (err) {\n console.error(\"Error in event subscriber:\", err);\n }\n });\n },\n [options.onEvent],\n );\n\n const uploadClient = useUploadistaClient({\n ...options,\n onEvent: wrappedOnEvent,\n });\n\n const subscribeToEvents = useCallback(\n (handler: (event: UploadistaEvent) => void) => {\n eventSubscribersRef.current.add(handler);\n return () => {\n eventSubscribersRef.current.delete(handler);\n };\n },\n [],\n );\n\n // Memoize the context value to prevent unnecessary re-renders\n const contextValue = useMemo(\n () => ({\n ...uploadClient,\n subscribeToEvents,\n }),\n [uploadClient, subscribeToEvents],\n );\n\n return (\n <UploadistaContext.Provider value={contextValue}>\n {children}\n </UploadistaContext.Provider>\n );\n}\n\n/**\n * Hook to access the uploadista client from the UploadistaProvider context.\n * Must be used within an UploadistaProvider component.\n *\n * @returns Upload client instance from context\n * @throws Error if used outside of UploadistaProvider\n *\n * @example\n * ```tsx\n * function FileUploader() {\n * const uploadClient = useUploadistaContext();\n * const upload = useUpload(uploadClient);\n *\n * return (\n * <button\n * onClick={() => {\n * const input = document.createElement('input');\n * input.type = 'file';\n * input.onchange = (e) => {\n * const file = (e.target as HTMLInputElement).files?.[0];\n * if (file) upload.upload(file);\n * };\n * input.click();\n * }}\n * >\n * Upload File\n * </button>\n * );\n * }\n * ```\n */\nexport function useUploadistaContext(): UploadistaContextValue {\n const context = useContext(UploadistaContext);\n\n if (context === null) {\n throw new Error(\n \"useUploadistaContext must be used within an UploadistaProvider. \" +\n \"Make sure to wrap your component tree with <UploadistaProvider>.\",\n );\n }\n\n return context;\n}\n","import type {\n BrowserUploadInput,\n FlowUploadItem,\n MultiFlowUploadOptions,\n MultiFlowUploadState,\n} from \"@uploadista/client-browser\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n/**\n * Return value from the useMultiFlowUpload hook with batch upload control methods.\n *\n * @property state - Aggregated state across all flow upload items\n * @property addFiles - Add new files to the upload queue\n * @property removeFile - Remove a file from the queue (aborts if uploading)\n * @property startUpload - Begin uploading all pending files\n * @property abortUpload - Cancel a specific upload by its ID\n * @property abortAll - Cancel all active uploads\n * @property clear - Remove all items and abort active uploads\n * @property retryUpload - Retry a specific failed upload\n * @property isUploading - True when any uploads are in progress\n */\nexport interface UseMultiFlowUploadReturn {\n /**\n * Current upload state\n */\n state: MultiFlowUploadState<BrowserUploadInput>;\n\n /**\n * Add files to upload queue\n */\n addFiles: (files: File[] | FileList) => void;\n\n /**\n * Remove a file from the queue\n */\n removeFile: (id: string) => void;\n\n /**\n * Start uploading all pending files\n */\n startUpload: () => void;\n\n /**\n * Abort a specific upload by ID\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all active uploads\n */\n abortAll: () => void;\n\n /**\n * Clear all items (aborts any active uploads first)\n */\n clear: () => void;\n\n /**\n * Retry a specific failed upload by ID\n */\n retryUpload: (id: string) => void;\n\n /**\n * Whether uploads are in progress\n */\n isUploading: boolean;\n}\n\n/**\n * React hook for uploading multiple files through a flow with concurrent upload management.\n * Processes each file through the specified flow while respecting concurrency limits.\n *\n * Each file is uploaded and processed independently through the flow, with automatic\n * queue management. Failed uploads can be retried individually, and uploads can be\n * aborted at any time.\n *\n * Must be used within an UploadistaProvider. Flow events for each upload are automatically\n * tracked and synchronized.\n *\n * @param options - Multi-flow upload configuration including flow config and concurrency settings\n * @returns Multi-flow upload state and control methods\n *\n * @example\n * ```tsx\n * // Batch image upload with progress tracking\n * function BatchImageUploader() {\n * const multiFlowUpload = useMultiFlowUpload({\n * flowConfig: {\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * },\n * maxConcurrent: 3, // Process 3 files at a time\n * onItemSuccess: (item) => {\n * console.log(`${item.file.name} uploaded successfully`);\n * },\n * onItemError: (item, error) => {\n * console.error(`${item.file.name} failed:`, error);\n * },\n * onComplete: (items) => {\n * const successful = items.filter(i => i.status === 'success');\n * const failed = items.filter(i => i.status === 'error');\n * console.log(`Batch complete: ${successful.length} successful, ${failed.length} failed`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * accept=\"image/*\"\n * onChange={(e) => {\n * if (e.target.files) {\n * multiFlowUpload.addFiles(e.target.files);\n * multiFlowUpload.startUpload();\n * }\n * }}\n * />\n *\n * <div>\n * <p>Overall Progress: {multiFlowUpload.state.totalProgress}%</p>\n * <p>\n * {multiFlowUpload.state.activeUploads} uploading,\n * {multiFlowUpload.state.completedUploads} completed,\n * {multiFlowUpload.state.failedUploads} failed\n * </p>\n * </div>\n *\n * <div>\n * <button onClick={multiFlowUpload.startUpload} disabled={multiFlowUpload.isUploading}>\n * Start All\n * </button>\n * <button onClick={multiFlowUpload.abortAll} disabled={!multiFlowUpload.isUploading}>\n * Cancel All\n * </button>\n * <button onClick={multiFlowUpload.clear}>\n * Clear List\n * </button>\n * </div>\n *\n * {multiFlowUpload.state.items.map((item) => (\n * <div key={item.id} style={{\n * border: '1px solid #ccc',\n * padding: '1rem',\n * marginBottom: '0.5rem'\n * }}>\n * <div>{item.file instanceof File ? item.file.name : 'File'}</div>\n * <div>Status: {item.status}</div>\n *\n * {item.status === \"uploading\" && (\n * <div>\n * <progress value={item.progress} max={100} />\n * <span>{item.progress}%</span>\n * <button onClick={() => multiFlowUpload.abortUpload(item.id)}>\n * Cancel\n * </button>\n * </div>\n * )}\n *\n * {item.status === \"error\" && (\n * <div>\n * <p style={{ color: 'red' }}>{item.error?.message}</p>\n * <button onClick={() => multiFlowUpload.retryUpload(item.id)}>\n * Retry\n * </button>\n * <button onClick={() => multiFlowUpload.removeFile(item.id)}>\n * Remove\n * </button>\n * </div>\n * )}\n *\n * {item.status === \"success\" && (\n * <div>\n * <p style={{ color: 'green' }}>✓ Upload complete</p>\n * <button onClick={() => multiFlowUpload.removeFile(item.id)}>\n * Remove\n * </button>\n * </div>\n * )}\n * </div>\n * ))}\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link useFlowUpload} for single file flow uploads\n * @see {@link useMultiUpload} for multi-file uploads without flow processing\n */\nexport function useMultiFlowUpload(\n options: MultiFlowUploadOptions<BrowserUploadInput>,\n): UseMultiFlowUploadReturn {\n const client = useUploadistaContext();\n const [items, setItems] = useState<FlowUploadItem<BrowserUploadInput>[]>([]);\n const abortFnsRef = useRef<Map<string, () => void>>(new Map());\n const queueRef = useRef<string[]>([]);\n const activeCountRef = useRef(0);\n\n const maxConcurrent = options.maxConcurrent ?? 3;\n\n const calculateTotalProgress = useCallback(\n (items: FlowUploadItem<BrowserUploadInput>[]) => {\n if (items.length === 0) return 0;\n const totalProgress = items.reduce((sum, item) => sum + item.progress, 0);\n return Math.round(totalProgress / items.length);\n },\n [],\n );\n\n const processQueue = useCallback(async () => {\n if (\n activeCountRef.current >= maxConcurrent ||\n queueRef.current.length === 0\n ) {\n return;\n }\n\n const itemId = queueRef.current.shift();\n if (!itemId) return;\n\n const item = items.find((i) => i.id === itemId);\n if (!item || item.status !== \"pending\") {\n processQueue();\n return;\n }\n\n activeCountRef.current++;\n\n setItems((prev) =>\n prev.map((i) =>\n i.id === itemId ? { ...i, status: \"uploading\" as const } : i,\n ),\n );\n\n try {\n const { abort, jobId } = await client.client.uploadWithFlow(\n item.file,\n options.flowConfig,\n {\n onJobStart: (jobId: string) => {\n setItems((prev) =>\n prev.map((i) => (i.id === itemId ? { ...i, jobId } : i)),\n );\n },\n onProgress: (\n _uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => {\n const progress = totalBytes\n ? Math.round((bytesUploaded / totalBytes) * 100)\n : 0;\n\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId\n ? {\n ...i,\n progress,\n bytesUploaded,\n totalBytes: totalBytes || 0,\n }\n : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemProgress?.(updatedItem);\n }\n return updated;\n });\n },\n onSuccess: (result: UploadFile) => {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId\n ? { ...i, status: \"success\" as const, result, progress: 100 }\n : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemSuccess?.(updatedItem);\n }\n\n // Check if all uploads are complete\n const allComplete = updated.every(\n (i) =>\n i.status === \"success\" ||\n i.status === \"error\" ||\n i.status === \"aborted\",\n );\n if (allComplete) {\n options.onComplete?.(updated);\n }\n\n return updated;\n });\n\n abortFnsRef.current.delete(itemId);\n activeCountRef.current--;\n processQueue();\n },\n onError: (error: Error) => {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === itemId ? { ...i, status: \"error\" as const, error } : i,\n );\n const updatedItem = updated.find((i) => i.id === itemId);\n if (updatedItem) {\n options.onItemError?.(updatedItem, error);\n }\n\n // Check if all uploads are complete\n const allComplete = updated.every(\n (i) =>\n i.status === \"success\" ||\n i.status === \"error\" ||\n i.status === \"aborted\",\n );\n if (allComplete) {\n options.onComplete?.(updated);\n }\n\n return updated;\n });\n\n abortFnsRef.current.delete(itemId);\n activeCountRef.current--;\n processQueue();\n },\n onShouldRetry: options.onShouldRetry,\n },\n );\n\n abortFnsRef.current.set(itemId, abort);\n\n setItems((prev) =>\n prev.map((i) => (i.id === itemId ? { ...i, jobId } : i)),\n );\n } catch (error) {\n setItems((prev) =>\n prev.map((i) =>\n i.id === itemId\n ? { ...i, status: \"error\" as const, error: error as Error }\n : i,\n ),\n );\n\n activeCountRef.current--;\n processQueue();\n }\n }, [client, items, maxConcurrent, options]);\n\n const addFiles = useCallback((files: File[] | FileList) => {\n const fileArray = Array.from(files);\n const newItems: FlowUploadItem<BrowserUploadInput>[] = fileArray.map(\n (file) => ({\n id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n file,\n status: \"pending\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: file.size,\n error: null,\n result: null,\n jobId: null,\n }),\n );\n\n setItems((prev) => [...prev, ...newItems]);\n }, []);\n\n const removeFile = useCallback((id: string) => {\n const abortFn = abortFnsRef.current.get(id);\n if (abortFn) {\n abortFn();\n abortFnsRef.current.delete(id);\n }\n\n setItems((prev) => prev.filter((item) => item.id !== id));\n queueRef.current = queueRef.current.filter((queueId) => queueId !== id);\n }, []);\n\n const startUpload = useCallback(() => {\n const pendingItems = items.filter((item) => item.status === \"pending\");\n queueRef.current.push(...pendingItems.map((item) => item.id));\n\n for (let i = 0; i < maxConcurrent; i++) {\n processQueue();\n }\n }, [items, maxConcurrent, processQueue]);\n\n const abortUpload = useCallback(\n (id: string) => {\n const abortFn = abortFnsRef.current.get(id);\n if (abortFn) {\n abortFn();\n abortFnsRef.current.delete(id);\n\n setItems((prev) =>\n prev.map((item) =>\n item.id === id ? { ...item, status: \"aborted\" as const } : item,\n ),\n );\n\n activeCountRef.current--;\n processQueue();\n }\n },\n [processQueue],\n );\n\n const abortAll = useCallback(() => {\n for (const abortFn of abortFnsRef.current.values()) {\n abortFn();\n }\n abortFnsRef.current.clear();\n queueRef.current = [];\n activeCountRef.current = 0;\n\n setItems((prev) =>\n prev.map((item) =>\n item.status === \"uploading\"\n ? { ...item, status: \"aborted\" as const }\n : item,\n ),\n );\n }, []);\n\n const clear = useCallback(() => {\n abortAll();\n setItems([]);\n }, [abortAll]);\n\n const retryUpload = useCallback(\n (id: string) => {\n setItems((prev) =>\n prev.map((item) =>\n item.id === id\n ? {\n ...item,\n status: \"pending\" as const,\n progress: 0,\n bytesUploaded: 0,\n error: null,\n }\n : item,\n ),\n );\n\n queueRef.current.push(id);\n processQueue();\n },\n [processQueue],\n );\n\n const state: MultiFlowUploadState<BrowserUploadInput> = {\n items,\n totalProgress: calculateTotalProgress(items),\n activeUploads: items.filter((item) => item.status === \"uploading\").length,\n completedUploads: items.filter((item) => item.status === \"success\").length,\n failedUploads: items.filter((item) => item.status === \"error\").length,\n };\n\n return {\n state,\n addFiles,\n removeFile,\n startUpload,\n abortUpload,\n abortAll,\n clear,\n retryUpload,\n isUploading: state.activeUploads > 0,\n };\n}\n","import { useCallback, useRef, useState } from \"react\";\n\nexport interface DragDropOptions {\n /**\n * Accept specific file types (MIME types or file extensions)\n */\n accept?: string[];\n\n /**\n * Maximum number of files allowed\n */\n maxFiles?: number;\n\n /**\n * Maximum file size in bytes\n */\n maxFileSize?: number;\n\n /**\n * Whether to allow multiple files\n */\n multiple?: boolean;\n\n /**\n * Custom validation function for files\n */\n validator?: (files: File[]) => string[] | null;\n\n /**\n * Called when files are dropped or selected\n */\n onFilesReceived?: (files: File[]) => void;\n\n /**\n * Called when validation fails\n */\n onValidationError?: (errors: string[]) => void;\n\n /**\n * Called when drag state changes\n */\n onDragStateChange?: (isDragging: boolean) => void;\n}\n\nexport interface DragDropState {\n /**\n * Whether files are currently being dragged over the drop zone\n */\n isDragging: boolean;\n\n /**\n * Whether the drag is currently over the drop zone\n */\n isOver: boolean;\n\n /**\n * Whether the dragged items are valid files\n */\n isValid: boolean;\n\n /**\n * Current validation errors\n */\n errors: string[];\n}\n\nexport interface UseDragDropReturn {\n /**\n * Current drag and drop state\n */\n state: DragDropState;\n\n /**\n * Event handlers for the drop zone element\n */\n dragHandlers: {\n onDragEnter: (event: React.DragEvent) => void;\n onDragOver: (event: React.DragEvent) => void;\n onDragLeave: (event: React.DragEvent) => void;\n onDrop: (event: React.DragEvent) => void;\n };\n\n /**\n * Props for a file input element\n */\n inputProps: {\n type: \"file\";\n multiple: boolean;\n accept?: string;\n onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n style: { display: \"none\" };\n };\n\n /**\n * Open file picker dialog\n */\n openFilePicker: () => void;\n\n /**\n * Manually process files (useful for programmatic file handling)\n */\n processFiles: (files: File[]) => void;\n\n /**\n * Reset drag state\n */\n reset: () => void;\n}\n\nconst initialState: DragDropState = {\n isDragging: false,\n isOver: false,\n isValid: true,\n errors: [],\n};\n\n/**\n * React hook for handling drag and drop file uploads with validation.\n * Provides drag state management, file validation, and file picker integration.\n *\n * @param options - Configuration and event handlers\n * @returns Drag and drop state and handlers\n *\n * @example\n * ```tsx\n * const dragDrop = useDragDrop({\n * accept: ['image/*', '.pdf'],\n * maxFiles: 5,\n * maxFileSize: 10 * 1024 * 1024, // 10MB\n * multiple: true,\n * onFilesReceived: (files) => {\n * console.log('Received files:', files);\n * // Process files with upload hooks\n * },\n * onValidationError: (errors) => {\n * console.error('Validation errors:', errors);\n * },\n * });\n *\n * return (\n * <div>\n * <div\n * {...dragDrop.dragHandlers}\n * style={{\n * border: dragDrop.state.isDragging ? '2px dashed #007bff' : '2px dashed #ccc',\n * backgroundColor: dragDrop.state.isOver ? '#f8f9fa' : 'transparent',\n * padding: '2rem',\n * textAlign: 'center',\n * cursor: 'pointer',\n * }}\n * onClick={dragDrop.openFilePicker}\n * >\n * {dragDrop.state.isDragging ? (\n * <p>Drop files here...</p>\n * ) : (\n * <p>Drag files here or click to select</p>\n * )}\n *\n * {dragDrop.state.errors.length > 0 && (\n * <div style={{ color: 'red', marginTop: '1rem' }}>\n * {dragDrop.state.errors.map((error, index) => (\n * <p key={index}>{error}</p>\n * ))}\n * </div>\n * )}\n * </div>\n *\n * <input {...dragDrop.inputProps} />\n * </div>\n * );\n * ```\n */\nexport function useDragDrop(options: DragDropOptions = {}): UseDragDropReturn {\n const {\n accept,\n maxFiles,\n maxFileSize,\n multiple = true,\n validator,\n onFilesReceived,\n onValidationError,\n onDragStateChange,\n } = options;\n\n const [state, setState] = useState<DragDropState>(initialState);\n const inputRef = useRef<HTMLInputElement>(null);\n const dragCounterRef = useRef(0);\n\n const updateState = useCallback((update: Partial<DragDropState>) => {\n setState((prev) => ({ ...prev, ...update }));\n }, []);\n\n const validateFiles = useCallback(\n (files: File[]): string[] => {\n const errors: string[] = [];\n\n // Check file count\n if (maxFiles && files.length > maxFiles) {\n errors.push(\n `Maximum ${maxFiles} files allowed. You selected ${files.length} files.`,\n );\n }\n\n // Check individual files\n for (const file of files) {\n // Check file size\n if (maxFileSize && file.size > maxFileSize) {\n const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(1);\n const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);\n errors.push(\n `File \"${file.name}\" (${fileSizeMB}MB) exceeds maximum size of ${maxSizeMB}MB.`,\n );\n }\n\n // Check file type\n if (accept && accept.length > 0) {\n const isAccepted = accept.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n } else {\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n } else {\n return file.type === acceptType;\n }\n }\n });\n\n if (!isAccepted) {\n errors.push(\n `File \"${file.name}\" type \"${file.type}\" is not accepted. Accepted types: ${accept.join(\", \")}.`,\n );\n }\n }\n }\n\n // Run custom validator\n if (validator) {\n const customErrors = validator(files);\n if (customErrors) {\n errors.push(...customErrors);\n }\n }\n\n return errors;\n },\n [accept, maxFiles, maxFileSize, validator],\n );\n\n const processFiles = useCallback(\n (files: File[]) => {\n const fileArray = Array.from(files);\n const errors = validateFiles(fileArray);\n\n if (errors.length > 0) {\n updateState({ errors, isValid: false });\n onValidationError?.(errors);\n } else {\n updateState({ errors: [], isValid: true });\n onFilesReceived?.(fileArray);\n }\n },\n [validateFiles, updateState, onFilesReceived, onValidationError],\n );\n\n const getFilesFromDataTransfer = useCallback(\n (dataTransfer: DataTransfer): File[] => {\n const files: File[] = [];\n\n if (dataTransfer.items) {\n // Use DataTransferItemList interface\n for (let i = 0; i < dataTransfer.items.length; i++) {\n const item = dataTransfer.items[i];\n if (item && item.kind === \"file\") {\n const file = item.getAsFile();\n if (file) {\n files.push(file);\n }\n }\n }\n } else {\n // Fallback to DataTransfer.files\n for (let i = 0; i < dataTransfer.files.length; i++) {\n const file = dataTransfer.files[i];\n if (file) {\n files.push(file);\n }\n }\n }\n\n return files;\n },\n [],\n );\n\n const onDragEnter = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current++;\n\n if (dragCounterRef.current === 1) {\n updateState({ isDragging: true, isOver: true });\n onDragStateChange?.(true);\n }\n },\n [updateState, onDragStateChange],\n );\n\n const onDragOver = useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n // Set dropEffect to indicate what operation is allowed\n if (event.dataTransfer) {\n event.dataTransfer.dropEffect = \"copy\";\n }\n }, []);\n\n const onDragLeave = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current--;\n\n if (dragCounterRef.current === 0) {\n updateState({ isDragging: false, isOver: false, errors: [] });\n onDragStateChange?.(false);\n }\n },\n [updateState, onDragStateChange],\n );\n\n const onDrop = useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n dragCounterRef.current = 0;\n updateState({ isDragging: false, isOver: false });\n onDragStateChange?.(false);\n\n if (event.dataTransfer) {\n const files = getFilesFromDataTransfer(event.dataTransfer);\n if (files.length > 0) {\n processFiles(files);\n }\n }\n },\n [updateState, onDragStateChange, getFilesFromDataTransfer, processFiles],\n );\n\n const openFilePicker = useCallback(() => {\n inputRef.current?.click();\n }, []);\n\n const onInputChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n if (event.target.files && event.target.files.length > 0) {\n const files = Array.from(event.target.files);\n processFiles(files);\n }\n\n // Reset input value to allow selecting the same files again\n event.target.value = \"\";\n },\n [processFiles],\n );\n\n const reset = useCallback(() => {\n setState(initialState);\n dragCounterRef.current = 0;\n }, []);\n\n const dragHandlers = {\n onDragEnter,\n onDragOver,\n onDragLeave,\n onDrop,\n };\n\n const inputProps = {\n type: \"file\" as const,\n multiple,\n accept: accept?.join(\", \"),\n onChange: onInputChange,\n style: { display: \"none\" as const },\n ref: inputRef,\n };\n\n return {\n state,\n dragHandlers,\n inputProps,\n openFilePicker,\n processFiles,\n reset,\n };\n}\n","import type {\n FlowUploadOptions,\n UploadistaEvent,\n} from \"@uploadista/client-browser\";\nimport {\n FlowManager,\n type FlowUploadState,\n type FlowUploadStatus,\n type InternalFlowUploadOptions,\n} from \"@uploadista/client-core\";\nimport { EventType, type FlowEvent } from \"@uploadista/core/flow\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { UploadEventType } from \"@uploadista/core/types\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n/**\n * Type guard to check if an event is a flow event\n */\nfunction isFlowEvent(event: UploadistaEvent): event is FlowEvent {\n // FlowEvent has eventType, not type\n const flowEvent = event as FlowEvent;\n return (\n flowEvent.eventType === EventType.FlowStart ||\n flowEvent.eventType === EventType.FlowEnd ||\n flowEvent.eventType === EventType.FlowError ||\n flowEvent.eventType === EventType.NodeStart ||\n flowEvent.eventType === EventType.NodeEnd ||\n flowEvent.eventType === EventType.NodePause ||\n flowEvent.eventType === EventType.NodeResume ||\n flowEvent.eventType === EventType.NodeError\n );\n}\n\n// Re-export types from core for convenience\nexport type { FlowUploadState, FlowUploadStatus };\n\n/**\n * Return value from the useFlowUpload hook with upload control methods and state.\n *\n * @template TOutput - Type of the final output from the flow (defaults to UploadFile)\n *\n * @property state - Complete flow upload state with progress and outputs\n * @property upload - Function to initiate file upload through the flow\n * @property abort - Cancel the current upload and flow execution\n * @property pause - Pause the current upload\n * @property reset - Reset state to idle (clears all data)\n * @property isUploading - True when upload or processing is active\n * @property isUploadingFile - True only during file upload phase\n * @property isProcessing - True only during flow processing phase\n */\nexport interface UseFlowUploadReturn<TOutput = UploadFile> {\n /**\n * Current upload state\n */\n state: FlowUploadState<TOutput>;\n\n /**\n * Upload a file through the flow\n */\n upload: (file: File | Blob) => Promise<void>;\n\n /**\n * Abort the current upload\n */\n abort: () => void;\n\n /**\n * Pause the current upload\n */\n pause: () => void;\n\n /**\n * Reset the upload state\n */\n reset: () => void;\n\n /**\n * Whether an upload or flow execution is in progress (uploading OR processing)\n */\n isUploading: boolean;\n\n /**\n * Whether the file is currently being uploaded (chunks being sent)\n */\n isUploadingFile: boolean;\n\n /**\n * Whether the flow is currently processing (after upload completes)\n */\n isProcessing: boolean;\n}\n\nconst initialState: FlowUploadState = {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: null,\n error: null,\n result: null,\n jobId: null,\n flowStarted: false,\n currentNodeName: null,\n currentNodeType: null,\n flowOutputs: null,\n};\n\n/**\n * React hook for uploading files through a flow with automatic flow execution.\n * Handles both the file upload phase and the flow processing phase, providing\n * real-time progress updates and flow node execution tracking.\n *\n * The flow engine processes the uploaded file through a DAG of nodes, which can\n * perform operations like image optimization, storage saving, webhooks, etc.\n *\n * Must be used within an UploadistaProvider. Flow events (node start/end, flow complete)\n * are automatically subscribed through the provider context.\n *\n * @template TOutput - Type of the final result from the flow (defaults to UploadFile)\n * @param options - Flow upload configuration including flow ID and event handlers\n * @returns Flow upload state and control methods\n *\n * @example\n * ```tsx\n * // Basic flow upload with progress tracking\n * function ImageUploader() {\n * const flowUpload = useFlowUpload({\n * flowConfig: {\n * flowId: \"image-optimization-flow\",\n * storageId: \"s3-images\",\n * outputNodeId: \"optimized-output\", // Optional: specify which output to use\n * },\n * onSuccess: (result) => {\n * console.log(\"Image optimized and saved:\", result);\n * },\n * onFlowComplete: (outputs) => {\n * console.log(\"All flow outputs:\", outputs);\n * // outputs might include: { thumbnail: {...}, optimized: {...}, original: {...} }\n * },\n * onError: (error) => {\n * console.error(\"Upload or processing failed:\", error);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * accept=\"image/*\"\n * onChange={(e) => {\n * const file = e.target.files?.[0];\n * if (file) flowUpload.upload(file);\n * }}\n * />\n *\n * {flowUpload.isUploadingFile && (\n * <div>Uploading... {flowUpload.state.progress}%</div>\n * )}\n *\n * {flowUpload.isProcessing && (\n * <div>\n * Processing...\n * {flowUpload.state.currentNodeName && (\n * <span>Current step: {flowUpload.state.currentNodeName}</span>\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"success\" && (\n * <div>\n * <p>Upload complete!</p>\n * {flowUpload.state.result && (\n * <img src={flowUpload.state.result.url} alt=\"Uploaded\" />\n * )}\n * </div>\n * )}\n *\n * {flowUpload.state.status === \"error\" && (\n * <div>\n * <p>Error: {flowUpload.state.error?.message}</p>\n * <button onClick={flowUpload.reset}>Try Again</button>\n * </div>\n * )}\n *\n * {flowUpload.isUploading && (\n * <button onClick={flowUpload.abort}>Cancel</button>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @see {@link useMultiFlowUpload} for uploading multiple files through a flow\n * @see {@link useUpload} for simple uploads without flow processing\n */\nexport function useFlowUpload<TOutput = UploadFile>(\n options: FlowUploadOptions<TOutput>,\n): UseFlowUploadReturn<TOutput> {\n // Get client from context\n const client = useUploadistaContext();\n const [state, setState] = useState<FlowUploadState<TOutput>>(\n initialState as FlowUploadState<TOutput>,\n );\n const managerRef = useRef<FlowManager<File | Blob, TOutput> | null>(null);\n\n // Create FlowManager instance once (only recreate if client changes)\n // Note: We don't include options in deps to avoid recreating the manager on every render\n // The manager will use the latest options values through closures\n useEffect(() => {\n managerRef.current = new FlowManager(\n async (\n file: File | Blob,\n flowConfig: {\n flowId: string;\n storageId: string;\n outputNodeId?: string;\n metadata?: Record<string, string>;\n },\n internalOptions: InternalFlowUploadOptions,\n ) => {\n const result = await client.client.uploadWithFlow(file, flowConfig, {\n onJobStart: internalOptions.onJobStart,\n onProgress: internalOptions.onProgress,\n onChunkComplete: internalOptions.onChunkComplete,\n onSuccess: internalOptions.onSuccess,\n onError: internalOptions.onError,\n onShouldRetry: internalOptions.onShouldRetry,\n });\n // Return only abort and pause (ignore jobId and return value)\n return {\n abort: async () => {\n await result.abort();\n },\n pause: async () => {\n await result.pause();\n // Ignore the FlowJob return value\n },\n };\n },\n {\n onStateChange: setState,\n onProgress: options.onProgress,\n onChunkComplete: options.onChunkComplete,\n onFlowComplete: options.onFlowComplete,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onAbort: options.onAbort,\n },\n options,\n );\n\n return () => {\n managerRef.current?.cleanup();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client]);\n\n // Subscribe to events and forward them to the manager\n useEffect(() => {\n const unsubscribe = client.subscribeToEvents((event: UploadistaEvent) => {\n // Handle flow events\n if (isFlowEvent(event)) {\n console.log(\"[useFlowUpload] Flow event received:\", event.eventType, \"jobId:\", event.jobId, \"current:\", managerRef.current?.getJobId());\n managerRef.current?.handleFlowEvent(event);\n return;\n }\n\n // Handle upload progress events for this job's upload\n const uploadEvent = event as {\n type: string;\n data?: { id: string; progress: number; total: number };\n flow?: { jobId: string };\n };\n\n if (\n uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&\n uploadEvent.flow?.jobId === managerRef.current?.getJobId() &&\n uploadEvent.data\n ) {\n const { progress: bytesUploaded, total: totalBytes } = uploadEvent.data;\n\n managerRef.current?.handleUploadProgress(\n uploadEvent.data.id,\n bytesUploaded,\n totalBytes,\n );\n }\n });\n\n return unsubscribe;\n }, [client]);\n\n // Wrap manager methods with useCallback\n const upload = useCallback(async (file: File | Blob) => {\n await managerRef.current?.upload(file);\n }, []);\n\n const abort = useCallback(() => {\n managerRef.current?.abort();\n }, []);\n\n const pause = useCallback(() => {\n managerRef.current?.pause();\n }, []);\n\n const reset = useCallback(() => {\n managerRef.current?.reset();\n }, []);\n\n // Derive computed values from state (reactive to state changes)\n const isUploading =\n state.status === \"uploading\" || state.status === \"processing\";\n const isUploadingFile = state.status === \"uploading\";\n const isProcessing = state.status === \"processing\";\n\n return {\n state,\n upload,\n abort,\n pause,\n reset,\n isUploading,\n isUploadingFile,\n isProcessing,\n };\n}\n","import type { BrowserUploadInput } from \"@uploadista/client-browser\";\nimport type { UploadMetrics } from \"@uploadista/client-core\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\nimport type { UploadState, UploadStatus, UseUploadOptions } from \"./use-upload\";\n\nexport interface UploadItem {\n id: string;\n file: BrowserUploadInput;\n state: UploadState;\n}\n\nexport interface MultiUploadOptions\n extends Omit<UseUploadOptions, \"onSuccess\" | \"onError\" | \"onProgress\"> {\n /**\n * Maximum number of concurrent uploads\n */\n maxConcurrent?: number;\n\n /**\n * Called when an individual file upload starts\n */\n onUploadStart?: (item: UploadItem) => void;\n\n /**\n * Called when an individual file upload progresses\n */\n onUploadProgress?: (\n item: UploadItem,\n progress: number,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => void;\n\n /**\n * Called when an individual file upload succeeds\n */\n onUploadSuccess?: (item: UploadItem, result: UploadFile) => void;\n\n /**\n * Called when an individual file upload fails\n */\n onUploadError?: (item: UploadItem, error: Error) => void;\n\n /**\n * Called when all uploads complete (successfully or with errors)\n */\n onComplete?: (results: {\n successful: UploadItem[];\n failed: UploadItem[];\n total: number;\n }) => void;\n}\n\nexport interface MultiUploadState {\n /**\n * Total number of uploads\n */\n total: number;\n\n /**\n * Number of completed uploads (successful + failed)\n */\n completed: number;\n\n /**\n * Number of successful uploads\n */\n successful: number;\n\n /**\n * Number of failed uploads\n */\n failed: number;\n\n /**\n * Number of currently uploading files\n */\n uploading: number;\n\n /**\n * Overall progress as a percentage (0-100)\n */\n progress: number;\n\n /**\n * Total bytes uploaded across all files\n */\n totalBytesUploaded: number;\n\n /**\n * Total bytes to upload across all files\n */\n totalBytes: number;\n\n /**\n * Whether any uploads are currently active\n */\n isUploading: boolean;\n\n /**\n * Whether all uploads have completed\n */\n isComplete: boolean;\n}\n\nexport interface UseMultiUploadReturn {\n /**\n * Current multi-upload state\n */\n state: MultiUploadState;\n\n /**\n * Array of all upload items\n */\n items: UploadItem[];\n\n /**\n * Add files to the upload queue\n */\n addFiles: (files: BrowserUploadInput[]) => void;\n\n /**\n * Remove an item from the queue (only if not currently uploading)\n */\n removeItem: (id: string) => void;\n\n /**\n * Remove a file from the queue (alias for removeItem)\n */\n removeFile: (id: string) => void;\n\n /**\n * Start all pending uploads\n */\n startAll: () => void;\n\n /**\n * Abort a specific upload by ID\n */\n abortUpload: (id: string) => void;\n\n /**\n * Abort all active uploads\n */\n abortAll: () => void;\n\n /**\n * Retry a specific failed upload by ID\n */\n retryUpload: (id: string) => void;\n\n /**\n * Retry all failed uploads\n */\n retryFailed: () => void;\n\n /**\n * Clear all completed uploads (successful and failed)\n */\n clearCompleted: () => void;\n\n /**\n * Clear all items\n */\n clearAll: () => void;\n\n /**\n * Get items by status\n */\n getItemsByStatus: (status: UploadStatus) => UploadItem[];\n\n /**\n * Aggregated upload metrics and performance insights from the client\n */\n metrics: UploadMetrics;\n}\n\n/**\n * React hook for managing multiple file uploads with queue management,\n * concurrent upload limits, and batch operations.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param options - Multi-upload configuration and event handlers\n * @returns Multi-upload state and control methods\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const multiUpload = useMultiUpload({\n * maxConcurrent: 3,\n * onUploadSuccess: (item, result) => {\n * console.log(`${item.file.name} uploaded successfully`);\n * },\n * onComplete: (results) => {\n * console.log(`Upload batch complete: ${results.successful.length}/${results.total} successful`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * multiple\n * onChange={(e) => {\n * if (e.target.files) {\n * multiUpload.addFiles(Array.from(e.target.files));\n * }\n * }}\n * />\n *\n * <div>Progress: {multiUpload.state.progress}%</div>\n * <div>\n * {multiUpload.state.uploading} uploading, {multiUpload.state.successful} successful,\n * {multiUpload.state.failed} failed\n * </div>\n *\n * <button onClick={multiUpload.startAll} disabled={multiUpload.state.isUploading}>\n * Start All\n * </button>\n * <button onClick={multiUpload.abortAll} disabled={!multiUpload.state.isUploading}>\n * Abort All\n * </button>\n * <button onClick={multiUpload.retryFailed} disabled={multiUpload.state.failed === 0}>\n * Retry Failed\n * </button>\n *\n * {multiUpload.items.map((item) => (\n * <div key={item.id}>\n * {item.file.name}: {item.state.status} ({item.state.progress}%)\n * </div>\n * ))}\n * </div>\n * );\n * }\n * ```\n */\n\nexport function useMultiUpload(\n options: MultiUploadOptions = {},\n): UseMultiUploadReturn {\n const uploadClient = useUploadistaContext();\n const { maxConcurrent = 3 } = options;\n const [items, setItems] = useState<UploadItem[]>([]);\n const itemsRef = useRef<UploadItem[]>([]);\n const nextIdRef = useRef(0);\n const activeUploadsRef = useRef(new Set<string>());\n\n // Store abort controllers for each upload\n const abortControllersRef = useRef<Map<string, { abort: () => void }>>(\n new Map(),\n );\n\n // Keep ref in sync with state (also updated synchronously in setItems callbacks)\n itemsRef.current = items;\n\n // Generate a unique ID for each upload item\n const generateId = useCallback(() => {\n return `upload-${Date.now()}-${nextIdRef.current++}`;\n }, []);\n\n // State update callback for individual uploads\n const onStateUpdate = useCallback(\n (id: string, state: Partial<UploadState>) => {\n setItems((prev) => {\n const updated = prev.map((item) =>\n item.id === id\n ? { ...item, state: { ...item.state, ...state } }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n },\n [],\n );\n\n // Check if all uploads are complete and trigger completion callback\n const checkForCompletion = useCallback(() => {\n const currentItems = itemsRef.current;\n const allComplete = currentItems.every((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n );\n\n if (allComplete && currentItems.length > 0) {\n const successful = currentItems.filter(\n (item) => item.state.status === \"success\",\n );\n const failed = currentItems.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n );\n\n options.onComplete?.({\n successful,\n failed,\n total: currentItems.length,\n });\n }\n }, [options]);\n\n // Start the next available upload if we have capacity\n const startNextUpload = useCallback(() => {\n if (activeUploadsRef.current.size >= maxConcurrent) {\n return;\n }\n\n const currentItems = itemsRef.current;\n const nextItem = currentItems.find(\n (item) =>\n item.state.status === \"idle\" && !activeUploadsRef.current.has(item.id),\n );\n\n if (!nextItem) {\n return;\n }\n\n // Perform upload inline to avoid circular dependency\n const performUploadInline = async () => {\n activeUploadsRef.current.add(nextItem.id);\n options.onUploadStart?.(nextItem);\n\n // Update state to uploading\n onStateUpdate(nextItem.id, { status: \"uploading\" });\n\n try {\n const controller = await uploadClient.client.upload(nextItem.file, {\n metadata: options.metadata,\n uploadLengthDeferred: options.uploadLengthDeferred,\n uploadSize: options.uploadSize,\n\n onProgress: (\n _uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => {\n const progress = totalBytes\n ? Math.round((bytesUploaded / totalBytes) * 100)\n : 0;\n\n onStateUpdate(nextItem.id, {\n progress,\n bytesUploaded,\n totalBytes,\n });\n\n options.onUploadProgress?.(\n nextItem,\n progress,\n bytesUploaded,\n totalBytes,\n );\n },\n\n onChunkComplete: () => {\n // Optional: could expose this as an option\n },\n\n onSuccess: (result: UploadFile) => {\n onStateUpdate(nextItem.id, {\n status: \"success\",\n result,\n progress: 100,\n });\n\n const updatedItem = {\n ...nextItem,\n state: { ...nextItem.state, status: \"success\" as const, result },\n };\n options.onUploadSuccess?.(updatedItem, result);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n },\n\n onError: (error: Error) => {\n onStateUpdate(nextItem.id, {\n status: \"error\",\n error,\n });\n\n const updatedItem = {\n ...nextItem,\n state: { ...nextItem.state, status: \"error\" as const, error },\n };\n options.onUploadError?.(updatedItem, error);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n },\n\n onShouldRetry: options.onShouldRetry,\n });\n\n // Store abort controller\n abortControllersRef.current.set(nextItem.id, controller);\n } catch (error) {\n onStateUpdate(nextItem.id, {\n status: \"error\",\n error: error as Error,\n });\n\n const updatedItem = {\n ...nextItem,\n state: {\n ...nextItem.state,\n status: \"error\" as const,\n error: error as Error,\n },\n };\n options.onUploadError?.(updatedItem, error as Error);\n\n // Mark complete and start next\n activeUploadsRef.current.delete(nextItem.id);\n abortControllersRef.current.delete(nextItem.id);\n startNextUpload();\n checkForCompletion();\n }\n };\n\n performUploadInline();\n }, [maxConcurrent, uploadClient, options, onStateUpdate, checkForCompletion]);\n\n // Calculate overall state\n const state: MultiUploadState = {\n total: items.length,\n completed: items.filter((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n ).length,\n successful: items.filter((item) => item.state.status === \"success\").length,\n failed: items.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n ).length,\n uploading: items.filter((item) => item.state.status === \"uploading\").length,\n progress:\n items.length > 0\n ? Math.round(\n items.reduce((sum, item) => sum + item.state.progress, 0) /\n items.length,\n )\n : 0,\n totalBytesUploaded: items.reduce(\n (sum, item) => sum + item.state.bytesUploaded,\n 0,\n ),\n totalBytes: items.reduce(\n (sum, item) => sum + (item.state.totalBytes || 0),\n 0,\n ),\n isUploading: items.some((item) => item.state.status === \"uploading\"),\n isComplete:\n items.length > 0 &&\n items.every((item) =>\n [\"success\", \"error\", \"aborted\"].includes(item.state.status),\n ),\n };\n\n const addFiles = useCallback(\n (files: BrowserUploadInput[]) => {\n const newItems: UploadItem[] = files.map((file) => {\n const id = generateId();\n return {\n id,\n file,\n state: {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: file instanceof File ? file.size : null,\n error: null,\n result: null,\n },\n };\n });\n\n console.log(\"addFiles: Adding\", newItems.length, \"files\");\n\n // Update ref synchronously BEFORE setItems\n const updated = [...itemsRef.current, ...newItems];\n itemsRef.current = updated;\n console.log(\n \"addFiles: Updated itemsRef.current to\",\n updated.length,\n \"items\",\n );\n\n setItems(updated);\n },\n [generateId],\n );\n\n const removeItem = useCallback((id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && item.state.status === \"uploading\") {\n // Abort before removing\n const controller = abortControllersRef.current.get(id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(id);\n }\n }\n\n setItems((prev) => {\n const updated = prev.filter((item) => item.id !== id);\n itemsRef.current = updated;\n return updated;\n });\n activeUploadsRef.current.delete(id);\n }, []);\n\n const abortUpload = useCallback(\n (id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && item.state.status === \"uploading\") {\n const controller = abortControllersRef.current.get(id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(id);\n }\n\n activeUploadsRef.current.delete(id);\n\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === id\n ? { ...i, state: { ...i.state, status: \"aborted\" as const } }\n : i,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Try to start next upload in queue\n startNextUpload();\n }\n },\n [startNextUpload],\n );\n\n const retryUpload = useCallback(\n (id: string) => {\n const currentItems = itemsRef.current;\n const item = currentItems.find((i) => i.id === id);\n if (item && [\"error\", \"aborted\"].includes(item.state.status)) {\n setItems((prev) => {\n const updated = prev.map((i) =>\n i.id === id\n ? {\n ...i,\n state: { ...i.state, status: \"idle\" as const, error: null },\n }\n : i,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Auto-start the upload\n setTimeout(() => startNextUpload(), 0);\n }\n },\n [startNextUpload],\n );\n\n const startAll = useCallback(() => {\n const currentItems = itemsRef.current;\n console.log(\"Starting all uploads\", currentItems);\n // Start as many uploads as we can up to the concurrent limit\n const idleItems = currentItems.filter(\n (item) => item.state.status === \"idle\",\n );\n const slotsAvailable = maxConcurrent - activeUploadsRef.current.size;\n const itemsToStart = idleItems.slice(0, slotsAvailable);\n\n for (const item of itemsToStart) {\n console.log(\"Starting next upload\", item);\n startNextUpload();\n }\n }, [maxConcurrent, startNextUpload]);\n\n const abortAll = useCallback(() => {\n const currentItems = itemsRef.current;\n currentItems\n .filter((item) => item.state.status === \"uploading\")\n .forEach((item) => {\n const controller = abortControllersRef.current.get(item.id);\n if (controller) {\n controller.abort();\n abortControllersRef.current.delete(item.id);\n }\n });\n\n activeUploadsRef.current.clear();\n\n // Update all uploading items to aborted status\n setItems((prev) => {\n const updated = prev.map((item) =>\n item.state.status === \"uploading\"\n ? { ...item, state: { ...item.state, status: \"aborted\" as const } }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n }, []);\n\n const retryFailed = useCallback(() => {\n const currentItems = itemsRef.current;\n const failedItems = currentItems.filter((item) =>\n [\"error\", \"aborted\"].includes(item.state.status),\n );\n\n if (failedItems.length > 0) {\n setItems((prev) => {\n const updated = prev.map((item) =>\n failedItems.some((f) => f.id === item.id)\n ? {\n ...item,\n state: { ...item.state, status: \"idle\" as const, error: null },\n }\n : item,\n );\n itemsRef.current = updated;\n return updated;\n });\n\n // Auto-start uploads if we have capacity\n setTimeout(startAll, 0);\n }\n }, [startAll]);\n\n const clearCompleted = useCallback(() => {\n setItems((prev) => {\n const updated = prev.filter(\n (item) => ![\"success\", \"error\", \"aborted\"].includes(item.state.status),\n );\n itemsRef.current = updated;\n return updated;\n });\n }, []);\n\n const clearAll = useCallback(() => {\n abortAll();\n setItems([]);\n itemsRef.current = [];\n activeUploadsRef.current.clear();\n }, [abortAll]);\n\n const getItemsByStatus = useCallback((status: UploadStatus) => {\n return itemsRef.current.filter((item) => item.state.status === status);\n }, []);\n\n // Create aggregated metrics object that delegates to the upload client\n const metrics: UploadMetrics = {\n getInsights: () => uploadClient.client.getChunkingInsights(),\n exportMetrics: () => uploadClient.client.exportMetrics(),\n getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),\n getNetworkCondition: () => uploadClient.client.getNetworkCondition(),\n resetMetrics: () => uploadClient.client.resetMetrics(),\n };\n\n return {\n state,\n items,\n addFiles,\n removeItem,\n removeFile: removeItem, // Alias for consistency with MultiUploadExample\n startAll,\n abortUpload,\n abortAll,\n retryUpload,\n retryFailed,\n clearCompleted,\n clearAll,\n getItemsByStatus,\n metrics,\n };\n}\n","import type { BrowserUploadInput } from \"@uploadista/client-browser\";\nimport type { UploadMetrics, UploadOptions } from \"@uploadista/client-core\";\nimport {\n UploadManager,\n type UploadState,\n type UploadStatus,\n} from \"@uploadista/client-core\";\nimport type { UploadFile } from \"@uploadista/core/types\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\n// Re-export types from core for convenience\nexport type { UploadState, UploadStatus };\n\nexport interface UseUploadOptions {\n /**\n * Upload metadata to attach to the file\n */\n metadata?: Record<string, string>;\n\n /**\n * Whether to defer the upload size calculation\n */\n uploadLengthDeferred?: boolean;\n\n /**\n * Manual upload size override\n */\n uploadSize?: number;\n\n /**\n * Called when upload progress updates\n *\n * @param uploadId - The unique identifier for this upload\n * @param bytesUploaded - Number of bytes uploaded\n * @param totalBytes - Total bytes to upload, null if unknown\n */\n onProgress?: (\n uploadId: string,\n bytesUploaded: number,\n totalBytes: number | null,\n ) => void;\n\n /**\n * Called when a chunk completes\n *\n * @param chunkSize - Size of the completed chunk\n * @param bytesAccepted - Total bytes accepted so far\n * @param bytesTotal - Total bytes to upload, null if unknown\n */\n onChunkComplete?: (\n chunkSize: number,\n bytesAccepted: number,\n bytesTotal: number | null,\n ) => void;\n\n /**\n * Called when upload succeeds\n *\n * @param result - The uploaded file result\n */\n onSuccess?: (result: UploadFile) => void;\n\n /**\n * Called when upload fails\n *\n * @param error - The error that caused the failure\n */\n onError?: (error: Error) => void;\n\n /**\n * Called when upload is aborted\n */\n onAbort?: () => void;\n\n /**\n * Custom retry logic\n *\n * @param error - The error that triggered the retry check\n * @param retryAttempt - The current retry attempt number\n * @returns true to retry, false to fail\n */\n onShouldRetry?: (error: Error, retryAttempt: number) => boolean;\n}\n\nexport interface UseUploadReturn {\n /**\n * Current upload state\n */\n state: UploadState;\n\n /**\n * Start uploading a file\n */\n upload: (file: BrowserUploadInput) => void;\n\n /**\n * Abort the current upload\n */\n abort: () => void;\n\n /**\n * Reset the upload state to idle\n */\n reset: () => void;\n\n /**\n * Retry the last failed upload\n */\n retry: () => void;\n\n /**\n * Whether an upload is currently active\n */\n isUploading: boolean;\n\n /**\n * Whether the upload can be retried\n */\n canRetry: boolean;\n\n /**\n * Upload metrics and performance insights from the client\n */\n metrics: UploadMetrics;\n}\n\n/**\n * React hook for managing individual file uploads with full state management.\n * Provides upload progress tracking, error handling, abort functionality, and retry logic.\n *\n * Must be used within an UploadistaProvider.\n *\n * @param options - Upload configuration and event handlers\n * @returns Upload state and control methods\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const upload = useUpload({\n * onSuccess: (result) => console.log('Upload complete:', result),\n * onError: (error) => console.error('Upload failed:', error),\n * onProgress: (uploadId, bytesUploaded, totalBytes) => {\n * const progress = totalBytes ? Math.round((bytesUploaded / totalBytes) * 100) : 0;\n * console.log(`Upload ${uploadId}: ${progress}% (${bytesUploaded}/${totalBytes} bytes)`);\n * },\n * });\n *\n * return (\n * <div>\n * <input\n * type=\"file\"\n * onChange={(e) => {\n * const file = e.target.files?.[0];\n * if (file) upload.upload(file);\n * }}\n * />\n * {upload.isUploading && <div>Progress: {upload.state.progress}%</div>}\n * {upload.state.error && <div>Error: {upload.state.error.message}</div>}\n * {upload.canRetry && <button onClick={upload.retry}>Retry</button>}\n * <button onClick={upload.abort} disabled={!upload.isUploading}>Abort</button>\n * </div>\n * );\n * }\n * ```\n */\nconst initialState: UploadState = {\n status: \"idle\",\n progress: 0,\n bytesUploaded: 0,\n totalBytes: null,\n error: null,\n result: null,\n};\n\nexport function useUpload(options: UseUploadOptions = {}): UseUploadReturn {\n const uploadClient = useUploadistaContext();\n const [state, setState] = useState<UploadState>(initialState);\n const managerRef = useRef<UploadManager<\n BrowserUploadInput,\n UploadOptions\n > | null>(null);\n\n // Create UploadManager instance\n useEffect(() => {\n managerRef.current = new UploadManager(\n (file: BrowserUploadInput, opts: UploadOptions) =>\n uploadClient.client.upload(file, opts),\n {\n onStateChange: setState,\n onProgress: options.onProgress,\n onChunkComplete: options.onChunkComplete,\n onSuccess: options.onSuccess,\n onError: options.onError,\n onAbort: options.onAbort,\n },\n {\n metadata: options.metadata,\n uploadLengthDeferred: options.uploadLengthDeferred,\n uploadSize: options.uploadSize,\n onShouldRetry: options.onShouldRetry,\n },\n );\n\n return () => {\n managerRef.current?.cleanup();\n };\n }, [uploadClient, options]);\n\n // Wrap manager methods with useCallback\n const upload = useCallback((file: BrowserUploadInput) => {\n managerRef.current?.upload(file);\n }, []);\n\n const abort = useCallback(() => {\n managerRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n managerRef.current?.reset();\n }, []);\n\n const retry = useCallback(() => {\n managerRef.current?.retry();\n }, []);\n\n // Derive computed values from state\n const isUploading = state.status === \"uploading\";\n const canRetry = managerRef.current?.canRetry() ?? false;\n\n // Create metrics object that delegates to the upload client\n const metrics: UploadMetrics = {\n getInsights: () => uploadClient.client.getChunkingInsights(),\n exportMetrics: () => uploadClient.client.exportMetrics(),\n getNetworkMetrics: () => uploadClient.client.getNetworkMetrics(),\n getNetworkCondition: () => uploadClient.client.getNetworkCondition(),\n resetMetrics: () => uploadClient.client.resetMetrics(),\n };\n\n return {\n state,\n upload,\n abort,\n reset,\n retry,\n isUploading,\n canRetry,\n metrics,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{s as e}from"./use-upload-
|
|
2
|
-
//# sourceMappingURL=use-upload-metrics-
|
|
1
|
+
import{s as e}from"./use-upload-CbeBq3kV.mjs";import t,{useCallback as n,useRef as r,useState as i}from"react";const a={totalBytesUploaded:0,totalBytes:0,averageSpeed:0,currentSpeed:0,estimatedTimeRemaining:null,totalFiles:0,completedFiles:0,activeUploads:0,progress:0,peakSpeed:0,startTime:null,endTime:null,totalDuration:null,insights:{overallEfficiency:0,chunkingEffectiveness:0,networkStability:0,recommendations:[],optimalChunkSizeRange:{min:256*1024,max:2*1024*1024}},sessionMetrics:[],chunkMetrics:[]};function o(o={}){let{speedCalculationInterval:s=1e3,speedSampleSize:c=10,onMetricsUpdate:l,onFileStart:u,onFileProgress:d,onFileComplete:f}=o,p=e(),[m,h]=i(a),[g,_]=i([]),v=r([]),y=r(0),b=r(null),x=n((e,t)=>{let n={time:e,bytes:t};v.current.push(n),v.current.length>c&&(v.current=v.current.slice(-c));let r=0;if(v.current.length>=2){let e=v.current[v.current.length-1],t=v.current[v.current.length-2];if(e&&t){let n=(e.time-t.time)/1e3,i=e.bytes-t.bytes;r=n>0?i/n:0}}let i=0;if(v.current.length>=2){let e=v.current[0],t=v.current[v.current.length-1];if(e&&t){let n=(t.time-e.time)/1e3,r=t.bytes-e.bytes;i=n>0?r/n:0}}return{currentSpeed:r,averageSpeed:i}},[c]),S=n(()=>{let e=Date.now(),t=g.reduce((e,t)=>e+t.size,0),n=g.reduce((e,t)=>e+t.bytesUploaded,0),r=g.filter(e=>e.isComplete).length,i=g.filter(e=>!e.isComplete&&e.bytesUploaded>0).length,{currentSpeed:a,averageSpeed:o}=x(e,n),s=t>0?Math.round(n/t*100):0,c=null;a>0&&(c=(t-n)/a*1e3);let u=g.filter(e=>e.startTime>0),d=u.length>0?Math.min(...u.map(e=>e.startTime)):null,f=g.filter(e=>e.endTime!==null),_=f.length>0&&r===g.length?Math.max(...f.map(e=>e.endTime).filter(e=>e!==null)):null,v=d&&_?_-d:null,y={totalBytesUploaded:n,totalBytes:t,averageSpeed:o,currentSpeed:a,estimatedTimeRemaining:c,totalFiles:g.length,completedFiles:r,activeUploads:i,progress:s,peakSpeed:Math.max(m.peakSpeed,a),startTime:d,endTime:_,totalDuration:v,insights:p.client.getChunkingInsights(),sessionMetrics:[p.client.exportMetrics().session],chunkMetrics:p.client.exportMetrics().chunks};h(y),l?.(y)},[g,m.peakSpeed,x,l,p.client]),C=n(()=>(b.current&&clearInterval(b.current),b.current=setInterval(()=>{g.some(e=>!e.isComplete&&e.bytesUploaded>0)&&S()},s),()=>{b.current&&=(clearInterval(b.current),null)}),[s,S,g]),w=n((e,t,n)=>{let r={id:e,filename:t,size:n,bytesUploaded:0,progress:0,speed:0,startTime:Date.now(),endTime:null,duration:null,isComplete:!1};_(t=>t.find(t=>t.id===e)?t.map(t=>t.id===e?r:t):[...t,r]),u?.(r),g.filter(e=>!e.isComplete).length===0&&C()},[g,u,C]),T=n((e,t)=>{let n=Date.now();_(r=>r.map(r=>{if(r.id!==e)return r;let i=(n-r.startTime)/1e3,a=i>0?t/i:0,o=r.size>0?Math.round(t/r.size*100):0,s={...r,bytesUploaded:t,progress:o,speed:a};return d?.(s),s})),setTimeout(S,0)},[d,S]),E=n(e=>{let t=Date.now();_(n=>n.map(n=>{if(n.id!==e)return n;let r=t-n.startTime,i=r>0?n.size/r*1e3:0,a={...n,bytesUploaded:n.size,progress:100,speed:i,endTime:t,duration:r,isComplete:!0};return f?.(a),a})),setTimeout(S,0)},[f,S]),D=n(e=>{_(t=>t.filter(t=>t.id!==e)),setTimeout(S,0)},[S]),O=n(()=>{b.current&&=(clearInterval(b.current),null),h(a),_([]),v.current=[],y.current=0},[]),k=n(e=>g.find(t=>t.id===e),[g]),A=n(()=>({overall:m,files:g,exportTime:Date.now()}),[m,g]);return t.useEffect(()=>()=>{b.current&&clearInterval(b.current)},[]),{metrics:m,fileMetrics:g,startFileUpload:w,updateFileProgress:T,completeFileUpload:E,removeFile:D,reset:O,getFileMetrics:k,exportMetrics:A}}export{o as t};
|
|
2
|
+
//# sourceMappingURL=use-upload-metrics-dMP4qxq0.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-upload-metrics-BE7UcAaz.mjs","names":["initialMetrics: UploadMetrics","estimatedTimeRemaining: number | null","newMetrics: UploadMetrics","fileMetric: FileUploadMetrics"],"sources":["../src/hooks/use-upload-metrics.ts"],"sourcesContent":["import type {\n ChunkMetrics,\n PerformanceInsights,\n UploadSessionMetrics,\n} from \"@uploadista/client-core\";\nimport React, { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\nexport type Timeout = ReturnType<typeof setInterval>;\n\nexport interface UploadMetrics {\n /**\n * Total bytes uploaded across all files\n */\n totalBytesUploaded: number;\n\n /**\n * Total bytes to upload across all files\n */\n totalBytes: number;\n\n /**\n * Overall upload speed in bytes per second\n */\n averageSpeed: number;\n\n /**\n * Current upload speed in bytes per second\n */\n currentSpeed: number;\n\n /**\n * Estimated time remaining in milliseconds\n */\n estimatedTimeRemaining: number | null;\n\n /**\n * Total number of files being tracked\n */\n totalFiles: number;\n\n /**\n * Number of files completed\n */\n completedFiles: number;\n\n /**\n * Number of files currently uploading\n */\n activeUploads: number;\n\n /**\n * Overall progress as percentage (0-100)\n */\n progress: number;\n\n /**\n * Peak upload speed achieved\n */\n peakSpeed: number;\n\n /**\n * Start time of the first upload\n */\n startTime: number | null;\n\n /**\n * End time of the last completed upload\n */\n endTime: number | null;\n\n /**\n * Total duration of all uploads\n */\n totalDuration: number | null;\n\n /**\n * Detailed performance insights from the upload client\n */\n insights: PerformanceInsights;\n\n /**\n * Session metrics for completed uploads\n */\n sessionMetrics: Partial<UploadSessionMetrics>[];\n\n /**\n * Detailed chunk metrics from recent uploads\n */\n chunkMetrics: ChunkMetrics[];\n}\n\nexport interface FileUploadMetrics {\n id: string;\n filename: string;\n size: number;\n bytesUploaded: number;\n progress: number;\n speed: number;\n startTime: number;\n endTime: number | null;\n duration: number | null;\n isComplete: boolean;\n}\n\nexport interface UseUploadMetricsOptions {\n /**\n * Interval for calculating current speed (in milliseconds)\n */\n speedCalculationInterval?: number;\n\n /**\n * Number of speed samples to keep for average calculation\n */\n speedSampleSize?: number;\n\n /**\n * Called when metrics are updated\n */\n onMetricsUpdate?: (metrics: UploadMetrics) => void;\n\n /**\n * Called when a file upload starts\n */\n onFileStart?: (fileMetrics: FileUploadMetrics) => void;\n\n /**\n * Called when a file upload progresses\n */\n onFileProgress?: (fileMetrics: FileUploadMetrics) => void;\n\n /**\n * Called when a file upload completes\n */\n onFileComplete?: (fileMetrics: FileUploadMetrics) => void;\n}\n\nexport interface UseUploadMetricsReturn {\n /**\n * Current overall metrics\n */\n metrics: UploadMetrics;\n\n /**\n * Individual file metrics\n */\n fileMetrics: FileUploadMetrics[];\n\n /**\n * Start tracking a new file upload\n */\n startFileUpload: (id: string, filename: string, size: number) => void;\n\n /**\n * Update progress for a file upload\n */\n updateFileProgress: (id: string, bytesUploaded: number) => void;\n\n /**\n * Mark a file upload as complete\n */\n completeFileUpload: (id: string) => void;\n\n /**\n * Remove a file from tracking\n */\n removeFile: (id: string) => void;\n\n /**\n * Reset all metrics\n */\n reset: () => void;\n\n /**\n * Get metrics for a specific file\n */\n getFileMetrics: (id: string) => FileUploadMetrics | undefined;\n\n /**\n * Export metrics as JSON\n */\n exportMetrics: () => {\n overall: UploadMetrics;\n files: FileUploadMetrics[];\n exportTime: number;\n };\n}\n\nconst initialMetrics: UploadMetrics = {\n totalBytesUploaded: 0,\n totalBytes: 0,\n averageSpeed: 0,\n currentSpeed: 0,\n estimatedTimeRemaining: null,\n totalFiles: 0,\n completedFiles: 0,\n activeUploads: 0,\n progress: 0,\n peakSpeed: 0,\n startTime: null,\n endTime: null,\n totalDuration: null,\n insights: {\n overallEfficiency: 0,\n chunkingEffectiveness: 0,\n networkStability: 0,\n recommendations: [],\n optimalChunkSizeRange: { min: 256 * 1024, max: 2 * 1024 * 1024 },\n },\n sessionMetrics: [],\n chunkMetrics: [],\n};\n\n/**\n * React hook for tracking detailed upload metrics and performance statistics.\n * Provides comprehensive monitoring of upload progress, speed, and timing data.\n *\n * @param options - Configuration and event handlers\n * @returns Upload metrics state and control methods\n *\n * @example\n * ```tsx\n * const uploadMetrics = useUploadMetrics({\n * speedCalculationInterval: 1000, // Update speed every second\n * speedSampleSize: 10, // Keep last 10 speed samples for average\n * onMetricsUpdate: (metrics) => {\n * console.log(`Overall progress: ${metrics.progress}%`);\n * console.log(`Speed: ${(metrics.currentSpeed / 1024).toFixed(1)} KB/s`);\n * console.log(`ETA: ${metrics.estimatedTimeRemaining}ms`);\n * },\n * onFileComplete: (fileMetrics) => {\n * console.log(`${fileMetrics.filename} completed in ${fileMetrics.duration}ms`);\n * },\n * });\n *\n * // Start tracking a file\n * const handleFileStart = (file: File) => {\n * uploadMetrics.startFileUpload(file.name, file.name, file.size);\n * };\n *\n * // Update progress during upload\n * const handleProgress = (fileId: string, bytesUploaded: number) => {\n * uploadMetrics.updateFileProgress(fileId, bytesUploaded);\n * };\n *\n * // Display metrics\n * return (\n * <div>\n * <div>Overall Progress: {uploadMetrics.metrics.progress}%</div>\n * <div>Speed: {(uploadMetrics.metrics.currentSpeed / 1024).toFixed(1)} KB/s</div>\n * <div>Files: {uploadMetrics.metrics.completedFiles}/{uploadMetrics.metrics.totalFiles}</div>\n *\n * {uploadMetrics.metrics.estimatedTimeRemaining && (\n * <div>ETA: {Math.round(uploadMetrics.metrics.estimatedTimeRemaining / 1000)}s</div>\n * )}\n *\n * {uploadMetrics.fileMetrics.map((file) => (\n * <div key={file.id}>\n * {file.filename}: {file.progress}% ({(file.speed / 1024).toFixed(1)} KB/s)\n * </div>\n * ))}\n * </div>\n * );\n * ```\n */\nexport function useUploadMetrics(\n options: UseUploadMetricsOptions = {},\n): UseUploadMetricsReturn {\n const {\n speedCalculationInterval = 1000,\n speedSampleSize = 10,\n onMetricsUpdate,\n onFileStart,\n onFileProgress,\n onFileComplete,\n } = options;\n\n const uploadClient = useUploadistaContext();\n\n const [metrics, setMetrics] = useState<UploadMetrics>(initialMetrics);\n const [fileMetrics, setFileMetrics] = useState<FileUploadMetrics[]>([]);\n\n const speedSamplesRef = useRef<Array<{ time: number; bytes: number }>>([]);\n const lastUpdateRef = useRef<number>(0);\n const intervalRef = useRef<Timeout | null>(null);\n\n const calculateSpeed = useCallback(\n (currentTime: number, totalBytesUploaded: number) => {\n const sample = { time: currentTime, bytes: totalBytesUploaded };\n speedSamplesRef.current.push(sample);\n\n // Keep only recent samples\n if (speedSamplesRef.current.length > speedSampleSize) {\n speedSamplesRef.current = speedSamplesRef.current.slice(\n -speedSampleSize,\n );\n }\n\n // Calculate current speed (bytes per second)\n let currentSpeed = 0;\n if (speedSamplesRef.current.length >= 2) {\n const recent =\n speedSamplesRef.current[speedSamplesRef.current.length - 1];\n const previous =\n speedSamplesRef.current[speedSamplesRef.current.length - 2];\n if (recent && previous) {\n const timeDiff = (recent.time - previous.time) / 1000; // Convert to seconds\n const bytesDiff = recent.bytes - previous.bytes;\n currentSpeed = timeDiff > 0 ? bytesDiff / timeDiff : 0;\n }\n }\n\n // Calculate average speed\n let averageSpeed = 0;\n if (speedSamplesRef.current.length >= 2) {\n const first = speedSamplesRef.current[0];\n const last =\n speedSamplesRef.current[speedSamplesRef.current.length - 1];\n if (first && last) {\n const totalTime = (last.time - first.time) / 1000; // Convert to seconds\n const totalBytes = last.bytes - first.bytes;\n averageSpeed = totalTime > 0 ? totalBytes / totalTime : 0;\n }\n }\n\n return { currentSpeed, averageSpeed };\n },\n [speedSampleSize],\n );\n\n const updateMetrics = useCallback(() => {\n const now = Date.now();\n\n // Calculate totals from file metrics\n const totalBytes = fileMetrics.reduce((sum, file) => sum + file.size, 0);\n const totalBytesUploaded = fileMetrics.reduce(\n (sum, file) => sum + file.bytesUploaded,\n 0,\n );\n const completedFiles = fileMetrics.filter((file) => file.isComplete).length;\n const activeUploads = fileMetrics.filter(\n (file) => !file.isComplete && file.bytesUploaded > 0,\n ).length;\n\n // Calculate speeds\n const { currentSpeed, averageSpeed } = calculateSpeed(\n now,\n totalBytesUploaded,\n );\n\n // Calculate progress\n const progress =\n totalBytes > 0 ? Math.round((totalBytesUploaded / totalBytes) * 100) : 0;\n\n // Calculate estimated time remaining\n let estimatedTimeRemaining: number | null = null;\n if (currentSpeed > 0) {\n const remainingBytes = totalBytes - totalBytesUploaded;\n estimatedTimeRemaining = (remainingBytes / currentSpeed) * 1000; // Convert to milliseconds\n }\n\n // Find start and end times\n const activeTimes = fileMetrics.filter((file) => file.startTime > 0);\n const startTime =\n activeTimes.length > 0\n ? Math.min(...activeTimes.map((file) => file.startTime))\n : null;\n\n const completedTimes = fileMetrics.filter((file) => file.endTime !== null);\n const endTime =\n completedTimes.length > 0 && completedFiles === fileMetrics.length\n ? Math.max(\n ...completedTimes\n .map((file) => file.endTime)\n .filter((time) => time !== null),\n )\n : null;\n\n const totalDuration = startTime && endTime ? endTime - startTime : null;\n\n const newMetrics: UploadMetrics = {\n totalBytesUploaded,\n totalBytes,\n averageSpeed,\n currentSpeed,\n estimatedTimeRemaining,\n totalFiles: fileMetrics.length,\n completedFiles,\n activeUploads,\n progress,\n peakSpeed: Math.max(metrics.peakSpeed, currentSpeed),\n startTime,\n endTime,\n totalDuration,\n insights: uploadClient.client.getChunkingInsights(),\n sessionMetrics: [uploadClient.client.exportMetrics().session],\n chunkMetrics: uploadClient.client.exportMetrics().chunks,\n };\n\n setMetrics(newMetrics);\n onMetricsUpdate?.(newMetrics);\n }, [\n fileMetrics,\n metrics.peakSpeed,\n calculateSpeed,\n onMetricsUpdate,\n uploadClient.client,\n ]);\n\n // Set up periodic speed calculations\n const setupSpeedCalculation = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n }\n\n intervalRef.current = setInterval(() => {\n if (\n fileMetrics.some((file) => !file.isComplete && file.bytesUploaded > 0)\n ) {\n updateMetrics();\n }\n }, speedCalculationInterval);\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n }, [speedCalculationInterval, updateMetrics, fileMetrics]);\n\n const startFileUpload = useCallback(\n (id: string, filename: string, size: number) => {\n const now = Date.now();\n\n const fileMetric: FileUploadMetrics = {\n id,\n filename,\n size,\n bytesUploaded: 0,\n progress: 0,\n speed: 0,\n startTime: now,\n endTime: null,\n duration: null,\n isComplete: false,\n };\n\n setFileMetrics((prev) => {\n const existing = prev.find((file) => file.id === id);\n if (existing) {\n return prev.map((file) => (file.id === id ? fileMetric : file));\n }\n return [...prev, fileMetric];\n });\n\n onFileStart?.(fileMetric);\n\n // Start speed calculation if this is the first active upload\n if (fileMetrics.filter((file) => !file.isComplete).length === 0) {\n setupSpeedCalculation();\n }\n },\n [fileMetrics, onFileStart, setupSpeedCalculation],\n );\n\n const updateFileProgress = useCallback(\n (id: string, bytesUploaded: number) => {\n const now = Date.now();\n\n setFileMetrics((prev) =>\n prev.map((file) => {\n if (file.id !== id) return file;\n\n const timeDiff = (now - file.startTime) / 1000; // seconds\n const speed = timeDiff > 0 ? bytesUploaded / timeDiff : 0;\n const progress =\n file.size > 0 ? Math.round((bytesUploaded / file.size) * 100) : 0;\n\n const updatedFile = {\n ...file,\n bytesUploaded,\n progress,\n speed,\n };\n\n onFileProgress?.(updatedFile);\n return updatedFile;\n }),\n );\n\n // Trigger metrics update\n setTimeout(updateMetrics, 0);\n },\n [onFileProgress, updateMetrics],\n );\n\n const completeFileUpload = useCallback(\n (id: string) => {\n const now = Date.now();\n\n setFileMetrics((prev) =>\n prev.map((file) => {\n if (file.id !== id) return file;\n\n const duration = now - file.startTime;\n const speed = duration > 0 ? (file.size / duration) * 1000 : 0; // bytes per second\n\n const completedFile = {\n ...file,\n bytesUploaded: file.size,\n progress: 100,\n speed,\n endTime: now,\n duration,\n isComplete: true,\n };\n\n onFileComplete?.(completedFile);\n return completedFile;\n }),\n );\n\n // Trigger metrics update\n setTimeout(updateMetrics, 0);\n },\n [onFileComplete, updateMetrics],\n );\n\n const removeFile = useCallback(\n (id: string) => {\n setFileMetrics((prev) => prev.filter((file) => file.id !== id));\n setTimeout(updateMetrics, 0);\n },\n [updateMetrics],\n );\n\n const reset = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n\n setMetrics(initialMetrics);\n setFileMetrics([]);\n speedSamplesRef.current = [];\n lastUpdateRef.current = 0;\n }, []);\n\n const getFileMetrics = useCallback(\n (id: string) => {\n return fileMetrics.find((file) => file.id === id);\n },\n [fileMetrics],\n );\n\n const exportMetrics = useCallback(() => {\n return {\n overall: metrics,\n files: fileMetrics,\n exportTime: Date.now(),\n };\n }, [metrics, fileMetrics]);\n\n // Cleanup on unmount\n React.useEffect(() => {\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n }\n };\n }, []);\n\n return {\n metrics,\n fileMetrics,\n startFileUpload,\n updateFileProgress,\n completeFileUpload,\n removeFile,\n reset,\n getFileMetrics,\n exportMetrics,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-upload-metrics-dMP4qxq0.mjs","names":["initialMetrics: UploadMetrics","estimatedTimeRemaining: number | null","newMetrics: UploadMetrics","fileMetric: FileUploadMetrics"],"sources":["../src/hooks/use-upload-metrics.ts"],"sourcesContent":["import type {\n ChunkMetrics,\n PerformanceInsights,\n UploadSessionMetrics,\n} from \"@uploadista/client-core\";\nimport React, { useCallback, useRef, useState } from \"react\";\nimport { useUploadistaContext } from \"../components/uploadista-provider\";\n\nexport type Timeout = ReturnType<typeof setInterval>;\n\nexport interface UploadMetrics {\n /**\n * Total bytes uploaded across all files\n */\n totalBytesUploaded: number;\n\n /**\n * Total bytes to upload across all files\n */\n totalBytes: number;\n\n /**\n * Overall upload speed in bytes per second\n */\n averageSpeed: number;\n\n /**\n * Current upload speed in bytes per second\n */\n currentSpeed: number;\n\n /**\n * Estimated time remaining in milliseconds\n */\n estimatedTimeRemaining: number | null;\n\n /**\n * Total number of files being tracked\n */\n totalFiles: number;\n\n /**\n * Number of files completed\n */\n completedFiles: number;\n\n /**\n * Number of files currently uploading\n */\n activeUploads: number;\n\n /**\n * Overall progress as percentage (0-100)\n */\n progress: number;\n\n /**\n * Peak upload speed achieved\n */\n peakSpeed: number;\n\n /**\n * Start time of the first upload\n */\n startTime: number | null;\n\n /**\n * End time of the last completed upload\n */\n endTime: number | null;\n\n /**\n * Total duration of all uploads\n */\n totalDuration: number | null;\n\n /**\n * Detailed performance insights from the upload client\n */\n insights: PerformanceInsights;\n\n /**\n * Session metrics for completed uploads\n */\n sessionMetrics: Partial<UploadSessionMetrics>[];\n\n /**\n * Detailed chunk metrics from recent uploads\n */\n chunkMetrics: ChunkMetrics[];\n}\n\nexport interface FileUploadMetrics {\n id: string;\n filename: string;\n size: number;\n bytesUploaded: number;\n progress: number;\n speed: number;\n startTime: number;\n endTime: number | null;\n duration: number | null;\n isComplete: boolean;\n}\n\nexport interface UseUploadMetricsOptions {\n /**\n * Interval for calculating current speed (in milliseconds)\n */\n speedCalculationInterval?: number;\n\n /**\n * Number of speed samples to keep for average calculation\n */\n speedSampleSize?: number;\n\n /**\n * Called when metrics are updated\n */\n onMetricsUpdate?: (metrics: UploadMetrics) => void;\n\n /**\n * Called when a file upload starts\n */\n onFileStart?: (fileMetrics: FileUploadMetrics) => void;\n\n /**\n * Called when a file upload progresses\n */\n onFileProgress?: (fileMetrics: FileUploadMetrics) => void;\n\n /**\n * Called when a file upload completes\n */\n onFileComplete?: (fileMetrics: FileUploadMetrics) => void;\n}\n\nexport interface UseUploadMetricsReturn {\n /**\n * Current overall metrics\n */\n metrics: UploadMetrics;\n\n /**\n * Individual file metrics\n */\n fileMetrics: FileUploadMetrics[];\n\n /**\n * Start tracking a new file upload\n */\n startFileUpload: (id: string, filename: string, size: number) => void;\n\n /**\n * Update progress for a file upload\n */\n updateFileProgress: (id: string, bytesUploaded: number) => void;\n\n /**\n * Mark a file upload as complete\n */\n completeFileUpload: (id: string) => void;\n\n /**\n * Remove a file from tracking\n */\n removeFile: (id: string) => void;\n\n /**\n * Reset all metrics\n */\n reset: () => void;\n\n /**\n * Get metrics for a specific file\n */\n getFileMetrics: (id: string) => FileUploadMetrics | undefined;\n\n /**\n * Export metrics as JSON\n */\n exportMetrics: () => {\n overall: UploadMetrics;\n files: FileUploadMetrics[];\n exportTime: number;\n };\n}\n\nconst initialMetrics: UploadMetrics = {\n totalBytesUploaded: 0,\n totalBytes: 0,\n averageSpeed: 0,\n currentSpeed: 0,\n estimatedTimeRemaining: null,\n totalFiles: 0,\n completedFiles: 0,\n activeUploads: 0,\n progress: 0,\n peakSpeed: 0,\n startTime: null,\n endTime: null,\n totalDuration: null,\n insights: {\n overallEfficiency: 0,\n chunkingEffectiveness: 0,\n networkStability: 0,\n recommendations: [],\n optimalChunkSizeRange: { min: 256 * 1024, max: 2 * 1024 * 1024 },\n },\n sessionMetrics: [],\n chunkMetrics: [],\n};\n\n/**\n * React hook for tracking detailed upload metrics and performance statistics.\n * Provides comprehensive monitoring of upload progress, speed, and timing data.\n *\n * @param options - Configuration and event handlers\n * @returns Upload metrics state and control methods\n *\n * @example\n * ```tsx\n * const uploadMetrics = useUploadMetrics({\n * speedCalculationInterval: 1000, // Update speed every second\n * speedSampleSize: 10, // Keep last 10 speed samples for average\n * onMetricsUpdate: (metrics) => {\n * console.log(`Overall progress: ${metrics.progress}%`);\n * console.log(`Speed: ${(metrics.currentSpeed / 1024).toFixed(1)} KB/s`);\n * console.log(`ETA: ${metrics.estimatedTimeRemaining}ms`);\n * },\n * onFileComplete: (fileMetrics) => {\n * console.log(`${fileMetrics.filename} completed in ${fileMetrics.duration}ms`);\n * },\n * });\n *\n * // Start tracking a file\n * const handleFileStart = (file: File) => {\n * uploadMetrics.startFileUpload(file.name, file.name, file.size);\n * };\n *\n * // Update progress during upload\n * const handleProgress = (fileId: string, bytesUploaded: number) => {\n * uploadMetrics.updateFileProgress(fileId, bytesUploaded);\n * };\n *\n * // Display metrics\n * return (\n * <div>\n * <div>Overall Progress: {uploadMetrics.metrics.progress}%</div>\n * <div>Speed: {(uploadMetrics.metrics.currentSpeed / 1024).toFixed(1)} KB/s</div>\n * <div>Files: {uploadMetrics.metrics.completedFiles}/{uploadMetrics.metrics.totalFiles}</div>\n *\n * {uploadMetrics.metrics.estimatedTimeRemaining && (\n * <div>ETA: {Math.round(uploadMetrics.metrics.estimatedTimeRemaining / 1000)}s</div>\n * )}\n *\n * {uploadMetrics.fileMetrics.map((file) => (\n * <div key={file.id}>\n * {file.filename}: {file.progress}% ({(file.speed / 1024).toFixed(1)} KB/s)\n * </div>\n * ))}\n * </div>\n * );\n * ```\n */\nexport function useUploadMetrics(\n options: UseUploadMetricsOptions = {},\n): UseUploadMetricsReturn {\n const {\n speedCalculationInterval = 1000,\n speedSampleSize = 10,\n onMetricsUpdate,\n onFileStart,\n onFileProgress,\n onFileComplete,\n } = options;\n\n const uploadClient = useUploadistaContext();\n\n const [metrics, setMetrics] = useState<UploadMetrics>(initialMetrics);\n const [fileMetrics, setFileMetrics] = useState<FileUploadMetrics[]>([]);\n\n const speedSamplesRef = useRef<Array<{ time: number; bytes: number }>>([]);\n const lastUpdateRef = useRef<number>(0);\n const intervalRef = useRef<Timeout | null>(null);\n\n const calculateSpeed = useCallback(\n (currentTime: number, totalBytesUploaded: number) => {\n const sample = { time: currentTime, bytes: totalBytesUploaded };\n speedSamplesRef.current.push(sample);\n\n // Keep only recent samples\n if (speedSamplesRef.current.length > speedSampleSize) {\n speedSamplesRef.current = speedSamplesRef.current.slice(\n -speedSampleSize,\n );\n }\n\n // Calculate current speed (bytes per second)\n let currentSpeed = 0;\n if (speedSamplesRef.current.length >= 2) {\n const recent =\n speedSamplesRef.current[speedSamplesRef.current.length - 1];\n const previous =\n speedSamplesRef.current[speedSamplesRef.current.length - 2];\n if (recent && previous) {\n const timeDiff = (recent.time - previous.time) / 1000; // Convert to seconds\n const bytesDiff = recent.bytes - previous.bytes;\n currentSpeed = timeDiff > 0 ? bytesDiff / timeDiff : 0;\n }\n }\n\n // Calculate average speed\n let averageSpeed = 0;\n if (speedSamplesRef.current.length >= 2) {\n const first = speedSamplesRef.current[0];\n const last =\n speedSamplesRef.current[speedSamplesRef.current.length - 1];\n if (first && last) {\n const totalTime = (last.time - first.time) / 1000; // Convert to seconds\n const totalBytes = last.bytes - first.bytes;\n averageSpeed = totalTime > 0 ? totalBytes / totalTime : 0;\n }\n }\n\n return { currentSpeed, averageSpeed };\n },\n [speedSampleSize],\n );\n\n const updateMetrics = useCallback(() => {\n const now = Date.now();\n\n // Calculate totals from file metrics\n const totalBytes = fileMetrics.reduce((sum, file) => sum + file.size, 0);\n const totalBytesUploaded = fileMetrics.reduce(\n (sum, file) => sum + file.bytesUploaded,\n 0,\n );\n const completedFiles = fileMetrics.filter((file) => file.isComplete).length;\n const activeUploads = fileMetrics.filter(\n (file) => !file.isComplete && file.bytesUploaded > 0,\n ).length;\n\n // Calculate speeds\n const { currentSpeed, averageSpeed } = calculateSpeed(\n now,\n totalBytesUploaded,\n );\n\n // Calculate progress\n const progress =\n totalBytes > 0 ? Math.round((totalBytesUploaded / totalBytes) * 100) : 0;\n\n // Calculate estimated time remaining\n let estimatedTimeRemaining: number | null = null;\n if (currentSpeed > 0) {\n const remainingBytes = totalBytes - totalBytesUploaded;\n estimatedTimeRemaining = (remainingBytes / currentSpeed) * 1000; // Convert to milliseconds\n }\n\n // Find start and end times\n const activeTimes = fileMetrics.filter((file) => file.startTime > 0);\n const startTime =\n activeTimes.length > 0\n ? Math.min(...activeTimes.map((file) => file.startTime))\n : null;\n\n const completedTimes = fileMetrics.filter((file) => file.endTime !== null);\n const endTime =\n completedTimes.length > 0 && completedFiles === fileMetrics.length\n ? Math.max(\n ...completedTimes\n .map((file) => file.endTime)\n .filter((time) => time !== null),\n )\n : null;\n\n const totalDuration = startTime && endTime ? endTime - startTime : null;\n\n const newMetrics: UploadMetrics = {\n totalBytesUploaded,\n totalBytes,\n averageSpeed,\n currentSpeed,\n estimatedTimeRemaining,\n totalFiles: fileMetrics.length,\n completedFiles,\n activeUploads,\n progress,\n peakSpeed: Math.max(metrics.peakSpeed, currentSpeed),\n startTime,\n endTime,\n totalDuration,\n insights: uploadClient.client.getChunkingInsights(),\n sessionMetrics: [uploadClient.client.exportMetrics().session],\n chunkMetrics: uploadClient.client.exportMetrics().chunks,\n };\n\n setMetrics(newMetrics);\n onMetricsUpdate?.(newMetrics);\n }, [\n fileMetrics,\n metrics.peakSpeed,\n calculateSpeed,\n onMetricsUpdate,\n uploadClient.client,\n ]);\n\n // Set up periodic speed calculations\n const setupSpeedCalculation = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n }\n\n intervalRef.current = setInterval(() => {\n if (\n fileMetrics.some((file) => !file.isComplete && file.bytesUploaded > 0)\n ) {\n updateMetrics();\n }\n }, speedCalculationInterval);\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n }, [speedCalculationInterval, updateMetrics, fileMetrics]);\n\n const startFileUpload = useCallback(\n (id: string, filename: string, size: number) => {\n const now = Date.now();\n\n const fileMetric: FileUploadMetrics = {\n id,\n filename,\n size,\n bytesUploaded: 0,\n progress: 0,\n speed: 0,\n startTime: now,\n endTime: null,\n duration: null,\n isComplete: false,\n };\n\n setFileMetrics((prev) => {\n const existing = prev.find((file) => file.id === id);\n if (existing) {\n return prev.map((file) => (file.id === id ? fileMetric : file));\n }\n return [...prev, fileMetric];\n });\n\n onFileStart?.(fileMetric);\n\n // Start speed calculation if this is the first active upload\n if (fileMetrics.filter((file) => !file.isComplete).length === 0) {\n setupSpeedCalculation();\n }\n },\n [fileMetrics, onFileStart, setupSpeedCalculation],\n );\n\n const updateFileProgress = useCallback(\n (id: string, bytesUploaded: number) => {\n const now = Date.now();\n\n setFileMetrics((prev) =>\n prev.map((file) => {\n if (file.id !== id) return file;\n\n const timeDiff = (now - file.startTime) / 1000; // seconds\n const speed = timeDiff > 0 ? bytesUploaded / timeDiff : 0;\n const progress =\n file.size > 0 ? Math.round((bytesUploaded / file.size) * 100) : 0;\n\n const updatedFile = {\n ...file,\n bytesUploaded,\n progress,\n speed,\n };\n\n onFileProgress?.(updatedFile);\n return updatedFile;\n }),\n );\n\n // Trigger metrics update\n setTimeout(updateMetrics, 0);\n },\n [onFileProgress, updateMetrics],\n );\n\n const completeFileUpload = useCallback(\n (id: string) => {\n const now = Date.now();\n\n setFileMetrics((prev) =>\n prev.map((file) => {\n if (file.id !== id) return file;\n\n const duration = now - file.startTime;\n const speed = duration > 0 ? (file.size / duration) * 1000 : 0; // bytes per second\n\n const completedFile = {\n ...file,\n bytesUploaded: file.size,\n progress: 100,\n speed,\n endTime: now,\n duration,\n isComplete: true,\n };\n\n onFileComplete?.(completedFile);\n return completedFile;\n }),\n );\n\n // Trigger metrics update\n setTimeout(updateMetrics, 0);\n },\n [onFileComplete, updateMetrics],\n );\n\n const removeFile = useCallback(\n (id: string) => {\n setFileMetrics((prev) => prev.filter((file) => file.id !== id));\n setTimeout(updateMetrics, 0);\n },\n [updateMetrics],\n );\n\n const reset = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n\n setMetrics(initialMetrics);\n setFileMetrics([]);\n speedSamplesRef.current = [];\n lastUpdateRef.current = 0;\n }, []);\n\n const getFileMetrics = useCallback(\n (id: string) => {\n return fileMetrics.find((file) => file.id === id);\n },\n [fileMetrics],\n );\n\n const exportMetrics = useCallback(() => {\n return {\n overall: metrics,\n files: fileMetrics,\n exportTime: Date.now(),\n };\n }, [metrics, fileMetrics]);\n\n // Cleanup on unmount\n React.useEffect(() => {\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n }\n };\n }, []);\n\n return {\n metrics,\n fileMetrics,\n startFileUpload,\n updateFileProgress,\n completeFileUpload,\n removeFile,\n reset,\n getFileMetrics,\n exportMetrics,\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15-beta.1",
|
|
5
5
|
"description": "React client for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"react": "19.2.0",
|
|
24
24
|
"react-dom": "19.2.0",
|
|
25
|
-
"@uploadista/core": "0.0.
|
|
26
|
-
"@uploadista/client-core": "0.0.
|
|
27
|
-
"@uploadista/client-browser": "0.0.
|
|
25
|
+
"@uploadista/core": "0.0.15-beta.1",
|
|
26
|
+
"@uploadista/client-core": "0.0.15-beta.1",
|
|
27
|
+
"@uploadista/client-browser": "0.0.15-beta.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/react": "19.2.4",
|
|
31
31
|
"@types/react-dom": "19.2.3",
|
|
32
32
|
"tsdown": "0.16.4",
|
|
33
33
|
"vitest": "4.0.8",
|
|
34
|
-
"@uploadista/typescript-config": "0.0.
|
|
34
|
+
"@uploadista/typescript-config": "0.0.15-beta.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsdown",
|
|
@@ -260,6 +260,7 @@ export function useFlowUpload<TOutput = UploadFile>(
|
|
|
260
260
|
const unsubscribe = client.subscribeToEvents((event: UploadistaEvent) => {
|
|
261
261
|
// Handle flow events
|
|
262
262
|
if (isFlowEvent(event)) {
|
|
263
|
+
console.log("[useFlowUpload] Flow event received:", event.eventType, "jobId:", event.jobId, "current:", managerRef.current?.getJobId());
|
|
263
264
|
managerRef.current?.handleFlowEvent(event);
|
|
264
265
|
return;
|
|
265
266
|
}
|