@blakesteve/roster 5.15.0 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- package/dist/components/molecules/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/molecules/FileUpload/FileUpload.d.ts +20 -3
- package/dist/components/molecules/FileUpload/FileUpload.js +266 -216
- package/dist/components/molecules/FileUpload/file-upload-rejection.cjs +1 -0
- package/dist/components/molecules/FileUpload/file-upload-rejection.d.ts +24 -0
- package/dist/components/molecules/FileUpload/file-upload-rejection.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/meta.json +3 -3
- package/dist/roster.cjs +1 -1
- package/dist/roster.css +1 -1
- package/dist/roster.js +130 -128
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1428,6 +1428,8 @@ import { FileUpload } from "@blakesteve/roster";
|
|
|
1428
1428
|
- **Resolve** with what the form needs later, an id say; it comes back as that
|
|
1429
1429
|
file's `result` in `onChange`. **Reject** with an Error whose message says
|
|
1430
1430
|
why; it's shown, with Try again.
|
|
1431
|
+
- **Reject with a `FileUploadRejection`** when the file can never be added (see
|
|
1432
|
+
below): it's turned away rather than failed, with no Try again.
|
|
1431
1433
|
|
|
1432
1434
|
Each file is `queued`, `uploading`, `processing`, `done`, `failed` or
|
|
1433
1435
|
`rejected`. A few upload at once (`concurrency`, 3 by default) and the rest
|
|
@@ -1441,6 +1443,56 @@ turned away stays in the list with its reason and is never sent. A file that
|
|
|
1441
1443
|
arrives with no type, as some cloud-backed picks on Android do, is judged by
|
|
1442
1444
|
its extension.
|
|
1443
1445
|
|
|
1446
|
+
**Turned away by the app.** Some files can only be judged once they're sent:
|
|
1447
|
+
the server can't decode one, says it isn't a photo, or refuses its type.
|
|
1448
|
+
Retrying those only repeats the answer, so `upload` can say the file will never
|
|
1449
|
+
go up by rejecting with a `FileUploadRejection`. The file then ends `rejected`,
|
|
1450
|
+
as a file the rules turn away does: "Not added." with its message as the
|
|
1451
|
+
reason, no Try again, announced as not added. It stops counting toward
|
|
1452
|
+
`maxFiles` and toward "2 of 3 uploaded", its bar goes, and its place goes to
|
|
1453
|
+
the next file waiting. A plain `Error` still means it failed, with Try again.
|
|
1454
|
+
|
|
1455
|
+
```tsx
|
|
1456
|
+
import { FileUpload, FileUploadRejection } from "@blakesteve/roster";
|
|
1457
|
+
|
|
1458
|
+
upload={async (file, { signal }) => {
|
|
1459
|
+
const response = await fetch("/photos", { method: "POST", body: file, signal });
|
|
1460
|
+
if (response.status === 415) throw new FileUploadRejection("That isn't a photo.");
|
|
1461
|
+
if (!response.ok) throw new Error("The server is busy.");
|
|
1462
|
+
return (await response.json()).id;
|
|
1463
|
+
}}
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
It's recognized by a mark as well as by `instanceof`, so one made by another
|
|
1467
|
+
copy of the library on the page still counts.
|
|
1468
|
+
|
|
1469
|
+
**A picture in the drop box.** For one picture, an avatar or a cover,
|
|
1470
|
+
`preview="dropzone"` shows it big, filling the drop box, rather than as the
|
|
1471
|
+
row's thumbnail. It takes `maxFiles={1}`; with any other `maxFiles` the
|
|
1472
|
+
pictures stay on the rows, and a development build says so.
|
|
1473
|
+
|
|
1474
|
+
```tsx
|
|
1475
|
+
<FileUpload label="Cover photo" maxFiles={1} preview="dropzone" upload={uploadCover} />
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
- **The row keeps everything else:** the name, the status, the progress,
|
|
1479
|
+
Remove and Try again.
|
|
1480
|
+
- **The picture is contained,** uncropped, as it will be sent, 12px inside the
|
|
1481
|
+
dashed edge, so the edge never sits on the photo. A file turned away shows
|
|
1482
|
+
faded, as its thumbnail did; once removed, the box is empty again.
|
|
1483
|
+
- **The box keeps its height,** with a picture or without, so nothing below it
|
|
1484
|
+
moves when one arrives: 13rem, or `--roster-file-preview-height`.
|
|
1485
|
+
- **The picture takes no input.** A click or a drop on it lands on the box, as
|
|
1486
|
+
with no picture, and the button over it keeps its 44px target and its focus
|
|
1487
|
+
ring, painted above the photo.
|
|
1488
|
+
- **Replacing it:** the button over the picture reads Replace photo
|
|
1489
|
+
(`strings.replace`) and opens the picker, and the box is still a drop target.
|
|
1490
|
+
A file that passes the rules, picked or dropped, takes the place of the one
|
|
1491
|
+
there, and that one's upload stops. A file the rules turn away leaves it as
|
|
1492
|
+
it was.
|
|
1493
|
+
- **Its object URL is revoked** when the file is removed or replaced, and when
|
|
1494
|
+
the field unmounts.
|
|
1495
|
+
|
|
1444
1496
|
**Phones first.** `accept` defaults to `"image/*"`, with `multiple` and no
|
|
1445
1497
|
`capture`. That combination is what opens Android's system photo picker, with
|
|
1446
1498
|
albums and cloud photos; a list of exact types or an extension gets a file
|
|
@@ -2270,7 +2322,7 @@ function App() {
|
|
|
2270
2322
|
| `MatchupCard` | Head-to-head comparison card |
|
|
2271
2323
|
| `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
|
|
2272
2324
|
| `Menu` | A button's menu of actions: icons, shortcuts, checkbox and radio items, links, groups, submenus; the full keyboard pattern, 44px targets (see [Menu](#menu)) |
|
|
2273
|
-
| `FileUpload` | Picks files and uploads each through the app's `upload`, with per-file progress, retry and rules |
|
|
2325
|
+
| `FileUpload` | Picks files and uploads each through the app's `upload`, with per-file progress, retry and rules; one picture can fill the drop box (see [FileUpload](#fileupload)) |
|
|
2274
2326
|
| `Pagination` | Page numbers with gaps, or a compact page picker on narrow screens; buttons or real links, 44px targets |
|
|
2275
2327
|
| `SkeletonCard`, `SkeletonTableRow`, `SkeletonAvatar`, `SkeletonStat` | Skeleton presets in the real component's box |
|
|
2276
2328
|
| `ProgressField` | Progress with a label and value on one baseline and a detail line: the form to reach for |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react/jsx-runtime"),o=require("react"),p=require("../../../lib/utils.cjs"),pt=require("../../../lib/labels.cjs"),H=require("../../atoms/Button/Button.cjs"),gt=require("../../atoms/Progress/Progress.cjs"),R=require("./file-rules.cjs"),ht=require("../../../internal/key-focus.cjs"),xt={choose:"Choose photos",dropHint:"or drag them here",dropHere:"Drop to add them",queued:"Waiting",uploading:n=>n===null?"Uploading":`Uploading, ${n}%`,processing:"Finishing up",done:"Uploaded",failed:"Didn't upload.",rejected:"Not added.",remove:n=>`Remove ${n}`,retry:n=>`Try ${n} again`,wrongType:()=>"That type of file isn't accepted.",tooLarge:(n,l)=>`It's over ${R.formatBytes(l)}.`,tooMany:n=>`Only ${n} can be added.`,duplicate:()=>"It's already added.",uploadError:n=>n instanceof Error&&n.message?n.message:"Something went wrong.",size:R.formatBytes,added:n=>`${n} ${n===1?"photo":"photos"} added.`,progress:(n,l)=>`${n} of ${l} uploaded.`,failedNotice:(n,l)=>`${n} didn't upload. ${l}`,rejectedNotice:(n,l)=>`${n} wasn't added. ${l}`,removed:n=>`${n} removed.`};function mt({file:n,status:l}){const[q,L]=o.useState(!1),B=!q&&(n.type.startsWith("image/")||/\.(jpe?g|png|gif|webp|avif|heic|heif)$/i.test(n.name)),N=o.useCallback(M=>{if(!M)return;const w=URL.createObjectURL(n);return M.src=w,()=>URL.revokeObjectURL(w)},[n]),x="rst:h-14 rst:w-14 rst:shrink-0 rst:rounded-md rst:border rst:border-[var(--roster-card-border)]";return B?r.jsx("img",{ref:N,alt:"",onError:()=>L(!0),className:p.cn(x,"rst:object-cover",l==="rejected"&&"rst:opacity-50"),"data-file-thumb":"image"}):r.jsx("span",{"aria-hidden":"true",className:p.cn(x,"rst:flex rst:items-center rst:justify-center rst:text-gray-500 rst:dark:text-gray-400"),"data-file-thumb":"placeholder",children:r.jsxs("svg",{viewBox:"0 0 24 24",width:"24",height:"24",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("rect",{x:"3",y:"5",width:"18",height:"14",rx:"2"}),r.jsx("circle",{cx:"9",cy:"10",r:"1.6"}),r.jsx("path",{d:"m21 15-4.2-4.2a1.6 1.6 0 0 0-2.3 0L7 18"})]})})}const W="rst:cursor-not-allowed rst:opacity-50",S={width:20,height:20,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};function jt({label:n,upload:l,onChange:q,accept:L="image/*",types:B,maxSize:N,maxFiles:x,validate:M,concurrency:w=3,helperText:E,errorMessage:T,disabled:i=!1,strings:Q,className:X}){ht.useKeyFocus();const c=pt.withLabels(xt,Q),D=o.useId(),F=`${D}-label`,G=`${D}-help`,K=`${D}-error`,[I,Y]=o.useState([]),d=o.useRef(I),Z=o.useRef(0),m=o.useRef(new Map),U=o.useRef(null),V=o.useRef(null),g=o.useRef({upload:l,onChange:q,concurrency:w,t:c,pump:()=>{}}),[z,tt]=o.useState({text:"",n:0}),j=o.useRef([]),y=o.useRef(null),$=o.useCallback((t,s=!1)=>{s&&(j.current=j.current.filter(e=>!e.count)),j.current.push({text:t,count:s}),!y.current&&(y.current=setTimeout(()=>{y.current=null;const e=j.current.map(u=>u.text).join(" ");j.current=[],tt(u=>({text:e,n:u.n+1}))},0))},[]),O=o.useRef(null),[et,rt]=o.useState(0),_=t=>{O.current=t,rt(s=>s+1)};o.useLayoutEffect(()=>{const t=O.current;if(!t)return;O.current=null,(U.current?.querySelector(t)??U.current?.querySelector("[data-file-choose]"))?.focus()},[et]);const v=o.useCallback(t=>{d.current=t,Y(t),g.current.onChange?.(t)},[]),b=o.useCallback((t,s)=>{d.current.some(e=>e.id===t)&&v(d.current.map(e=>e.id===t?{...e,...s}:e))},[v]);function k(){const{concurrency:t}=g.current,s=d.current.filter(u=>u.status==="uploading"||u.status==="processing").length,e=d.current.filter(u=>u.status==="queued").slice(0,Math.max(0,Math.max(1,t)-s));for(const u of e)st(u)}function st(t){const s=new AbortController;m.current.set(t.id,s),b(t.id,{status:"uploading",progress:null,reason:void 0});const e=()=>!s.signal.aborted&&m.current.get(t.id)===s,u={onProgress:a=>{if(!e())return;const f=Math.round(Math.min(1,Math.max(0,Number.isFinite(a)?a:0))*100)/100,h=d.current.find(ft=>ft.id===t.id);h?.status==="uploading"&&h.progress!==f&&b(t.id,{progress:f})},onProcessing:()=>{e()&&b(t.id,{status:"processing"})},signal:s.signal};new Promise(a=>a(g.current.upload(t.file,u))).then(a=>{if(!e())return;m.current.delete(t.id),b(t.id,{status:"done",progress:1,result:a});const f=d.current.filter(h=>h.status!=="rejected");$(g.current.t.progress(f.filter(h=>h.status==="done").length,f.length),!0),k()},a=>{if(!e())return;m.current.delete(t.id);const f=g.current.t.uploadError(a);b(t.id,{status:"failed",reason:f}),$(g.current.t.failedNotice(t.file.name,f)),k()})}o.useLayoutEffect(()=>{g.current={upload:l,onChange:q,concurrency:w,t:c,pump:k}}),o.useEffect(()=>{const t=m.current;return d.current.some(e=>(e.status==="uploading"||e.status==="processing")&&!t.has(e.id))&&(v(d.current.map(e=>(e.status==="uploading"||e.status==="processing")&&!t.has(e.id)?{...e,status:"queued",progress:null}:e)),g.current.pump()),()=>{for(const e of t.values())e.abort();t.clear(),y.current&&clearTimeout(y.current),y.current=null,j.current=[]}},[v]);const nt=B??R.acceptList(L),at=(t,s)=>{if(s.some(u=>R.sameFile(u.file,t)))return c.duplicate(t);const e=M?.(t);if(e)return e;if(!R.matchesType(t,nt))return c.wrongType(t);if(N!==void 0&&t.size>N)return c.tooLarge(t,N);if(x!==void 0&&s.length>=x)return c.tooMany(x)},J=t=>{if(i||t.length===0)return;const s=d.current.filter(a=>a.status!=="rejected"),e=[];for(const a of t){const f=at(a,[...s,...e.filter(h=>h.status!=="rejected")]);e.push({id:`${D}-file-${Z.current++}`,file:a,status:f?"rejected":"queued",progress:null,reason:f})}v([...d.current,...e]);const u=e.filter(a=>a.status==="queued").length;u&&$(c.added(u));for(const a of e)a.reason&&$(c.rejectedNotice(a.file.name,a.reason));k()},ot=t=>{if(i)return;const s=d.current,e=s.findIndex(a=>a.id===t);if(e<0)return;m.current.get(t)?.abort(),m.current.delete(t);const u=s[e+1]??s[e-1];v(s.filter(a=>a.id!==t)),$(c.removed(s[e].file.name)),_(u?`[data-file-remove="${u.id}"]`:"[data-file-choose]"),k()},ct=t=>{i||(b(t,{status:"queued",progress:null,reason:void 0}),_(`[data-file-remove="${t}"]`),k())},[ut,P]=o.useState(0),C=t=>Array.from(t.dataTransfer?.types??[]).includes("Files"),it={onDragEnter:t=>{C(t)&&(t.preventDefault(),P(s=>s+1))},onDragOver:t=>{C(t)&&(t.preventDefault(),t.dataTransfer.dropEffect=i?"none":"copy")},onDragLeave:t=>{C(t)&&P(s=>Math.max(0,s-1))},onDrop:t=>{C(t)&&(t.preventDefault(),P(0),J(Array.from(t.dataTransfer.files)))}},A=ut>0&&!i,lt=t=>{switch(t.status){case"queued":return c.queued;case"uploading":return c.uploading(t.progress===null?null:Math.round(t.progress*100));case"processing":return c.processing;case"done":return c.done;case"failed":return`${c.failed} ${t.reason??""}`.trim();case"rejected":return`${c.rejected} ${t.reason??""}`.trim()}},dt=[E?G:null,T?K:null].filter(Boolean).join(" ")||void 0;return r.jsxs("div",{ref:U,role:"group","aria-labelledby":F,className:p.cn("rst:font-ui rst:w-full",X),"data-file-upload":"",children:[r.jsx("p",{id:F,className:p.cn("rst:mb-1.5 rst:text-left rst:text-sm rst:font-medium rst:leading-none rst:text-[var(--roster-control-text)]",i&&"rst:opacity-70"),children:n}),r.jsxs("div",{...it,"data-file-drop":"","data-dragging":A?"":void 0,className:p.cn("rst:flex rst:flex-col rst:items-center rst:justify-center rst:gap-2 rst:rounded-lg rst:border-2 rst:border-dashed rst:px-4 rst:py-6 rst:text-center rst:transition-colors rst:motion-reduce:transition-none",A?"rst:border-primary-500 rst:bg-primary-500/10":T?"rst:border-error-500 rst:dark:border-error-400":"rst:border-gray-300 rst:dark:border-gray-600"),children:[r.jsx(H.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"lg",className:p.cn("rst:px-5",i&&W),"aria-disabled":i||void 0,"aria-describedby":dt,onClick:()=>V.current?.click(),"data-file-choose":"",startIcon:r.jsx("svg",{...S,"aria-hidden":"true",children:r.jsx("path",{d:"M12 16V4M7 9l5-5 5 5M5 20h14"})}),children:c.choose}),r.jsx("p",{className:"rst:hidden rst:text-xs rst:text-gray-600 rst:pointer-fine:block rst:dark:text-gray-400","data-file-drop-hint":"",children:A?c.dropHere:c.dropHint}),r.jsx("input",{ref:V,type:"file",hidden:!0,tabIndex:-1,accept:L,multiple:x!==1,disabled:i,onChange:t=>{J(Array.from(t.currentTarget.files??[])),t.currentTarget.value=""},"data-file-input":""})]}),E&&r.jsx("p",{id:G,className:"rst:mt-1.5 rst:text-left rst:text-xs rst:text-gray-500 rst:dark:text-gray-400",children:E}),T&&r.jsx("p",{id:K,className:"rst:mt-1.5 rst:text-left rst:text-xs rst:font-medium rst:text-error-600 rst:dark:text-error-400",children:T}),I.length>0&&r.jsx("ul",{className:"rst:mt-3 rst:flex rst:flex-col rst:gap-2","aria-labelledby":F,children:I.map(t=>{const s=t.status==="failed"||t.status==="rejected",e=t.status==="uploading"||t.status==="processing"||t.status==="queued";return r.jsxs("li",{"data-file-item":t.id,"data-status":t.status,className:"rst:flex rst:items-center rst:gap-3 rst:rounded-md rst:border rst:border-[var(--roster-card-border)] rst:p-2",children:[r.jsx(mt,{file:t.file,status:t.status}),r.jsxs("div",{className:"rst:min-w-0 rst:flex-1 rst:text-left",children:[r.jsx("p",{className:"rst:truncate rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)]",title:t.file.name,children:t.file.name}),r.jsxs("p",{className:p.cn("rst:mt-0.5 rst:flex rst:items-start rst:gap-1 rst:text-xs",s?"rst:text-error-600 rst:dark:text-error-400":"rst:text-gray-600 rst:dark:text-gray-400"),"data-file-status":"",children:[t.status==="done"&&r.jsx("svg",{...S,width:14,height:14,"aria-hidden":"true",className:"rst:mt-px rst:shrink-0 rst:text-success-600 rst:dark:text-success-400",children:r.jsx("path",{d:"M20 6 9 17l-5-5"})}),r.jsxs("span",{children:[r.jsx("span",{className:"rst:tabular-nums",children:c.size(t.file.size)}),r.jsx("span",{"aria-hidden":"true",children:" · "}),lt(t)]})]}),e&&r.jsx("div",{className:"rst:mt-1.5","data-file-progress":"",children:r.jsx(gt.Progress,{size:"sm",value:t.status==="processing"?100:t.status==="uploading"?(t.progress??0)*100:0,busy:t.status==="processing"||t.status==="uploading"&&t.progress===null,announce:!1})})]}),t.status==="failed"&&r.jsx(H.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:p.cn("rst:h-11 rst:w-11",i&&W),"aria-disabled":i||void 0,"aria-label":c.retry(t.file.name),onClick:()=>ct(t.id),"data-file-retry":t.id,children:r.jsxs("svg",{...S,"aria-hidden":"true",children:[r.jsx("path",{d:"M3 12a9 9 0 1 0 3-6.7L3 8"}),r.jsx("path",{d:"M3 3v5h5"})]})}),r.jsx(H.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:p.cn("rst:h-11 rst:w-11",i&&W),"aria-disabled":i||void 0,"aria-label":c.remove(t.file.name),onClick:()=>ot(t.id),"data-file-remove":t.id,children:r.jsx("svg",{...S,"aria-hidden":"true",children:r.jsx("path",{d:"M18 6 6 18M6 6l12 12"})})})]},t.id)})}),r.jsxs("div",{role:"status","aria-live":"polite","aria-atomic":"true",className:"rst:sr-only","data-file-live":"","data-file-live-n":z.n,children:[z.text,z.n%2?" ":""]})]})}exports.FileUpload=jt;
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),i=require("react"),h=require("../../../lib/utils.cjs"),ve=require("../../../lib/labels.cjs"),_=require("../../atoms/Button/Button.cjs"),ye=require("../../atoms/Progress/Progress.cjs"),T=require("./file-rules.cjs"),be=require("./file-upload-rejection.cjs"),ke=require("../../../internal/key-focus.cjs"),we=require("../../../internal/dev.cjs"),Ne={choose:"Choose photos",replace:"Replace photo",dropHint:"or drag them here",dropHere:"Drop to add them",queued:"Waiting",uploading:s=>s===null?"Uploading":`Uploading, ${s}%`,processing:"Finishing up",done:"Uploaded",failed:"Didn't upload.",rejected:"Not added.",remove:s=>`Remove ${s}`,retry:s=>`Try ${s} again`,wrongType:()=>"That type of file isn't accepted.",tooLarge:(s,d)=>`It's over ${T.formatBytes(d)}.`,tooMany:s=>`Only ${s} can be added.`,duplicate:()=>"It's already added.",uploadError:s=>s instanceof Error&&s.message?s.message:"Something went wrong.",size:T.formatBytes,added:s=>`${s} ${s===1?"photo":"photos"} added.`,progress:(s,d)=>`${s} of ${d} uploaded.`,failedNotice:(s,d)=>`${s} didn't upload. ${d}`,rejectedNotice:(s,d)=>`${s} wasn't added. ${d}`,removed:s=>`${s} removed.`};function Z(s){const[d,x]=i.useState(!1),v=!d&&(s.type.startsWith("image/")||/\.(jpe?g|png|gif|webp|avif|heic|heif)$/i.test(s.name)),b=i.useCallback(m=>{if(!m)return;const g=URL.createObjectURL(s);return m.src=g,()=>URL.revokeObjectURL(g)},[s]);return{image:v,attach:b,broke:()=>x(!0)}}const ee=t.jsxs("svg",{viewBox:"0 0 24 24",width:"24",height:"24",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[t.jsx("rect",{x:"3",y:"5",width:"18",height:"14",rx:"2"}),t.jsx("circle",{cx:"9",cy:"10",r:"1.6"}),t.jsx("path",{d:"m21 15-4.2-4.2a1.6 1.6 0 0 0-2.3 0L7 18"})]});function $e({file:s,status:d}){const{image:x,attach:v,broke:b}=Z(s),m="rst:h-14 rst:w-14 rst:shrink-0 rst:rounded-md rst:border rst:border-[var(--roster-card-border)]";return x?t.jsx("img",{ref:v,alt:"",onError:b,className:h.cn(m,"rst:object-cover",d==="rejected"&&"rst:opacity-50"),"data-file-thumb":"image"}):t.jsx("span",{"aria-hidden":"true",className:h.cn(m,"rst:flex rst:items-center rst:justify-center rst:text-gray-500 rst:dark:text-gray-400"),"data-file-thumb":"placeholder",children:ee})}function Re({file:s,status:d}){const{image:x,attach:v,broke:b}=Z(s);return t.jsx("span",{"aria-hidden":"true",className:"rst:pointer-events-none rst:absolute rst:inset-3 rst:flex rst:items-center rst:justify-center","data-file-preview":x?"image":"placeholder",children:x?t.jsx("img",{ref:v,alt:"",onError:b,className:h.cn("rst:h-full rst:w-full rst:rounded-md rst:object-contain",d==="rejected"&&"rst:opacity-50")}):t.jsx("span",{className:"rst:text-gray-500 rst:dark:text-gray-400",children:ee})})}const G="rst:cursor-not-allowed rst:opacity-50",U={width:20,height:20,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};function qe({label:s,upload:d,onChange:x,accept:v="image/*",types:b,maxSize:m,maxFiles:g,validate:te,concurrency:K=3,helperText:z,errorMessage:E,disabled:f=!1,strings:re,className:se,preview:F="row"}){ke.useKeyFocus();const D=F==="dropzone"&&g===1;i.useEffect(()=>{F==="dropzone"&&g!==1&&we.isDevelopment()&&console.warn(`FileUpload: preview="dropzone" shows one file, so it takes maxFiles={1}. With maxFiles=${g}, the pictures stay on the rows.`)},[F,g]);const a=ve.withLabels(Ne,re),S=i.useId(),I=`${S}-label`,V=`${S}-help`,J=`${S}-error`,[C,ne]=i.useState([]),p=i.useRef(C),oe=i.useRef(0),j=i.useRef(new Map),P=i.useRef(null),Q=i.useRef(null),k=i.useRef({upload:d,onChange:x,concurrency:K,t:a,pump:()=>{}}),[O,ae]=i.useState({text:"",n:0}),R=i.useRef([]),q=i.useRef(null),w=i.useCallback((e,n=!1)=>{n&&(R.current=R.current.filter(r=>!r.count)),R.current.push({text:e,count:n}),!q.current&&(q.current=setTimeout(()=>{q.current=null;const r=R.current.map(c=>c.text).join(" ");R.current=[],ae(c=>({text:r,n:c.n+1}))},0))},[]),A=i.useRef(null),[ce,ie]=i.useState(0),X=e=>{A.current=e,ie(n=>n+1)};i.useLayoutEffect(()=>{const e=A.current;if(!e)return;A.current=null,(P.current?.querySelector(e)??P.current?.querySelector("[data-file-choose]"))?.focus()},[ce]);const L=i.useCallback(e=>{p.current=e,ne(e),k.current.onChange?.(e)},[]),N=i.useCallback((e,n)=>{p.current.some(r=>r.id===e)&&L(p.current.map(r=>r.id===e?{...r,...n}:r))},[L]);function M(){const{concurrency:e}=k.current,n=p.current.filter(c=>c.status==="uploading"||c.status==="processing").length,r=p.current.filter(c=>c.status==="queued").slice(0,Math.max(0,Math.max(1,e)-n));for(const c of r)ue(c)}function ue(e){const n=new AbortController;j.current.set(e.id,n),N(e.id,{status:"uploading",progress:null,reason:void 0});const r=()=>!n.signal.aborted&&j.current.get(e.id)===n,c={onProgress:u=>{if(!r())return;const l=Math.round(Math.min(1,Math.max(0,Number.isFinite(u)?u:0))*100)/100,o=p.current.find($=>$.id===e.id);o?.status==="uploading"&&o.progress!==l&&N(e.id,{progress:l})},onProcessing:()=>{r()&&N(e.id,{status:"processing"})},signal:n.signal};new Promise(u=>u(k.current.upload(e.file,c))).then(u=>{if(!r())return;j.current.delete(e.id),N(e.id,{status:"done",progress:1,result:u});const l=p.current.filter(o=>o.status!=="rejected");w(k.current.t.progress(l.filter(o=>o.status==="done").length,l.length),!0),M()},u=>{if(!r())return;j.current.delete(e.id);const{t:l}=k.current;if(be.isFileUploadRejection(u)){const o=u.message,$=typeof o=="string"&&o?o:l.uploadError(u);N(e.id,{status:"rejected",progress:null,reason:$}),w(l.rejectedNotice(e.file.name,$))}else{const o=l.uploadError(u);N(e.id,{status:"failed",reason:o}),w(l.failedNotice(e.file.name,o))}M()})}i.useLayoutEffect(()=>{k.current={upload:d,onChange:x,concurrency:K,t:a,pump:M}}),i.useEffect(()=>{const e=j.current;return p.current.some(r=>(r.status==="uploading"||r.status==="processing")&&!e.has(r.id))&&(L(p.current.map(r=>(r.status==="uploading"||r.status==="processing")&&!e.has(r.id)?{...r,status:"queued",progress:null}:r)),k.current.pump()),()=>{for(const r of e.values())r.abort();e.clear(),q.current&&clearTimeout(q.current),q.current=null,R.current=[]}},[L]);const le=b??T.acceptList(v),de=(e,n)=>{if(n.some(c=>T.sameFile(c.file,e)))return a.duplicate(e);const r=te?.(e);if(r)return r;if(!T.matchesType(e,le))return a.wrongType(e);if(m!==void 0&&e.size>m)return a.tooLarge(e,m);if(g!==void 0&&n.length>=g)return a.tooMany(g)},Y=e=>{if(f||e.length===0)return;const n=D?p.current.find(o=>o.status!=="rejected"):void 0,r=p.current.filter(o=>o.status!=="rejected"&&o!==n),c=[];for(const o of e){const $=n&&T.sameFile(n.file,o)?a.duplicate(o):de(o,[...r,...c.filter(je=>je.status!=="rejected")]);c.push({id:`${S}-file-${oe.current++}`,file:o,status:$?"rejected":"queued",progress:null,reason:$})}const u=c.filter(o=>o.status==="queued").length,l=u?n:void 0;l&&(j.current.get(l.id)?.abort(),j.current.delete(l.id)),L([...p.current.filter(o=>o!==l),...c]),l&&w(a.removed(l.file.name)),u&&w(a.added(u));for(const o of c)o.reason&&w(a.rejectedNotice(o.file.name,o.reason));M()},fe=e=>{if(f)return;const n=p.current,r=n.findIndex(u=>u.id===e);if(r<0)return;j.current.get(e)?.abort(),j.current.delete(e);const c=n[r+1]??n[r-1];L(n.filter(u=>u.id!==e)),w(a.removed(n[r].file.name)),X(c?`[data-file-remove="${c.id}"]`:"[data-file-choose]"),M()},pe=e=>{f||(N(e,{status:"queued",progress:null,reason:void 0}),X(`[data-file-remove="${e}"]`),M())},[ge,H]=i.useState(0),B=e=>Array.from(e.dataTransfer?.types??[]).includes("Files"),he={onDragEnter:e=>{B(e)&&(e.preventDefault(),H(n=>n+1))},onDragOver:e=>{B(e)&&(e.preventDefault(),e.dataTransfer.dropEffect=f?"none":"copy")},onDragLeave:e=>{B(e)&&H(n=>Math.max(0,n-1))},onDrop:e=>{B(e)&&(e.preventDefault(),H(0),Y(Array.from(e.dataTransfer.files)))}},W=ge>0&&!f,xe=e=>{switch(e.status){case"queued":return a.queued;case"uploading":return a.uploading(e.progress===null?null:Math.round(e.progress*100));case"processing":return a.processing;case"done":return a.done;case"failed":return`${a.failed} ${e.reason??""}`.trim();case"rejected":return`${a.rejected} ${e.reason??""}`.trim()}},me=[z?V:null,E?J:null].filter(Boolean).join(" ")||void 0,y=D?C.find(e=>e.status!=="rejected")??C.at(-1):void 0;return t.jsxs("div",{ref:P,role:"group","aria-labelledby":I,className:h.cn("rst:font-ui rst:w-full",se),"data-file-upload":"",children:[t.jsx("p",{id:I,className:h.cn("rst:mb-1.5 rst:text-left rst:text-sm rst:font-medium rst:leading-none rst:text-[var(--roster-control-text)]",f&&"rst:opacity-70"),children:s}),t.jsxs("div",{...he,"data-file-drop":"","data-dragging":W?"":void 0,className:h.cn("rst:flex rst:flex-col rst:items-center rst:justify-center rst:gap-2 rst:rounded-lg rst:border-2 rst:border-dashed rst:px-4 rst:py-6 rst:text-center rst:transition-colors rst:motion-reduce:transition-none",D&&"rst:relative rst:h-[var(--roster-file-preview-height,13rem)]",D&&y&&"rst:justify-end",W?"rst:border-primary-500 rst:bg-primary-500/10":E?"rst:border-error-500 rst:dark:border-error-400":"rst:border-gray-300 rst:dark:border-gray-600"),children:[y&&t.jsx(Re,{file:y.file,status:y.status},y.id),t.jsx(_.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"lg",className:h.cn("rst:px-5",f&&G,y&&"rst:relative rst:z-10 rst:bg-[var(--roster-card-bg)] rst:dark:hover:bg-gray-800"),"aria-disabled":f||void 0,"aria-describedby":me,onClick:()=>Q.current?.click(),"data-file-choose":"",startIcon:t.jsx("svg",{...U,"aria-hidden":"true",children:t.jsx("path",{d:"M12 16V4M7 9l5-5 5 5M5 20h14"})}),children:y?a.replace:a.choose}),!y&&t.jsx("p",{className:"rst:hidden rst:text-xs rst:text-gray-600 rst:pointer-fine:block rst:dark:text-gray-400","data-file-drop-hint":"",children:W?a.dropHere:a.dropHint}),t.jsx("input",{ref:Q,type:"file",hidden:!0,tabIndex:-1,accept:v,multiple:g!==1,disabled:f,onChange:e=>{Y(Array.from(e.currentTarget.files??[])),e.currentTarget.value=""},"data-file-input":""})]}),z&&t.jsx("p",{id:V,className:"rst:mt-1.5 rst:text-left rst:text-xs rst:text-gray-500 rst:dark:text-gray-400",children:z}),E&&t.jsx("p",{id:J,className:"rst:mt-1.5 rst:text-left rst:text-xs rst:font-medium rst:text-error-600 rst:dark:text-error-400",children:E}),C.length>0&&t.jsx("ul",{className:"rst:mt-3 rst:flex rst:flex-col rst:gap-2","aria-labelledby":I,children:C.map(e=>{const n=e.status==="failed"||e.status==="rejected",r=e.status==="uploading"||e.status==="processing"||e.status==="queued";return t.jsxs("li",{"data-file-item":e.id,"data-status":e.status,className:"rst:flex rst:items-center rst:gap-3 rst:rounded-md rst:border rst:border-[var(--roster-card-border)] rst:p-2",children:[!D&&t.jsx($e,{file:e.file,status:e.status}),t.jsxs("div",{className:"rst:min-w-0 rst:flex-1 rst:text-left",children:[t.jsx("p",{className:"rst:truncate rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)]",title:e.file.name,children:e.file.name}),t.jsxs("p",{className:h.cn("rst:mt-0.5 rst:flex rst:items-start rst:gap-1 rst:text-xs",n?"rst:text-error-600 rst:dark:text-error-400":"rst:text-gray-600 rst:dark:text-gray-400"),"data-file-status":"",children:[e.status==="done"&&t.jsx("svg",{...U,width:14,height:14,"aria-hidden":"true",className:"rst:mt-px rst:shrink-0 rst:text-success-600 rst:dark:text-success-400",children:t.jsx("path",{d:"M20 6 9 17l-5-5"})}),t.jsxs("span",{children:[t.jsx("span",{className:"rst:tabular-nums",children:a.size(e.file.size)}),t.jsx("span",{"aria-hidden":"true",children:" · "}),xe(e)]})]}),r&&t.jsx("div",{className:"rst:mt-1.5","data-file-progress":"",children:t.jsx(ye.Progress,{size:"sm",value:e.status==="processing"?100:e.status==="uploading"?(e.progress??0)*100:0,busy:e.status==="processing"||e.status==="uploading"&&e.progress===null,announce:!1})})]}),e.status==="failed"&&t.jsx(_.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:h.cn("rst:h-11 rst:w-11",f&&G),"aria-disabled":f||void 0,"aria-label":a.retry(e.file.name),onClick:()=>pe(e.id),"data-file-retry":e.id,children:t.jsxs("svg",{...U,"aria-hidden":"true",children:[t.jsx("path",{d:"M3 12a9 9 0 1 0 3-6.7L3 8"}),t.jsx("path",{d:"M3 3v5h5"})]})}),t.jsx(_.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:h.cn("rst:h-11 rst:w-11",f&&G),"aria-disabled":f||void 0,"aria-label":a.remove(e.file.name),onClick:()=>fe(e.id),"data-file-remove":e.id,children:t.jsx("svg",{...U,"aria-hidden":"true",children:t.jsx("path",{d:"M18 6 6 18M6 6l12 12"})})})]},e.id)})}),t.jsxs("div",{role:"status","aria-live":"polite","aria-atomic":"true",className:"rst:sr-only","data-file-live":"","data-file-live-n":O.n,children:[O.text,O.n%2?" ":""]})]})}exports.FileUpload=qe;
|
|
@@ -19,12 +19,14 @@ export interface FileUploadItem<R = unknown> {
|
|
|
19
19
|
progress: number | null;
|
|
20
20
|
/** What `upload` resolved with, once `done`: an id to send with the form, say. */
|
|
21
21
|
result?: R;
|
|
22
|
-
/** Why it `failed`, or why it was `rejected`. */
|
|
22
|
+
/** Why it `failed`, or why it was `rejected`: by the rules, or by `upload` with a `FileUploadRejection`. */
|
|
23
23
|
reason?: string;
|
|
24
24
|
}
|
|
25
25
|
/** Every word FileUpload shows or says. Pass any of them in `strings` to replace it. */
|
|
26
26
|
export interface FileUploadStrings {
|
|
27
27
|
choose: string;
|
|
28
|
+
/** The Choose button's words while the drop box shows a picture (`preview="dropzone"`). */
|
|
29
|
+
replace: string;
|
|
28
30
|
/** Beside the button where a pointer can drag; hidden on touch screens. */
|
|
29
31
|
dropHint: string;
|
|
30
32
|
/** While files are dragged over. */
|
|
@@ -56,7 +58,9 @@ export interface FileUploadProps<R = unknown> {
|
|
|
56
58
|
/**
|
|
57
59
|
* Sends one file, and resolves when it's done with whatever the app needs
|
|
58
60
|
* later (an id, a URL). Rejects with an Error whose message says why, and
|
|
59
|
-
* that message is shown.
|
|
61
|
+
* that message is shown, with Try again. Reject with a `FileUploadRejection`
|
|
62
|
+
* instead when the file can never be added: it ends `rejected`, with no Try
|
|
63
|
+
* again. FileUpload never sends anything itself.
|
|
60
64
|
*/
|
|
61
65
|
upload: (file: File, context: FileUploadContext) => Promise<R>;
|
|
62
66
|
/** Every change to the list, with each file's status and result. */
|
|
@@ -95,6 +99,19 @@ export interface FileUploadProps<R = unknown> {
|
|
|
95
99
|
disabled?: boolean;
|
|
96
100
|
strings?: Partial<FileUploadStrings>;
|
|
97
101
|
className?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Where a picture of the file shows. `"row"` (default): a thumbnail at the
|
|
104
|
+
* start of each file's row. `"dropzone"`: the picture fills the drop box,
|
|
105
|
+
* contained, and the rows keep everything else (name, status, progress,
|
|
106
|
+
* Remove, Try again).
|
|
107
|
+
*
|
|
108
|
+
* `"dropzone"` holds one file, so it takes `maxFiles={1}`; with any other
|
|
109
|
+
* `maxFiles` the pictures stay on the rows, and a development build says
|
|
110
|
+
* so. The box keeps its height whether it shows a picture or not
|
|
111
|
+
* (`--roster-file-preview-height`, 13rem by default), and a file that
|
|
112
|
+
* passes the rules takes the place of the one there, picked or dropped.
|
|
113
|
+
*/
|
|
114
|
+
preview?: "row" | "dropzone";
|
|
98
115
|
}
|
|
99
116
|
/**
|
|
100
117
|
* Picks files, uploads each one through the app, and shows where each stands.
|
|
@@ -107,5 +124,5 @@ export interface FileUploadProps<R = unknown> {
|
|
|
107
124
|
* Choosing is a button, which works everywhere; dragging files onto the field
|
|
108
125
|
* is an addition for a pointer, never the only way in.
|
|
109
126
|
*/
|
|
110
|
-
declare function FileUpload<R = unknown>({ label, upload, onChange, accept, types, maxSize, maxFiles, validate, concurrency, helperText, errorMessage, disabled, strings: overrides, className, }: FileUploadProps<R>): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
declare function FileUpload<R = unknown>({ label, upload, onChange, accept, types, maxSize, maxFiles, validate, concurrency, helperText, errorMessage, disabled, strings: overrides, className, preview, }: FileUploadProps<R>): import("react/jsx-runtime").JSX.Element;
|
|
111
128
|
export { FileUpload };
|