@blakesteve/roster 5.3.0 → 5.5.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.
Files changed (39) hide show
  1. package/README.md +177 -1
  2. package/dist/components/atoms/Button/Button.cjs +1 -1
  3. package/dist/components/atoms/Button/Button.d.ts +11 -0
  4. package/dist/components/atoms/Button/Button.js +42 -32
  5. package/dist/components/atoms/Button/button-variants.cjs +1 -1
  6. package/dist/components/atoms/Button/button-variants.js +3 -1
  7. package/dist/components/molecules/FileUpload/FileUpload.cjs +1 -0
  8. package/dist/components/molecules/FileUpload/FileUpload.d.ts +111 -0
  9. package/dist/components/molecules/FileUpload/FileUpload.js +403 -0
  10. package/dist/components/molecules/FileUpload/file-rules.cjs +1 -0
  11. package/dist/components/molecules/FileUpload/file-rules.d.ts +22 -0
  12. package/dist/components/molecules/FileUpload/file-rules.js +43 -0
  13. package/dist/components/molecules/MediaPreview/MediaPreview.cjs +1 -0
  14. package/dist/components/molecules/MediaPreview/MediaPreview.d.ts +27 -0
  15. package/dist/components/molecules/MediaPreview/MediaPreview.js +55 -0
  16. package/dist/components/organisms/Dialog/Dialog.cjs +1 -1
  17. package/dist/components/organisms/Dialog/Dialog.d.ts +9 -1
  18. package/dist/components/organisms/Dialog/Dialog.js +26 -24
  19. package/dist/components/organisms/ModerationQueue/ModerationQueue.cjs +1 -0
  20. package/dist/components/organisms/ModerationQueue/ModerationQueue.d.ts +73 -0
  21. package/dist/components/organisms/ModerationQueue/ModerationQueue.js +428 -0
  22. package/dist/components/organisms/ModerationQueue/QueueEditDialog.cjs +1 -0
  23. package/dist/components/organisms/ModerationQueue/QueueEditDialog.d.ts +8 -0
  24. package/dist/components/organisms/ModerationQueue/QueueEditDialog.js +37 -0
  25. package/dist/components/organisms/ModerationQueue/QueueEditSheet.cjs +1 -0
  26. package/dist/components/organisms/ModerationQueue/QueueEditSheet.d.ts +12 -0
  27. package/dist/components/organisms/ModerationQueue/QueueEditSheet.js +30 -0
  28. package/dist/components/organisms/ModerationQueue/queue-actions.cjs +1 -0
  29. package/dist/components/organisms/ModerationQueue/queue-actions.d.ts +157 -0
  30. package/dist/components/organisms/ModerationQueue/queue-actions.js +6 -0
  31. package/dist/components/organisms/Navbar/Navbar.cjs +1 -1
  32. package/dist/components/organisms/Navbar/Navbar.d.ts +2 -0
  33. package/dist/components/organisms/Navbar/Navbar.js +49 -49
  34. package/dist/index.d.ts +6 -0
  35. package/dist/meta.json +3 -3
  36. package/dist/roster.cjs +1 -1
  37. package/dist/roster.css +1 -1
  38. package/dist/roster.js +123 -111
  39. package/package.json +4 -3
package/README.md CHANGED
@@ -1208,6 +1208,179 @@ escape the box. Lay the options out across with `orientation` rather than
1208
1208
  through `optionsClassName`, for the same reason: the horizontal spacing that
1209
1209
  keeps two radios 44px apart rides on that prop.
1210
1210
 
1211
+ ### ModerationQueue
1212
+
1213
+ A queue of things waiting for a decision: submissions to approve, reports to
1214
+ resolve, claims to check before they publish. The app owns the data and every
1215
+ request; the queue owns the layout, the states, the steps and the keyboard.
1216
+ **It never fetches.** An action's `run` is the only path from a decision to
1217
+ the network, and the app says an item has left by removing it from `items`.
1218
+
1219
+ ```tsx
1220
+ import { ModerationQueue, QueueEditDialog } from "@blakesteve/roster";
1221
+
1222
+ <ModerationQueue
1223
+ label="New recipes"
1224
+ items={recipes}
1225
+ getKey={(r) => r.id}
1226
+ itemLabel={(r) => `${r.title}, from ${r.by}`}
1227
+ renderMeta={(r) => <Badge size="xs">{r.category}</Badge>}
1228
+ renderContent={(r) => <p>{r.method}</p>}
1229
+ status={loading ? "loading" : failed ? "error" : "ready"}
1230
+ onRetry={reload}
1231
+ actions={(a) => [
1232
+ { id: "list", label: "List it", done: "Listed", tone: "success",
1233
+ run: async (r) => { await api.list(r.id); await reload(); } },
1234
+ a.edit({ id: "edit", label: "Edit and list", done: "Listed with edits",
1235
+ edit: { title: "Tidy it up", submitLabel: "List it", surface: QueueEditDialog,
1236
+ initial: (r) => ({ title: r.title }), Form: TitleForm,
1237
+ validate: (d) => (d.title.trim() ? undefined : "Give it a title.") },
1238
+ run: async (r, { draft }) => { await api.list(r.id, draft); await reload(); } }),
1239
+ { id: "send-back", label: "Send back", done: "Sent back", tone: "danger",
1240
+ reason: { prompt: "Why? The cook will see this.", required: true,
1241
+ options: [{ value: "duplicate", label: "We already have this one" }] },
1242
+ run: async (r, { reason }) => { await api.sendBack(r.id, reason); await reload(); } },
1243
+ ]}
1244
+ />;
1245
+ ```
1246
+
1247
+ **Four kinds of action.** One click (no step). `confirm` asks in place first.
1248
+ `reason` asks why in place: radios, a note, or both. `edit` opens a form the
1249
+ app supplies, inline by default or in a Dialog or Sheet (`QueueEditDialog`,
1250
+ `QueueEditSheet`, separate exports so a queue that doesn't use them doesn't
1251
+ ship them).
1252
+
1253
+ **Drafts are typed.** An edit's draft is whatever its `initial` returns, and
1254
+ `validate`, the form and `run` all get that type. Inside JSX, pass `actions` as
1255
+ a function and write edits with the `edit` it's handed, as above; outside it,
1256
+ `editAction(…)` does the same in a list typed `QueueAction<Item>[]`. A literal
1257
+ `{ edit: … }` doesn't compile, because its draft could only be `any`.
1258
+
1259
+ What it promises:
1260
+
1261
+ - **Only the clicked action is busy, and it keeps focus.** It shows a spinner
1262
+ and is `aria-disabled` rather than `disabled`, which would drop focus to the
1263
+ page. That item's other actions wait; every other item stays live.
1264
+ - **A decision that lands and removes the item** is announced in a polite live
1265
+ region ("Listed: Smoky red lentil soup, from Dana Okafor. 2 left."), and
1266
+ focus moves to the next item, the previous one, or the empty state. Never to
1267
+ the page. If the app keeps the item, the announcement drops the count and
1268
+ focus stays. `run` may resolve with `{ announce }` to say something else
1269
+ ("Already decided by someone else.").
1270
+ - **A decision that fails** leaves the item where it was with an Alert saying
1271
+ why, and focus where it was. Inside a step, the step stays open with its
1272
+ draft. A reviewer who moved to another item meanwhile isn't pulled back.
1273
+ - **Several outcomes in one render are said in one sentence**: "Approved:
1274
+ Lentil soup. Approved: Rye crackers. 1 left."
1275
+ - **Steps are per item.** A reason half-written on one item survives another
1276
+ item's decision.
1277
+ - **Steps take focus and give it back.** Escape or Cancel returns focus to the
1278
+ button that opened the step.
1279
+ - **An item that disappears** (another reviewer, a reload) with its step open
1280
+ closes the step without deciding, and is announced once as decided
1281
+ elsewhere. Without a step, it moves focus only if focus was inside it.
1282
+ - **Headings follow the page.** `headingLevel` sets the queue's (2 by default),
1283
+ and items sit one below unless `itemHeadingLevel` says otherwise.
1284
+ - **Phones.** Every button is 44px tall at every width, and nothing scrolls
1285
+ sideways at 320px. Nothing animates as an item leaves.
1286
+
1287
+ Counts for a tab badge need no API: the app holds the items, so it's
1288
+ `items.length`. `MediaPreview` is the thumbnail to use in an item: when the
1289
+ image can't render (an expired link, a format the browser can't draw), it
1290
+ falls back to a link to the original in the same footprint.
1291
+
1292
+ ### FileUpload
1293
+
1294
+ Picks files, uploads each one through the app, and shows where each stands.
1295
+ **It never sends anything itself.** The app passes `upload`, which is called
1296
+ once per file and returns a promise; FileUpload owns picking, the rules, the
1297
+ list, each file's state, the keyboard and what a screen reader hears.
1298
+
1299
+ ```tsx
1300
+ import { FileUpload } from "@blakesteve/roster";
1301
+
1302
+ <FileUpload
1303
+ label="Photos"
1304
+ helperText="JPEG or PNG, up to 10 photos, 15 MB each."
1305
+ types={["image/jpeg", "image/png"]}
1306
+ maxFiles={10}
1307
+ maxSize={15 * 1024 * 1024}
1308
+ validate={(file) => (/\.hei[cf]$/i.test(file.name) ? "Export it as JPEG first." : undefined)}
1309
+ upload={async (file, { onProgress, onProcessing, signal }) => {
1310
+ const { id, url } = await api.presignPhoto(file.type, { signal });
1311
+ await putWithProgress(url, file, { onProgress, signal });
1312
+ onProcessing();
1313
+ await api.completePhoto(id, { signal });
1314
+ return id;
1315
+ }}
1316
+ onChange={(items) => setPhotoIds(items.filter((i) => i.status === "done").map((i) => i.result!))}
1317
+ />;
1318
+ ```
1319
+
1320
+ `upload(file, { onProgress, onProcessing, signal })`:
1321
+
1322
+ - **`onProgress(fraction)`** from 0 to 1, if the request can report it
1323
+ (`fetch` can't; an `XMLHttpRequest`'s `upload.onprogress` can). Without it the
1324
+ file reads Uploading with a bar that doesn't give a figure.
1325
+ - **`onProcessing()`** once the bytes are up and the app's server is working on
1326
+ them. The file reads Finishing up until the promise settles.
1327
+ - **`signal`** aborts when the person removes the file, or the field unmounts.
1328
+ Pass it to the request.
1329
+ - **Resolve** with what the form needs later, an id say; it comes back as that
1330
+ file's `result` in `onChange`. **Reject** with an Error whose message says
1331
+ why; it's shown, with Try again.
1332
+
1333
+ Each file is `queued`, `uploading`, `processing`, `done`, `failed` or
1334
+ `rejected`. A few upload at once (`concurrency`, 3 by default) and the rest
1335
+ wait.
1336
+
1337
+ **Rules** are checked as files arrive, picked or dropped: the same file picked
1338
+ twice is turned away first; then `validate(file)`, the app's own reason; then
1339
+ `types` (`image/jpeg`, `image/*`, `.png`,
1340
+ defaulting to `accept`); then `maxSize` in bytes; then `maxFiles`. A file
1341
+ turned away stays in the list with its reason and is never sent. A file that
1342
+ arrives with no type, as some cloud-backed picks on Android do, is judged by
1343
+ its extension.
1344
+
1345
+ **Phones first.** `accept` defaults to `"image/*"`, with `multiple` and no
1346
+ `capture`. That combination is what opens Android's system photo picker, with
1347
+ albums and cloud photos; a list of exact types or an extension gets a file
1348
+ browser instead. Keep `accept` broad and narrow with `types`.
1349
+
1350
+ What it promises:
1351
+
1352
+ - **Disabled, it keeps focus.** Its buttons are `aria-disabled` and ignore
1353
+ presses, so a form that disables the field while it sends doesn't drop
1354
+ whoever was on Remove to the top of the page.
1355
+ - **Choosing is a button** that opens the picker. Dragging files onto the
1356
+ field is an addition where there's a pointer, and the hint for it shows only
1357
+ there. A file let go over the field never replaces the page, even while
1358
+ disabled.
1359
+ - **Removing a file cancels its upload** and moves focus to the next file's
1360
+ Remove, the previous one's, or the Choose button. Never to the page. Try
1361
+ again moves focus to that file's Remove.
1362
+ - **A polite live region** says what happened, one sentence per moment: "3
1363
+ photos added. big.jpg wasn't added. It's over 15 MB.", "2 of 3 uploaded.",
1364
+ "c.jpg didn't upload. The server is busy." Of several counts at once, only
1365
+ the latest is said.
1366
+ - **Thumbnails** come from object URLs, revoked when the file goes. A picture
1367
+ the browser can't draw (HEIC outside Safari) shows a plain mark.
1368
+ - **Every word is replaceable** through `strings`, on screen and spoken.
1369
+ - **Phones.** Every button is a 44px target at 375px, and a long file name
1370
+ truncates rather than pushing Remove off its row.
1371
+
1372
+ ### Buttons that load
1373
+
1374
+ `isLoading` shows a spinner and ignores presses, and since 5.5.0 the button
1375
+ keeps focus while it does. It used to set `disabled`, and a browser drops focus
1376
+ from a disabled element to the page, so a keyboard or screen reader user who
1377
+ pressed Save was sent back to the top. A loading button is now `aria-disabled`
1378
+ and `aria-busy` instead, and ignores presses itself (clicks, Enter, Space, and the
1379
+ app's own press handlers, while hover ones still run), which also keeps a
1380
+ submit button from sending its form twice. It no longer dims, since
1381
+ the spinner says it's busy. `loadingLabel` names the spinner ("loading" by
1382
+ default). `disabled` is unchanged and wins over `isLoading`.
1383
+
1211
1384
  ### Carousel
1212
1385
 
1213
1386
  A sideways row: a row of cards, a selection strip, or a gallery of one per
@@ -1349,6 +1522,8 @@ function App() {
1349
1522
  | `LiquidNav` | `LiquidTabs`' strip as a `nav` of links; takes a router's link component |
1350
1523
  | `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, 44px `lg` size |
1351
1524
  | `MatchupCard` | Head-to-head comparison card |
1525
+ | `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
1526
+ | `FileUpload` | Picks files and uploads each through the app's `upload`, with per-file progress, retry and rules |
1352
1527
  | `Pullquote` | A line lifted out of prose, as `<figure>` + `<blockquote>` + `<figcaption>` |
1353
1528
  | `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
1354
1529
  | `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
@@ -1360,8 +1535,9 @@ function App() {
1360
1535
  | `ActionBar` | Sticky bottom action strip |
1361
1536
  | `Countdown` | Live countdown timer |
1362
1537
  | `DataTable` | Full-featured table with sorting and pagination via TanStack Table v9. Imported from `@blakesteve/roster/data-table` (see [DataTable columns](#datatable-columns)) |
1363
- | `Dialog` | Modal dialog |
1538
+ | `Dialog` | Modal dialog; `onAfterClose` runs once it has finished closing |
1364
1539
  | `Footer` | Site footer |
1540
+ | `ModerationQueue` | Items waiting for a decision: busy per action, confirm, reason and edit steps, focus after an item leaves, a live region. Never fetches (see [ModerationQueue](#moderationqueue)) |
1365
1541
  | `Navbar` | Responsive navigation bar with mobile slide-out |
1366
1542
  | `Sheet` | Bottom sheet: drag, Escape or backdrop to dismiss; only its content scrolls; returns focus, even from a deep link; the title may be a node |
1367
1543
  | `Table` | Static data table |
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),f=require("react"),h=require("../../../lib/utils.cjs"),j=require("../Spinner/Spinner.cjs"),p=require("./button-variants.cjs"),q=require("../../../node_modules/@headlessui/react/dist/components/button/button.cjs"),n=f.forwardRef(({className:i,colorScheme:a,variant:c,size:l,isLoading:t=!1,startIcon:s,endIcon:r,surface:u="auto",disabled:o,children:m,...d},x)=>{const b=o||t;return e.jsxs(q.Button,{className:h.cn(p.buttonVariants({colorScheme:a,variant:c,size:l}),u==="dark"&&"dark",i),ref:x,disabled:b,...d,children:[t&&e.jsx("span",{className:"rst:mr-2 rst:flex rst:shrink-0 rst:items-center",children:e.jsx(j.Spinner,{size:"sm",variant:"current"})}),!t&&s&&e.jsx("span",{className:"rst:mr-2 rst:inline-flex rst:shrink-0 rst:items-center",children:s}),m,!t&&r&&e.jsx("span",{className:"rst:ml-2 rst:inline-flex rst:shrink-0 rst:items-center",children:r})]})});n.displayName="Button";exports.Button=n;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),h=require("react"),j=require("../../../lib/utils.cjs"),p=require("../Spinner/Spinner.cjs"),y=require("./button-variants.cjs"),S=require("../../../node_modules/@headlessui/react/dist/components/button/button.cjs"),q=/^on(?:Click|DoubleClick|AuxClick|(?:Pointer|Mouse)(?:Down|Up)|Key(?:Down|Up|Press)|Touch(?:Start|End))(?:Capture)?$/;function P(u){const r={};for(const[e,a]of Object.entries(u))q.test(e)||(r[e]=a);return{...r,onClick:e=>e.preventDefault()}}const m=h.forwardRef(({className:u,colorScheme:r,variant:e,size:a,isLoading:s=!1,loadingLabel:b,startIcon:c,endIcon:o,surface:f="auto",disabled:l,children:k,...n},x)=>{const i=s&&!l,d=i?P(n):n;return t.jsxs(S.Button,{className:j.cn(y.buttonVariants({colorScheme:r,variant:e,size:a}),f==="dark"&&"dark",i&&"rst:cursor-progress",u),ref:x,disabled:l,...d,"aria-disabled":i?!0:n["aria-disabled"],"aria-busy":i?!0:n["aria-busy"],children:[s&&t.jsx("span",{className:"rst:mr-2 rst:flex rst:shrink-0 rst:items-center",children:t.jsx(p.Spinner,{size:"sm",variant:"current",label:b})}),!s&&c&&t.jsx("span",{className:"rst:mr-2 rst:inline-flex rst:shrink-0 rst:items-center",children:c}),k,!s&&o&&t.jsx("span",{className:"rst:ml-2 rst:inline-flex rst:shrink-0 rst:items-center",children:o})]})});m.displayName="Button";exports.Button=m;
@@ -2,7 +2,18 @@ import { default as React } from 'react';
2
2
  import { VariantProps } from 'class-variance-authority';
3
3
  import { buttonVariants } from './button-variants';
4
4
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
5
+ /**
6
+ * Shows a spinner and ignores presses while something the button started is
7
+ * in flight. Unlike `disabled`, it keeps the button focusable: a native
8
+ * `disabled` drops focus to the page the moment it lands, so a keyboard or
9
+ * screen reader user who just pressed Save is thrown back to the top. A
10
+ * loading button stays where it is, says it's busy (`aria-busy`) and
11
+ * unavailable (`aria-disabled`), and swallows clicks, Enter and Space, which
12
+ * also stops a submit button sending its form twice.
13
+ */
5
14
  isLoading?: boolean;
15
+ /** What a screen reader hears for the spinner while loading: "loading" by default. */
16
+ loadingLabel?: string;
6
17
  startIcon?: React.ReactNode;
7
18
  endIcon?: React.ReactNode;
8
19
  /**
@@ -1,50 +1,60 @@
1
1
  "use client";
2
- import { jsxs as d, jsx as t } from "react/jsx-runtime";
3
- import h from "react";
2
+ import { jsxs as b, jsx as a } from "react/jsx-runtime";
3
+ import d from "react";
4
4
  import { cn as x } from "../../../lib/utils.js";
5
- import { Spinner as k } from "../Spinner/Spinner.js";
6
- import { buttonVariants as N } from "./button-variants.js";
7
- import { Button as b } from "../../../node_modules/@headlessui/react/dist/components/button/button.js";
8
- const B = h.forwardRef(
5
+ import { Spinner as y } from "../Spinner/Spinner.js";
6
+ import { buttonVariants as C } from "./button-variants.js";
7
+ import { Button as N } from "../../../node_modules/@headlessui/react/dist/components/button/button.js";
8
+ const P = /^on(?:Click|DoubleClick|AuxClick|(?:Pointer|Mouse)(?:Down|Up)|Key(?:Down|Up|Press)|Touch(?:Start|End))(?:Capture)?$/;
9
+ function w(i) {
10
+ const t = {};
11
+ for (const [r, o] of Object.entries(i)) P.test(r) || (t[r] = o);
12
+ return { ...t, onClick: (r) => r.preventDefault() };
13
+ }
14
+ const D = d.forwardRef(
9
15
  ({
10
- className: n,
11
- colorScheme: m,
12
- variant: a,
13
- size: i,
14
- isLoading: r = !1,
15
- startIcon: s,
16
- endIcon: e,
17
- surface: l = "auto",
18
- disabled: o,
19
- children: c,
20
- ...f
21
- }, p) => {
22
- const u = o || r;
23
- return /* @__PURE__ */ d(
24
- b,
16
+ className: i,
17
+ colorScheme: t,
18
+ variant: r,
19
+ size: o,
20
+ isLoading: e = !1,
21
+ loadingLabel: m,
22
+ startIcon: l,
23
+ endIcon: c,
24
+ surface: f = "auto",
25
+ disabled: u,
26
+ children: p,
27
+ ...s
28
+ }, k) => {
29
+ const n = e && !u, h = n ? w(s) : s;
30
+ return /* @__PURE__ */ b(
31
+ N,
25
32
  {
26
33
  className: x(
27
- N({ colorScheme: m, variant: a, size: i }),
34
+ C({ colorScheme: t, variant: r, size: o }),
28
35
  // Roster's dark variant is `&:where(.dark, .dark *)`, so an element
29
36
  // carrying `.dark` itself satisfies it. That is all forcing the
30
37
  // dark surface requires.
31
- l === "dark" && "dark",
32
- n
38
+ f === "dark" && "dark",
39
+ n && "rst:cursor-progress",
40
+ i
33
41
  ),
34
- ref: p,
42
+ ref: k,
35
43
  disabled: u,
36
- ...f,
44
+ ...h,
45
+ "aria-disabled": n ? !0 : s["aria-disabled"],
46
+ "aria-busy": n ? !0 : s["aria-busy"],
37
47
  children: [
38
- r && /* @__PURE__ */ t("span", { className: "rst:mr-2 rst:flex rst:shrink-0 rst:items-center", children: /* @__PURE__ */ t(k, { size: "sm", variant: "current" }) }),
39
- !r && s && /* @__PURE__ */ t("span", { className: "rst:mr-2 rst:inline-flex rst:shrink-0 rst:items-center", children: s }),
40
- c,
41
- !r && e && /* @__PURE__ */ t("span", { className: "rst:ml-2 rst:inline-flex rst:shrink-0 rst:items-center", children: e })
48
+ e && /* @__PURE__ */ a("span", { className: "rst:mr-2 rst:flex rst:shrink-0 rst:items-center", children: /* @__PURE__ */ a(y, { size: "sm", variant: "current", label: m }) }),
49
+ !e && l && /* @__PURE__ */ a("span", { className: "rst:mr-2 rst:inline-flex rst:shrink-0 rst:items-center", children: l }),
50
+ p,
51
+ !e && c && /* @__PURE__ */ a("span", { className: "rst:ml-2 rst:inline-flex rst:shrink-0 rst:items-center", children: c })
42
52
  ]
43
53
  }
44
54
  );
45
55
  }
46
56
  );
47
- B.displayName = "Button";
57
+ D.displayName = "Button";
48
58
  export {
49
- B as Button
59
+ D as Button
50
60
  };
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("../../../node_modules/class-variance-authority/dist/index.cjs"),t=r.cva("rst:font-ui rst:inline-flex rst:items-center rst:justify-center rst:cursor-pointer rst:whitespace-nowrap rst:rounded-md rst:text-sm rst:font-medium rst:ring-offset-background rst:transition-colors rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:disabled:pointer-events-none rst:disabled:opacity-50",{variants:{variant:{solid:"rst:border rst:border-transparent rst:elevation-control",soft:"rst:border-transparent rst:shadow-none",outline:"rst:border rst:bg-transparent rst:elevation-control",ghost:"rst:border rst:border-transparent rst:bg-transparent",link:"rst:bg-transparent rst:underline-offset-4 rst:hover:underline"},colorScheme:{primary:"",orange:"",teal:"",purple:"",amber:"",success:"",error:"",neutral:""},size:{xs:"rst:h-7 rst:rounded-sm rst:px-2 rst:text-xs",sm:"rst:h-9 rst:rounded-md rst:px-3",default:"rst:h-10 rst:px-4 rst:py-2",lg:"rst:h-11 rst:rounded-md rst:px-8",icon:"rst:h-10 rst:w-10"}},compoundVariants:[{variant:"solid",colorScheme:"primary",className:"rst:bg-primary-600 rst:hover:bg-primary-700 rst:dark:bg-primary-600 rst:dark:hover:bg-primary-500 rst:text-primary-600-ink rst:hover:text-primary-700-ink rst:dark:hover:text-primary-500-ink"},{variant:"solid",colorScheme:"orange",className:"rst:bg-orange-600 rst:hover:bg-orange-700 rst:dark:bg-orange-600 rst:dark:hover:bg-orange-700 rst:text-orange-600-ink rst:hover:text-orange-700-ink rst:dark:hover:text-orange-700-ink"},{variant:"solid",colorScheme:"teal",className:"rst:bg-teal-600 rst:hover:bg-teal-500 rst:dark:bg-teal-600 rst:dark:hover:bg-teal-500 rst:text-teal-600-ink rst:hover:text-teal-500-ink rst:dark:hover:text-teal-500-ink"},{variant:"solid",colorScheme:"purple",className:"rst:bg-purple-600 rst:hover:bg-purple-700 rst:dark:bg-purple-600 rst:dark:hover:bg-purple-700 rst:text-purple-600-ink rst:hover:text-purple-700-ink rst:dark:hover:text-purple-700-ink"},{variant:"solid",colorScheme:"amber",className:"rst:bg-amber-400 rst:hover:bg-amber-500 rst:dark:bg-amber-500 rst:dark:hover:bg-amber-400 rst:text-amber-400-ink rst:hover:text-amber-500-ink rst:dark:text-amber-500-ink rst:dark:hover:text-amber-400-ink"},{variant:"solid",colorScheme:"success",className:"rst:bg-success-600 rst:hover:bg-success-700 rst:dark:bg-success-600 rst:dark:hover:bg-success-700 rst:text-success-600-ink rst:hover:text-success-700-ink rst:dark:hover:text-success-700-ink"},{variant:"solid",colorScheme:"error",className:"rst:bg-error-600 rst:hover:bg-error-700 rst:dark:bg-error-600 rst:dark:hover:bg-error-500 rst:text-error-600-ink rst:hover:text-error-700-ink rst:dark:hover:text-error-500-ink"},{variant:"solid",colorScheme:"neutral",className:"rst:bg-gray-600 rst:hover:bg-gray-700 rst:dark:bg-gray-600 rst:dark:hover:bg-gray-500 rst:text-gray-600-ink rst:hover:text-gray-700-ink rst:dark:hover:text-gray-500-ink"},{variant:"soft",colorScheme:"primary",className:"rst:bg-primary-50 rst:text-primary-700 rst:hover:bg-primary-100 rst:dark:bg-primary-900/40 rst:dark:text-primary-300 rst:dark:hover:bg-primary-900/60"},{variant:"soft",colorScheme:"orange",className:"rst:bg-orange-50 rst:text-orange-700 rst:hover:bg-orange-100 rst:dark:bg-orange-900/30 rst:dark:text-orange-300 rst:dark:hover:bg-orange-900/50"},{variant:"soft",colorScheme:"teal",className:"rst:bg-teal-100 rst:text-teal-800 rst:hover:bg-teal-200 rst:dark:bg-teal-900/30 rst:dark:text-teal-300 rst:dark:hover:bg-teal-900/50"},{variant:"soft",colorScheme:"purple",className:"rst:bg-purple-50 rst:text-purple-700 rst:hover:bg-purple-100 rst:dark:bg-purple-900/30 rst:dark:text-purple-300 rst:dark:hover:bg-purple-900/50"},{variant:"soft",colorScheme:"amber",className:"rst:bg-amber-50 rst:text-amber-800 rst:hover:bg-amber-100 rst:dark:bg-amber-900/30 rst:dark:text-amber-300 rst:dark:hover:bg-amber-900/50"},{variant:"soft",colorScheme:"success",className:"rst:bg-success-50 rst:text-success-700 rst:hover:bg-success-100 rst:dark:bg-success-900/30 rst:dark:text-success-300 rst:dark:hover:bg-success-900/50"},{variant:"soft",colorScheme:"error",className:"rst:bg-error-50 rst:text-error-700 rst:hover:bg-error-100 rst:dark:bg-error-900/30 rst:dark:text-error-300 rst:dark:hover:bg-error-900/50"},{variant:"soft",colorScheme:"neutral",className:"rst:bg-gray-100 rst:text-gray-700 rst:hover:bg-gray-200 rst:dark:bg-gray-800/50 rst:dark:text-gray-300 rst:dark:hover:bg-gray-800"},{variant:"outline",colorScheme:"primary",className:"rst:border-primary-600 rst:text-primary-600 rst:hover:bg-primary-50 rst:dark:border-primary-500 rst:dark:text-primary-400 rst:dark:hover:bg-primary-500/10"},{variant:"outline",colorScheme:"orange",className:"rst:border-orange-600 rst:text-orange-600 rst:hover:bg-orange-50 rst:dark:border-orange-500 rst:dark:text-orange-400 rst:dark:hover:bg-orange-500/10"},{variant:"outline",colorScheme:"teal",className:"rst:border-teal-600 rst:text-teal-600 rst:hover:bg-teal-50 rst:dark:border-teal-500 rst:dark:text-teal-400 rst:dark:hover:bg-teal-500/10"},{variant:"outline",colorScheme:"purple",className:"rst:border-purple-600 rst:text-purple-600 rst:hover:bg-purple-50 rst:dark:border-purple-500 rst:dark:text-purple-400 rst:dark:hover:bg-purple-500/10"},{variant:"outline",colorScheme:"amber",className:"rst:border-amber-600 rst:text-amber-600 rst:hover:bg-amber-50 rst:dark:border-amber-500 rst:dark:text-amber-400 rst:dark:hover:bg-amber-500/10"},{variant:"outline",colorScheme:"success",className:"rst:border-success-600 rst:text-success-600 rst:hover:bg-success-50 rst:dark:border-success-500 rst:dark:text-success-400 rst:dark:hover:bg-success-500/10"},{variant:"outline",colorScheme:"error",className:"rst:border-error-600 rst:text-error-600 rst:hover:bg-error-50 rst:dark:border-error-500 rst:dark:text-error-400 rst:dark:hover:bg-error-500/10"},{variant:"outline",colorScheme:"neutral",className:"rst:border-gray-300 rst:text-gray-700 rst:hover:bg-gray-50 rst:dark:border-gray-600 rst:dark:text-gray-300 rst:dark:hover:bg-gray-800/50"},{variant:"ghost",colorScheme:"primary",className:"rst:text-primary-600 rst:hover:bg-primary-500/10 rst:dark:text-primary-400 rst:dark:hover:bg-primary-400/15"},{variant:"ghost",colorScheme:"orange",className:"rst:text-orange-600 rst:hover:bg-orange-500/10 rst:dark:text-orange-400 rst:dark:hover:bg-orange-400/15"},{variant:"ghost",colorScheme:"teal",className:"rst:text-teal-600 rst:hover:bg-teal-500/10 rst:dark:text-teal-400 rst:dark:hover:bg-teal-400/15"},{variant:"ghost",colorScheme:"purple",className:"rst:text-purple-600 rst:hover:bg-purple-500/10 rst:dark:text-purple-400 rst:dark:hover:bg-purple-400/15"},{variant:"ghost",colorScheme:"amber",className:"rst:text-amber-600 rst:hover:bg-amber-500/10 rst:dark:text-amber-400 rst:dark:hover:bg-amber-400/15"},{variant:"ghost",colorScheme:"success",className:"rst:text-success-600 rst:hover:bg-success-500/10 rst:dark:text-success-400 rst:dark:hover:bg-success-400/15"},{variant:"ghost",colorScheme:"error",className:"rst:text-error-600 rst:hover:bg-error-500/10 rst:dark:text-error-400 rst:dark:hover:bg-error-400/15"},{variant:"ghost",colorScheme:"neutral",className:"rst:text-gray-600 rst:hover:bg-gray-500/10 rst:dark:text-gray-400 rst:dark:hover:bg-gray-400/15"},{variant:"link",colorScheme:"primary",className:"rst:text-primary-600 rst:hover:text-primary-700 rst:dark:text-primary-400 rst:dark:hover:text-primary-300"},{variant:"link",colorScheme:"orange",className:"rst:text-orange-600 rst:hover:text-orange-700 rst:dark:text-orange-400 rst:dark:hover:text-orange-300"},{variant:"link",colorScheme:"teal",className:"rst:text-teal-600 rst:hover:text-teal-700 rst:dark:text-teal-400 rst:dark:hover:text-teal-300"},{variant:"link",colorScheme:"purple",className:"rst:text-purple-600 rst:hover:text-purple-700 rst:dark:text-purple-400 rst:dark:hover:text-purple-300"},{variant:"link",colorScheme:"amber",className:"rst:text-amber-600 rst:hover:text-amber-700 rst:dark:text-amber-400 rst:dark:hover:text-amber-300"},{variant:"link",colorScheme:"success",className:"rst:text-success-600 rst:hover:text-success-700 rst:dark:text-success-400 rst:dark:hover:text-success-300"},{variant:"link",colorScheme:"error",className:"rst:text-error-600 rst:hover:text-error-700 rst:dark:text-error-400 rst:dark:hover:text-error-300"},{variant:"link",colorScheme:"neutral",className:"rst:text-gray-600 rst:hover:text-gray-900 rst:dark:text-gray-400 rst:dark:hover:text-gray-100"}],defaultVariants:{colorScheme:"primary",variant:"solid",size:"default"}});exports.buttonVariants=t;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("../../../node_modules/class-variance-authority/dist/index.cjs"),t=r.cva("rst:font-ui rst:inline-flex rst:items-center rst:justify-center rst:cursor-pointer rst:whitespace-nowrap rst:rounded-md rst:text-sm rst:font-medium rst:ring-offset-background rst:transition-colors rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:disabled:pointer-events-none rst:disabled:opacity-50",{variants:{variant:{solid:"rst:border rst:border-transparent rst:elevation-control",soft:"rst:border-transparent rst:shadow-none",outline:"rst:border rst:bg-transparent rst:elevation-control",ghost:"rst:border rst:border-transparent rst:bg-transparent",link:"rst:border-0 rst:bg-transparent rst:underline-offset-4 rst:hover:underline"},colorScheme:{primary:"",orange:"",teal:"",purple:"",amber:"",success:"",error:"",neutral:""},size:{xs:"rst:h-7 rst:rounded-sm rst:px-2 rst:text-xs",sm:"rst:h-9 rst:rounded-md rst:px-3",default:"rst:h-10 rst:px-4 rst:py-2",lg:"rst:h-11 rst:rounded-md rst:px-8",icon:"rst:h-10 rst:w-10"}},compoundVariants:[{variant:"solid",colorScheme:"primary",className:"rst:bg-primary-600 rst:hover:bg-primary-700 rst:dark:bg-primary-600 rst:dark:hover:bg-primary-500 rst:text-primary-600-ink rst:hover:text-primary-700-ink rst:dark:hover:text-primary-500-ink"},{variant:"solid",colorScheme:"orange",className:"rst:bg-orange-600 rst:hover:bg-orange-700 rst:dark:bg-orange-600 rst:dark:hover:bg-orange-700 rst:text-orange-600-ink rst:hover:text-orange-700-ink rst:dark:hover:text-orange-700-ink"},{variant:"solid",colorScheme:"teal",className:"rst:bg-teal-600 rst:hover:bg-teal-500 rst:dark:bg-teal-600 rst:dark:hover:bg-teal-500 rst:text-teal-600-ink rst:hover:text-teal-500-ink rst:dark:hover:text-teal-500-ink"},{variant:"solid",colorScheme:"purple",className:"rst:bg-purple-600 rst:hover:bg-purple-700 rst:dark:bg-purple-600 rst:dark:hover:bg-purple-700 rst:text-purple-600-ink rst:hover:text-purple-700-ink rst:dark:hover:text-purple-700-ink"},{variant:"solid",colorScheme:"amber",className:"rst:bg-amber-400 rst:hover:bg-amber-500 rst:dark:bg-amber-500 rst:dark:hover:bg-amber-400 rst:text-amber-400-ink rst:hover:text-amber-500-ink rst:dark:text-amber-500-ink rst:dark:hover:text-amber-400-ink"},{variant:"solid",colorScheme:"success",className:"rst:bg-success-600 rst:hover:bg-success-700 rst:dark:bg-success-600 rst:dark:hover:bg-success-700 rst:text-success-600-ink rst:hover:text-success-700-ink rst:dark:hover:text-success-700-ink"},{variant:"solid",colorScheme:"error",className:"rst:bg-error-600 rst:hover:bg-error-700 rst:dark:bg-error-600 rst:dark:hover:bg-error-500 rst:text-error-600-ink rst:hover:text-error-700-ink rst:dark:hover:text-error-500-ink"},{variant:"solid",colorScheme:"neutral",className:"rst:bg-gray-600 rst:hover:bg-gray-700 rst:dark:bg-gray-600 rst:dark:hover:bg-gray-500 rst:text-gray-600-ink rst:hover:text-gray-700-ink rst:dark:hover:text-gray-500-ink"},{variant:"soft",colorScheme:"primary",className:"rst:bg-primary-50 rst:text-primary-700 rst:hover:bg-primary-100 rst:dark:bg-primary-900/40 rst:dark:text-primary-300 rst:dark:hover:bg-primary-900/60"},{variant:"soft",colorScheme:"orange",className:"rst:bg-orange-50 rst:text-orange-700 rst:hover:bg-orange-100 rst:dark:bg-orange-900/30 rst:dark:text-orange-300 rst:dark:hover:bg-orange-900/50"},{variant:"soft",colorScheme:"teal",className:"rst:bg-teal-100 rst:text-teal-800 rst:hover:bg-teal-200 rst:dark:bg-teal-900/30 rst:dark:text-teal-300 rst:dark:hover:bg-teal-900/50"},{variant:"soft",colorScheme:"purple",className:"rst:bg-purple-50 rst:text-purple-700 rst:hover:bg-purple-100 rst:dark:bg-purple-900/30 rst:dark:text-purple-300 rst:dark:hover:bg-purple-900/50"},{variant:"soft",colorScheme:"amber",className:"rst:bg-amber-50 rst:text-amber-800 rst:hover:bg-amber-100 rst:dark:bg-amber-900/30 rst:dark:text-amber-300 rst:dark:hover:bg-amber-900/50"},{variant:"soft",colorScheme:"success",className:"rst:bg-success-50 rst:text-success-700 rst:hover:bg-success-100 rst:dark:bg-success-900/30 rst:dark:text-success-300 rst:dark:hover:bg-success-900/50"},{variant:"soft",colorScheme:"error",className:"rst:bg-error-50 rst:text-error-700 rst:hover:bg-error-100 rst:dark:bg-error-900/30 rst:dark:text-error-300 rst:dark:hover:bg-error-900/50"},{variant:"soft",colorScheme:"neutral",className:"rst:bg-gray-100 rst:text-gray-700 rst:hover:bg-gray-200 rst:dark:bg-gray-800/50 rst:dark:text-gray-300 rst:dark:hover:bg-gray-800"},{variant:"outline",colorScheme:"primary",className:"rst:border-primary-600 rst:text-primary-600 rst:hover:bg-primary-50 rst:dark:border-primary-500 rst:dark:text-primary-400 rst:dark:hover:bg-primary-500/10"},{variant:"outline",colorScheme:"orange",className:"rst:border-orange-600 rst:text-orange-600 rst:hover:bg-orange-50 rst:dark:border-orange-500 rst:dark:text-orange-400 rst:dark:hover:bg-orange-500/10"},{variant:"outline",colorScheme:"teal",className:"rst:border-teal-600 rst:text-teal-600 rst:hover:bg-teal-50 rst:dark:border-teal-500 rst:dark:text-teal-400 rst:dark:hover:bg-teal-500/10"},{variant:"outline",colorScheme:"purple",className:"rst:border-purple-600 rst:text-purple-600 rst:hover:bg-purple-50 rst:dark:border-purple-500 rst:dark:text-purple-400 rst:dark:hover:bg-purple-500/10"},{variant:"outline",colorScheme:"amber",className:"rst:border-amber-600 rst:text-amber-600 rst:hover:bg-amber-50 rst:dark:border-amber-500 rst:dark:text-amber-400 rst:dark:hover:bg-amber-500/10"},{variant:"outline",colorScheme:"success",className:"rst:border-success-600 rst:text-success-600 rst:hover:bg-success-50 rst:dark:border-success-500 rst:dark:text-success-400 rst:dark:hover:bg-success-500/10"},{variant:"outline",colorScheme:"error",className:"rst:border-error-600 rst:text-error-600 rst:hover:bg-error-50 rst:dark:border-error-500 rst:dark:text-error-400 rst:dark:hover:bg-error-500/10"},{variant:"outline",colorScheme:"neutral",className:"rst:border-gray-300 rst:text-gray-700 rst:hover:bg-gray-50 rst:dark:border-gray-600 rst:dark:text-gray-300 rst:dark:hover:bg-gray-800/50"},{variant:"ghost",colorScheme:"primary",className:"rst:text-primary-600 rst:hover:bg-primary-500/10 rst:dark:text-primary-400 rst:dark:hover:bg-primary-400/15"},{variant:"ghost",colorScheme:"orange",className:"rst:text-orange-600 rst:hover:bg-orange-500/10 rst:dark:text-orange-400 rst:dark:hover:bg-orange-400/15"},{variant:"ghost",colorScheme:"teal",className:"rst:text-teal-600 rst:hover:bg-teal-500/10 rst:dark:text-teal-400 rst:dark:hover:bg-teal-400/15"},{variant:"ghost",colorScheme:"purple",className:"rst:text-purple-600 rst:hover:bg-purple-500/10 rst:dark:text-purple-400 rst:dark:hover:bg-purple-400/15"},{variant:"ghost",colorScheme:"amber",className:"rst:text-amber-600 rst:hover:bg-amber-500/10 rst:dark:text-amber-400 rst:dark:hover:bg-amber-400/15"},{variant:"ghost",colorScheme:"success",className:"rst:text-success-600 rst:hover:bg-success-500/10 rst:dark:text-success-400 rst:dark:hover:bg-success-400/15"},{variant:"ghost",colorScheme:"error",className:"rst:text-error-600 rst:hover:bg-error-500/10 rst:dark:text-error-400 rst:dark:hover:bg-error-400/15"},{variant:"ghost",colorScheme:"neutral",className:"rst:text-gray-600 rst:hover:bg-gray-500/10 rst:dark:text-gray-400 rst:dark:hover:bg-gray-400/15"},{variant:"link",colorScheme:"primary",className:"rst:text-primary-600 rst:hover:text-primary-700 rst:dark:text-primary-400 rst:dark:hover:text-primary-300"},{variant:"link",colorScheme:"orange",className:"rst:text-orange-600 rst:hover:text-orange-700 rst:dark:text-orange-400 rst:dark:hover:text-orange-300"},{variant:"link",colorScheme:"teal",className:"rst:text-teal-600 rst:hover:text-teal-700 rst:dark:text-teal-400 rst:dark:hover:text-teal-300"},{variant:"link",colorScheme:"purple",className:"rst:text-purple-600 rst:hover:text-purple-700 rst:dark:text-purple-400 rst:dark:hover:text-purple-300"},{variant:"link",colorScheme:"amber",className:"rst:text-amber-600 rst:hover:text-amber-700 rst:dark:text-amber-400 rst:dark:hover:text-amber-300"},{variant:"link",colorScheme:"success",className:"rst:text-success-600 rst:hover:text-success-700 rst:dark:text-success-400 rst:dark:hover:text-success-300"},{variant:"link",colorScheme:"error",className:"rst:text-error-600 rst:hover:text-error-700 rst:dark:text-error-400 rst:dark:hover:text-error-300"},{variant:"link",colorScheme:"neutral",className:"rst:text-gray-600 rst:hover:text-gray-900 rst:dark:text-gray-400 rst:dark:hover:text-gray-100"}],defaultVariants:{colorScheme:"primary",variant:"solid",size:"default"}});exports.buttonVariants=t;
@@ -9,7 +9,9 @@ const e = /* @__PURE__ */ r(
9
9
  soft: "rst:border-transparent rst:shadow-none",
10
10
  outline: "rst:border rst:bg-transparent rst:elevation-control",
11
11
  ghost: "rst:border rst:border-transparent rst:bg-transparent",
12
- link: "rst:bg-transparent rst:underline-offset-4 rst:hover:underline"
12
+ // `border-0`: a <button> draws the browser's own border unless told
13
+ // otherwise, and an app without a reset would see it round a link.
14
+ link: "rst:border-0 rst:bg-transparent rst:underline-offset-4 rst:hover:underline"
13
15
  },
14
16
  colorScheme: {
15
17
  primary: "",
@@ -0,0 +1 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),o=require("react"),p=require("../../../lib/utils.cjs"),P=require("../../atoms/Button/Button.cjs"),R=require("./file-rules.cjs"),pt={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 gt({file:n,status:l}){const[M,q]=o.useState(!1),B=!M&&(n.type.startsWith("image/")||/\.(jpe?g|png|gif|webp|avif|heic|heif)$/i.test(n.name)),N=o.useCallback(L=>{if(!L)return;const w=URL.createObjectURL(n);return L.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?e.jsx("img",{ref:N,alt:"",onError:()=>q(!0),className:p.cn(x,"rst:object-cover",l==="rejected"&&"rst:opacity-50"),"data-file-thumb":"image"}):e.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:e.jsxs("svg",{viewBox:"0 0 24 24",width:"24",height:"24",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"3",y:"5",width:"18",height:"14",rx:"2"}),e.jsx("circle",{cx:"9",cy:"10",r:"1.6"}),e.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 ht({label:n,upload:l,onChange:M,accept:q="image/*",types:B,maxSize:N,maxFiles:x,validate:L,concurrency:w=3,helperText:E,errorMessage:T,disabled:u=!1,strings:K,className:Q}){const c={...pt,...K},D=o.useId(),I=`${D}-label`,G=`${D}-help`,V=`${D}-error`,[U,Y]=o.useState([]),d=o.useRef(U),Z=o.useRef(0),m=o.useRef(new Map),F=o.useRef(null),X=o.useRef(null),g=o.useRef({upload:l,onChange:M,concurrency:w,t:c,pump:()=>{}}),[O,tt]=o.useState({text:"",n:0}),v=o.useRef([]),y=o.useRef(null),$=o.useCallback((t,s=!1)=>{s&&(v.current=v.current.filter(r=>!r.count)),v.current.push({text:t,count:s}),!y.current&&(y.current=setTimeout(()=>{y.current=null;const r=v.current.map(i=>i.text).join(" ");v.current=[],tt(i=>({text:r,n:i.n+1}))},0))},[]),z=o.useRef(null),[rt,et]=o.useState(0),_=t=>{z.current=t,et(s=>s+1)};o.useLayoutEffect(()=>{const t=z.current;if(!t)return;z.current=null,(F.current?.querySelector(t)??F.current?.querySelector("[data-file-choose]"))?.focus()},[rt]);const j=o.useCallback(t=>{d.current=t,Y(t),g.current.onChange?.(t)},[]),b=o.useCallback((t,s)=>{d.current.some(r=>r.id===t)&&j(d.current.map(r=>r.id===t?{...r,...s}:r))},[j]);function k(){const{concurrency:t}=g.current,s=d.current.filter(i=>i.status==="uploading"||i.status==="processing").length,r=d.current.filter(i=>i.status==="queued").slice(0,Math.max(0,Math.max(1,t)-s));for(const i of r)st(i)}function st(t){const s=new AbortController;m.current.set(t.id,s),b(t.id,{status:"uploading",progress:null,reason:void 0});const r=()=>!s.signal.aborted&&m.current.get(t.id)===s,i={onProgress:a=>{if(!r())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:()=>{r()&&b(t.id,{status:"processing"})},signal:s.signal};new Promise(a=>a(g.current.upload(t.file,i))).then(a=>{if(!r())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(!r())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:M,concurrency:w,t:c,pump:k}}),o.useEffect(()=>{const t=m.current;return d.current.some(r=>(r.status==="uploading"||r.status==="processing")&&!t.has(r.id))&&(j(d.current.map(r=>(r.status==="uploading"||r.status==="processing")&&!t.has(r.id)?{...r,status:"queued",progress:null}:r)),g.current.pump()),()=>{for(const r of t.values())r.abort();t.clear(),y.current&&clearTimeout(y.current),y.current=null,v.current=[]}},[j]);const nt=B??R.acceptList(q),at=(t,s)=>{if(s.some(i=>R.sameFile(i.file,t)))return c.duplicate(t);const r=L?.(t);if(r)return r;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(u||t.length===0)return;const s=d.current.filter(a=>a.status!=="rejected"),r=[];for(const a of t){const f=at(a,[...s,...r.filter(h=>h.status!=="rejected")]);r.push({id:`${D}-file-${Z.current++}`,file:a,status:f?"rejected":"queued",progress:null,reason:f})}j([...d.current,...r]);const i=r.filter(a=>a.status==="queued").length;i&&$(c.added(i));for(const a of r)a.reason&&$(c.rejectedNotice(a.file.name,a.reason));k()},ot=t=>{if(u)return;const s=d.current,r=s.findIndex(a=>a.id===t);if(r<0)return;m.current.get(t)?.abort(),m.current.delete(t);const i=s[r+1]??s[r-1];j(s.filter(a=>a.id!==t)),$(c.removed(s[r].file.name)),_(i?`[data-file-remove="${i.id}"]`:"[data-file-choose]"),k()},ct=t=>{u||(b(t,{status:"queued",progress:null,reason:void 0}),_(`[data-file-remove="${t}"]`),k())},[it,A]=o.useState(0),C=t=>Array.from(t.dataTransfer?.types??[]).includes("Files"),ut={onDragEnter:t=>{C(t)&&(t.preventDefault(),A(s=>s+1))},onDragOver:t=>{C(t)&&(t.preventDefault(),t.dataTransfer.dropEffect=u?"none":"copy")},onDragLeave:t=>{C(t)&&A(s=>Math.max(0,s-1))},onDrop:t=>{C(t)&&(t.preventDefault(),A(0),J(Array.from(t.dataTransfer.files)))}},H=it>0&&!u,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?V:null].filter(Boolean).join(" ")||void 0;return e.jsxs("div",{ref:F,role:"group","aria-labelledby":I,className:p.cn("rst:font-ui rst:w-full",Q),"data-file-upload":"",children:[e.jsx("p",{id:I,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)]",u&&"rst:opacity-70"),children:n}),e.jsxs("div",{...ut,"data-file-drop":"","data-dragging":H?"":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",H?"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:[e.jsx(P.Button,{type:"button",variant:"outline",colorScheme:"neutral",size:"lg",className:p.cn("rst:px-5",u&&W),"aria-disabled":u||void 0,"aria-describedby":dt,onClick:()=>X.current?.click(),"data-file-choose":"",startIcon:e.jsx("svg",{...S,"aria-hidden":"true",children:e.jsx("path",{d:"M12 16V4M7 9l5-5 5 5M5 20h14"})}),children:c.choose}),e.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:H?c.dropHere:c.dropHint}),e.jsx("input",{ref:X,type:"file",hidden:!0,tabIndex:-1,accept:q,multiple:x!==1,disabled:u,onChange:t=>{J(Array.from(t.currentTarget.files??[])),t.currentTarget.value=""},"data-file-input":""})]}),E&&e.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&&e.jsx("p",{id:V,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}),U.length>0&&e.jsx("ul",{className:"rst:mt-3 rst:flex rst:flex-col rst:gap-2","aria-labelledby":I,children:U.map(t=>{const s=t.status==="failed"||t.status==="rejected",r=t.status==="uploading"||t.status==="processing"||t.status==="queued";return e.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:[e.jsx(gt,{file:t.file,status:t.status}),e.jsxs("div",{className:"rst:min-w-0 rst:flex-1 rst:text-left",children:[e.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}),e.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"&&e.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:e.jsx("path",{d:"M20 6 9 17l-5-5"})}),e.jsxs("span",{children:[e.jsx("span",{className:"rst:tabular-nums",children:c.size(t.file.size)}),e.jsx("span",{"aria-hidden":"true",children:" · "}),lt(t)]})]}),r&&e.jsx("div",{role:"progressbar","aria-label":t.file.name,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":t.status==="uploading"&&t.progress!==null?Math.round(t.progress*100):void 0,className:p.cn("rst:mt-1.5 rst:h-1.5 rst:overflow-hidden rst:rounded-full rst:bg-gray-200 rst:dark:bg-gray-700",t.status==="uploading"&&t.progress===null&&"rst:animate-pulse rst:motion-reduce:animate-none"),"data-file-progress":"",children:e.jsx("div",{className:p.cn("rst:h-full rst:origin-left rst:rounded-full rst:bg-primary-600 rst:transition-transform rst:motion-reduce:transition-none rst:dark:bg-primary-400",t.status==="processing"&&"rst:animate-pulse rst:motion-reduce:animate-none"),style:{transform:`scaleX(${t.status==="processing"?1:t.status==="uploading"?t.progress??0:0})`}})})]}),t.status==="failed"&&e.jsx(P.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:p.cn("rst:h-11 rst:w-11",u&&W),"aria-disabled":u||void 0,"aria-label":c.retry(t.file.name),onClick:()=>ct(t.id),"data-file-retry":t.id,children:e.jsxs("svg",{...S,"aria-hidden":"true",children:[e.jsx("path",{d:"M3 12a9 9 0 1 0 3-6.7L3 8"}),e.jsx("path",{d:"M3 3v5h5"})]})}),e.jsx(P.Button,{type:"button",variant:"ghost",colorScheme:"neutral",size:"icon",className:p.cn("rst:h-11 rst:w-11",u&&W),"aria-disabled":u||void 0,"aria-label":c.remove(t.file.name),onClick:()=>ot(t.id),"data-file-remove":t.id,children:e.jsx("svg",{...S,"aria-hidden":"true",children:e.jsx("path",{d:"M18 6 6 18M6 6l12 12"})})})]},t.id)})}),e.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=ht;
@@ -0,0 +1,111 @@
1
+ import { ReactNode } from 'react';
2
+ /** Where a file is: waiting its turn, on its way, being handled by the app's server, or finished one way or another. */
3
+ export type FileUploadStatus = "queued" | "uploading" | "processing" | "done" | "failed" | "rejected";
4
+ /** What FileUpload hands `upload` with each file. */
5
+ export interface FileUploadContext {
6
+ /** Report how much is sent, from 0 to 1. Without it the bar runs without a figure. */
7
+ onProgress: (fraction: number) => void;
8
+ /** Say the bytes are up and the app's server is working on them. */
9
+ onProcessing: () => void;
10
+ /** Aborts when the person removes the file, or the component goes away. */
11
+ signal: AbortSignal;
12
+ }
13
+ /** A file in the list, as `onChange` reports it. */
14
+ export interface FileUploadItem<R = unknown> {
15
+ id: string;
16
+ file: File;
17
+ status: FileUploadStatus;
18
+ /** From 0 to 1 once the app reports any; `null` before. */
19
+ progress: number | null;
20
+ /** What `upload` resolved with, once `done`: an id to send with the form, say. */
21
+ result?: R;
22
+ /** Why it `failed`, or why it was `rejected`. */
23
+ reason?: string;
24
+ }
25
+ /** Every word FileUpload shows or says. Pass any of them in `strings` to replace it. */
26
+ export interface FileUploadStrings {
27
+ choose: string;
28
+ /** Beside the button where a pointer can drag; hidden on touch screens. */
29
+ dropHint: string;
30
+ /** While files are dragged over. */
31
+ dropHere: string;
32
+ queued: string;
33
+ uploading: (percent: number | null) => string;
34
+ processing: string;
35
+ done: string;
36
+ failed: string;
37
+ rejected: string;
38
+ remove: (name: string) => string;
39
+ retry: (name: string) => string;
40
+ wrongType: (file: File) => string;
41
+ tooLarge: (file: File, maxSize: number) => string;
42
+ tooMany: (maxFiles: number) => string;
43
+ duplicate: (file: File) => string;
44
+ /** The reason shown when `upload` rejects. */
45
+ uploadError: (error: unknown) => string;
46
+ size: (bytes: number) => string;
47
+ added: (count: number) => string;
48
+ progress: (done: number, total: number) => string;
49
+ failedNotice: (name: string, reason: string) => string;
50
+ rejectedNotice: (name: string, reason: string) => string;
51
+ removed: (name: string) => string;
52
+ }
53
+ export interface FileUploadProps<R = unknown> {
54
+ /** What the files are: "Photos". Names the group. */
55
+ label: string;
56
+ /**
57
+ * Sends one file, and resolves when it's done with whatever the app needs
58
+ * later (an id, a URL). Rejects with an Error whose message says why, and
59
+ * that message is shown. FileUpload never sends anything itself.
60
+ */
61
+ upload: (file: File, context: FileUploadContext) => Promise<R>;
62
+ /** Every change to the list, with each file's status and result. */
63
+ onChange?: (items: FileUploadItem<R>[]) => void;
64
+ /**
65
+ * What the picker offers. Defaults to `"image/*"`, and on phones it should
66
+ * stay that way. Android's Chrome opens the system photo picker, with its
67
+ * albums and cloud photos, only when every accepted type is an image (or
68
+ * video) type and there's no `capture`; an extension among them gets a plain
69
+ * file browser. Exact types are riskier than they look: a picker that
70
+ * filters by the type a photo reports can hide photos whose reported type
71
+ * doesn't match. Narrow what's allowed with `types`, which is checked after
72
+ * picking.
73
+ */
74
+ accept?: string;
75
+ /**
76
+ * The types allowed, checked on every picked or dropped file: `image/jpeg`,
77
+ * `image/*` or `.png`. Defaults to `accept`. A file that arrives without a
78
+ * type is judged by its extension.
79
+ */
80
+ types?: string[];
81
+ /** The largest file allowed, in bytes. */
82
+ maxSize?: number;
83
+ /** How many files the list may hold, not counting the ones turned away. */
84
+ maxFiles?: number;
85
+ /**
86
+ * The app's own rule: return why a file can't be added, or nothing. Runs
87
+ * before the type and size rules, so its reason is the one shown.
88
+ */
89
+ validate?: (file: File) => string | null | undefined;
90
+ /** How many files upload at once. The rest wait. */
91
+ concurrency?: number;
92
+ helperText?: ReactNode;
93
+ /** A problem with the field as a whole, from the form: "Add at least one photo." */
94
+ errorMessage?: string;
95
+ disabled?: boolean;
96
+ strings?: Partial<FileUploadStrings>;
97
+ className?: string;
98
+ }
99
+ /**
100
+ * Picks files, uploads each one through the app, and shows where each stands.
101
+ *
102
+ * The app owns every request: `upload` is called once per file, with a way to
103
+ * report progress, a way to say the server is working on it, and a signal
104
+ * that aborts when the file is removed. This owns picking, the rules, the
105
+ * list, each file's state, the keyboard and what a screen reader hears.
106
+ *
107
+ * Choosing is a button, which works everywhere; dragging files onto the field
108
+ * is an addition for a pointer, never the only way in.
109
+ */
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;
111
+ export { FileUpload };