@blakesteve/roster 5.14.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.
Files changed (33) hide show
  1. package/README.md +153 -38
  2. package/dist/components/molecules/FileUpload/FileUpload.cjs +1 -1
  3. package/dist/components/molecules/FileUpload/FileUpload.d.ts +20 -3
  4. package/dist/components/molecules/FileUpload/FileUpload.js +266 -216
  5. package/dist/components/molecules/FileUpload/file-upload-rejection.cjs +1 -0
  6. package/dist/components/molecules/FileUpload/file-upload-rejection.d.ts +24 -0
  7. package/dist/components/molecules/FileUpload/file-upload-rejection.js +11 -0
  8. package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
  9. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
  10. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
  11. package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
  12. package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
  13. package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
  14. package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
  15. package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
  16. package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
  17. package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
  18. package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
  19. package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
  20. package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
  21. package/dist/components/molecules/Tabs/TabPanel.js +32 -0
  22. package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
  23. package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
  24. package/dist/components/molecules/Tabs/Tabs.js +145 -0
  25. package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
  26. package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
  27. package/dist/components/molecules/Tabs/tab-id.js +6 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/meta.json +3 -3
  30. package/dist/roster.cjs +1 -1
  31. package/dist/roster.css +1 -1
  32. package/dist/roster.js +150 -142
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -268,9 +268,9 @@ changes nothing.
268
268
  | token | default | reshapes |
269
269
  | ----- | ------- | -------- |
270
270
  | `--roster-radius-sm` | `0.25rem` | `Button` `xs`, `Checkbox` `sm` and `md`, `InlineCode`, `Link`, `MultiSelect`'s chip dismiss, the dismiss and reveal buttons in `Alert`, `Toast` and `PasswordInput` |
271
- | `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table` |
272
- | `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `LiquidTabs`, `MatchupCard`, `Navbar`'s mobile panel, `Skeleton` blocks (unless `--roster-skeleton-radius` is set) |
273
- | `--roster-radius-xl` | `0.75rem` | `Card`, `LiquidTabs`' `pill` strip |
271
+ | `--roster-radius-md` | `0.375rem` | `Button`, `Input`, `Textarea`, `Select`, `MultiSelect`, `Badge`, `Avatar` and its popover, `Checkbox` `lg`, `CheckboxGroup`'s panel, `Disclosure`, `Dialog`'s and `Sheet`'s close buttons, `Navbar`'s links and menu, popup panels, `Table`, a plain `Tabs`' focus ring |
272
+ | `--roster-radius-lg` | `0.5rem` | `Alert`, `Toast`, `Tooltip`, `CallToAction`, `EmptyState`, `ErrorState`, `Tabs`' liquid variant, `MatchupCard`, `Navbar`'s mobile panel, `Skeleton` blocks (unless `--roster-skeleton-radius` is set) |
273
+ | `--roster-radius-xl` | `0.75rem` | `Card`, the liquid `Tabs`' `pill` strip |
274
274
  | `--roster-radius-2xl` | `1rem` | `Dialog`, the top of `Sheet` |
275
275
  | `--roster-radius-3xl` | `1.5rem` | nothing yet; wired for the first component that uses it |
276
276
  | `--roster-border-width` | `1px` | every 1px edge: borders, `divide-y`, the ring `Select`'s trigger and the anchored panels draw their edge with, and the rules in `Accordion` and `LabeledDivider` |
@@ -477,11 +477,12 @@ Replace the rule with the token:
477
477
  :root { --roster-primary-600-ink: #10142e; }
478
478
  ```
479
479
 
480
- `LiquidTabs` followed later: its active label reads `--roster-lt-text-active`,
481
- which defaults to `--roster-primary-500-ink`, the ink of the pill's default
482
- fill. A rule naming `rst:text-white` on a tab stops matching; a rule on
483
- `[role="tab"][aria-selected="true"]` still does. If you repaint the pill with
484
- `--roster-lt-pill`, set `--roster-lt-text-active` to match it.
480
+ `LiquidTabs` followed later (now `Tabs`' liquid variant): its active label
481
+ reads `--roster-lt-text-active`, which defaults to `--roster-primary-500-ink`,
482
+ the ink of the pill's default fill. A rule naming `rst:text-white` on a tab
483
+ stops matching; a rule on `[role="tab"][aria-selected="true"]` still does. If
484
+ you repaint the pill with `--roster-lt-pill`, set `--roster-lt-text-active` to
485
+ match it.
485
486
 
486
487
  **If you pass a foreground through `className`, add the hover modifier.** Solid
487
488
  variants now carry `hover:text-*`, and `tailwind-merge` resolves conflicts per
@@ -565,7 +566,7 @@ a click. A small tracker marks those elements: keyboard mode starts on a navigat
565
566
  arrows, Escape, Enter, Space, Home, End, PageUp, PageDown) and ends on pointer
566
567
  input, and it marks only what Chromium's own `:focus-visible` already matches,
567
568
  so Chromium's rings are unchanged. It runs while a component that moves focus
568
- is mounted (Menu, the select family, Dialog, Sheet, RadioGroup, LiquidTabs,
569
+ is mounted (Menu, the select family, Dialog, Sheet, RadioGroup, Tabs,
569
570
  Slider and the rest), on one set of document listeners.
570
571
 
571
572
  Your own components' rings come from your Tailwind, not Roster's, so they
@@ -1382,7 +1383,7 @@ What it promises:
1382
1383
  sideways at 320px. Nothing animates as an item leaves.
1383
1384
 
1384
1385
  Counts for a tab need no API: the app holds the items, so it's
1385
- `items.length`, passed as a `LiquidTabs` tab's `count`. `MediaPreview` is the
1386
+ `items.length`, passed as a `Tabs` tab's `count`. `MediaPreview` is the
1386
1387
  thumbnail to use in an item: when the image can't render (an expired link, a
1387
1388
  format the browser can't draw), it falls back to a link to the original in the
1388
1389
  same footprint.
@@ -1427,6 +1428,8 @@ import { FileUpload } from "@blakesteve/roster";
1427
1428
  - **Resolve** with what the form needs later, an id say; it comes back as that
1428
1429
  file's `result` in `onChange`. **Reject** with an Error whose message says
1429
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.
1430
1433
 
1431
1434
  Each file is `queued`, `uploading`, `processing`, `done`, `failed` or
1432
1435
  `rejected`. A few upload at once (`concurrency`, 3 by default) and the rest
@@ -1440,6 +1443,56 @@ turned away stays in the list with its reason and is never sent. A file that
1440
1443
  arrives with no type, as some cloud-backed picks on Android do, is judged by
1441
1444
  its extension.
1442
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
+
1443
1496
  **Phones first.** `accept` defaults to `"image/*"`, with `multiple` and no
1444
1497
  `capture`. That combination is what opens Android's system photo picker, with
1445
1498
  albums and cloud photos; a list of exact types or an extension gets a file
@@ -1983,17 +2036,54 @@ clicks and focus are one listener each on the row. Measured at 1,000 options
1983
2036
  in Chromium: mounting takes about 30ms longer than the default mode, a key
1984
2037
  moves focus within a millisecond, and the frame after it lands in about 7ms.
1985
2038
 
1986
- ### LiquidTabs
2039
+ ### Tabs
1987
2040
 
1988
- A tab strip with a sliding pill, on the WAI tabs pattern: one tab stop, arrows
1989
- that wrap, Home and End, and `activation="manual"` for tabs that are costly to
1990
- switch. `LiquidNav` is the same strip as a `nav` of links.
2041
+ A controlled tab strip, on the WAI tabs pattern in every variant: one tab
2042
+ stop, arrows that wrap, Home and End, and `activation="manual"` for tabs that
2043
+ are costly to switch. Every tab is `type="button"`, so a strip inside a form
2044
+ never submits it.
1991
2045
 
1992
- **Counts.** A tab or a nav link takes `count`, shown in a small mark after its
1993
- label:
2046
+ ```tsx
2047
+ import { Tabs } from "@blakesteve/roster";
2048
+
2049
+ <Tabs aria-label="Project" tabs={[{ id: "overview", label: "Overview" }, …]} activeTab={tab} onChange={setTab} />
2050
+ ```
2051
+
2052
+ **Plain, by default.** The selected tab is underlined, 2px, on a 1px rule
2053
+ that runs the strip's width. The other tabs are muted and darken under the
2054
+ pointer, with a lighter underline as a hint. The weight never changes, so
2055
+ selecting a tab never moves the row. Keyboard focus is Roster's ring. The
2056
+ underline is a border, so it stays in forced colors, and only the selected
2057
+ tab draws one there. Its colors are tokens, each set in light and dark:
2058
+
2059
+ | token | light | dark | paints |
2060
+ | ----- | ----- | ---- | ------ |
2061
+ | `--roster-tabs-indicator` | `primary-500` | `primary-400` | the selected tab's underline |
2062
+ | `--roster-tabs-indicator-hover` | `gray-300` | `gray-600` | the underline under the pointer |
2063
+ | `--roster-tabs-border` | `gray-200` | `gray-800` | the rule along the strip |
2064
+ | `--roster-tabs-text` | `gray-500` | `gray-400` | a tab at rest |
2065
+ | `--roster-tabs-text-hover` | `gray-900` | `gray-100` | a tab under the pointer |
2066
+ | `--roster-tabs-text-active` | `gray-900` | `gray-100` | the selected tab |
2067
+
2068
+ On Roster's own surfaces the underline clears 3:1 against the page (6.37:1 in
2069
+ light, 7.31:1 in dark), a label at rest 4.5:1 (5.99:1 and 7.83:1), and a
2070
+ count's number 4.5:1 against its mark (5.07:1 at the least). `fullWidth` gives
2071
+ every tab an equal share of the row, and `size="lg"` makes each at least 44px
2072
+ tall.
2073
+
2074
+ **Liquid.** `variant="liquid"` draws the sliding pill instead:
2075
+ `liquidStyle="pill"` (the default), a floating pill in a padded strip, or
2076
+ `"filled"`, where the selected tab fills its cell. The pill follows
2077
+ `activeTab`, never the click, and moves in two phases, a stretch and a
2078
+ settle, or not at all under reduced motion. Its colors are the
2079
+ `--roster-lt-*` tokens. `liquidStyle` is a type error without
2080
+ `variant="liquid"`.
2081
+
2082
+ **Counts.** A tab takes `count`, in every variant, and so does a `LiquidNav` link, shown
2083
+ in a small mark after its label:
1994
2084
 
1995
2085
  ```tsx
1996
- <LiquidTabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
2086
+ <Tabs tabs={[{ id: "queue", label: "Queue", count: queue.length }, …]} … />
1997
2087
  ```
1998
2088
 
1999
2089
  The count is part of the name, "Queue, 12 items" rather than "Queue12" or
@@ -2014,29 +2104,29 @@ labels={{ count: (n) => (n === 1 ? "1 game" : `${n.toLocaleString()} games`) }}
2014
2104
  shorten the mark, pass `labels.countMark` (`(n) => "12K"`); the name stays
2015
2105
  whole, since a screen reader has no width to save.
2016
2106
  - **The mark is drawn from the tab's own text color:** its number in that
2017
- color on a 12% tint of it. So it follows the tab through every state, both
2018
- variants and both themes, and through any palette set on the
2019
- `--roster-lt-*` tokens, and its number clears 4.5:1 in every state on
2020
- Roster's own (4.88:1 at the least). `Badge` and `Pill` each draw from a fixed
2107
+ color on a 12% tint of it. So it follows the tab through every state, every
2108
+ variant and both themes, and through any palette set on the tabs' tokens,
2109
+ and its number clears 4.5:1 in every state on Roster's own (4.88:1 at the
2110
+ least, on the liquid pill). `Badge` and `Pill` each draw from a fixed
2021
2111
  palette, which on the selected tab would be a second chip sitting on the
2022
2112
  pill.
2023
- - **The pill follows,** re-measuring when a count widens its tab or one
2024
- before it.
2113
+ - **The liquid pill follows,** re-measuring when a count widens its tab or
2114
+ one before it.
2025
2115
 
2026
- **The panel.** `LiquidTabPanel` is the `role="tabpanel"` a tab controls, paired
2027
- with it by construction: its `id` is the tab's `panelId`, and its
2028
- `aria-labelledby` points at the tab through `liquidTabId`, so the panel takes
2029
- the tab's name, count included. Give the `LiquidTabs` an `id` to pair it with.
2030
- One panel that swaps its content, or a panel per tab:
2116
+ **The panel.** `TabPanel` is the `role="tabpanel"` a tab controls, in every
2117
+ variant, paired with it by construction: its `id` is the tab's `panelId`, and
2118
+ its `aria-labelledby` points at the tab through `tabId`, so the panel takes the
2119
+ tab's name, count included. Give the `Tabs` an `id` to pair it with. One panel
2120
+ that swaps its content, or a panel per tab:
2031
2121
 
2032
2122
  ```tsx
2033
2123
  // One panel: every tab carries the same panelId, and `tab` is the selected one.
2034
- <LiquidTabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
2035
- <LiquidTabPanel tabsId="lists" id="lists-panel" tab={list}>…</LiquidTabPanel>
2124
+ <Tabs id="lists" tabs={tabs.map((t) => ({ ...t, panelId: "lists-panel" }))} activeTab={list} onChange={setList} />
2125
+ <TabPanel tabsId="lists" id="lists-panel" tab={list}>…</TabPanel>
2036
2126
 
2037
2127
  // A panel per tab: each its own panelId, and every panel gets activeTab.
2038
- <LiquidTabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</LiquidTabPanel>
2039
- <LiquidTabPanel tabsId="views" id="views-list" tab="list" activeTab={view} keepMounted>…</LiquidTabPanel>
2128
+ <TabPanel tabsId="views" id="views-board" tab="board" activeTab={view}>…</TabPanel>
2129
+ <TabPanel tabsId="views" id="views-list" tab="list" activeTab={view} keepMounted>…</TabPanel>
2040
2130
  ```
2041
2131
 
2042
2132
  Per tab, a panel that isn't selected stays in the page, hidden, so every tab's
@@ -2048,6 +2138,22 @@ can reach what they just selected; it leaves the tab order once its content
2048
2138
  holds a link, a button or a field, rechecked as the content changes. Pass
2049
2139
  `tabIndex` to decide it yourself.
2050
2140
 
2141
+ **From `LiquidTabs`.** `LiquidTabs` is deprecated, and keeps working,
2142
+ unchanged: it renders `Tabs` with the liquid variant, the same markup, styles
2143
+ and test ids it always has. To move, rename it and add the variant; a
2144
+ `variant="filled"` moves to `liquidStyle`:
2145
+
2146
+ ```tsx
2147
+ <LiquidTabs … /> → <Tabs variant="liquid" … />
2148
+ <LiquidTabs variant="filled" … /> → <Tabs variant="liquid" liquidStyle="filled" … />
2149
+ <LiquidTabPanel … /> → <TabPanel … />
2150
+ liquidTabId(id, tab) → tabId(id, tab)
2151
+ ```
2152
+
2153
+ Nothing else changes: the props, ids, keys, pill and `data-testid` hooks are
2154
+ the same. `LiquidTabsLabels` is now `TabsLabels`, and the old name still works.
2155
+ `LiquidNav`, the liquid strip as a `nav` of links, is unchanged.
2156
+
2051
2157
  ### Components that render links
2052
2158
 
2053
2159
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -2098,8 +2204,8 @@ Dialog or Sheet), alongside `Pagination`, the loading system and DataTable's
2098
2204
  instead, and 5.8.0 adds the ones that were missing: MultiSelect's
2099
2205
  `countLabel`, DataTable's `emptyMessage` and MediaPreview's `newTabLabel`, next
2100
2206
  to the existing `dismissLabel`, `placeholder`, `removeLabel`, `prevLabel` and
2101
- the like. `LiquidTabs` and `LiquidNav` take `labels` for the words a count
2102
- says. Words that take a number or a name are functions, so a language
2207
+ the like. `Tabs`, `LiquidTabs` and `LiquidNav` take `labels` for the words a
2208
+ count says. Words that take a number or a name are functions, so a language
2103
2209
  whose word changes with the count can say so. An entry passed as `undefined`
2104
2210
  (a catalog missing a key) keeps its English rather than leaving a control
2105
2211
  unnamed.
@@ -2210,13 +2316,13 @@ function App() {
2210
2316
  | `DescriptionList` | Label and value pairs as a real `<dl>`: inline, stacked, or split |
2211
2317
  | `EmptyState` | Zero-data placeholder with icon and action slot; `headingLevel` sets its title's `h1` to `h6` |
2212
2318
  | `ErrorState` | Error display with retry action |
2213
- | `LiquidNav` | `LiquidTabs`' strip as a `nav` of links; takes a router's link component |
2214
- | `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, counts, 44px `lg` size (see [LiquidTabs](#liquidtabs)) |
2215
- | `LiquidTabPanel` | The panel a `LiquidTabs` tab controls, named for its tab by construction; one swapping panel or one per tab |
2319
+ | `LiquidNav` | The liquid tab strip as a `nav` of links; takes a router's link component |
2320
+ | `LiquidTabs` | Deprecated: `Tabs` with `variant="liquid"`, which it renders (see [Tabs](#tabs)) |
2321
+ | `LiquidTabPanel` | Deprecated: the old name of `TabPanel` |
2216
2322
  | `MatchupCard` | Head-to-head comparison card |
2217
2323
  | `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
2218
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)) |
2219
- | `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)) |
2220
2326
  | `Pagination` | Page numbers with gaps, or a compact page picker on narrow screens; buttons or real links, 44px targets |
2221
2327
  | `SkeletonCard`, `SkeletonTableRow`, `SkeletonAvatar`, `SkeletonStat` | Skeleton presets in the real component's box |
2222
2328
  | `ProgressField` | Progress with a label and value on one baseline and a detail line: the form to reach for |
@@ -2225,6 +2331,8 @@ function App() {
2225
2331
  | `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
2226
2332
  | `Slider` | A value along a track, starting in the middle or not set on request; named snap-point chips, a visible number, the full keyboard, 44px targets (see [Slider](#slider)) |
2227
2333
  | `RangeSlider` | A low and a high on one track: two thumbs that meet and never cross, each its own slider to the keyboard (see [RangeSlider](#rangeslider)) |
2334
+ | `Tabs` | Tab strip, plain (an underline) or liquid (a sliding pill); arrow keys, one tab stop, counts, 44px `lg` size (see [Tabs](#tabs)) |
2335
+ | `TabPanel` | The panel a `Tabs` tab controls, named for its tab by construction; one swapping panel or one per tab |
2228
2336
  | `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
2229
2337
 
2230
2338
  ### Organisms
@@ -2390,6 +2498,13 @@ npm run check:prefix
2390
2498
  npm run check:classes-emit
2391
2499
  ```
2392
2500
 
2501
+ In CI the Storybook projects run inside Playwright's own image for the locked
2502
+ version (`mcr.microsoft.com/playwright:v<version>-noble` in `ci.yml`), so no
2503
+ browser or system library is installed from Ubuntu's mirrors. Bumping
2504
+ `playwright` means bumping that tag too: `src/ci-workflow.test.ts` fails until
2505
+ they match. Every job and test step has a time limit, so a stall fails with
2506
+ GitHub's "has timed out" message instead of running for hours.
2507
+
2393
2508
  Three guards are worth knowing about, because the bugs they catch cannot be
2394
2509
  reproduced from inside this repo.
2395
2510
 
@@ -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. FileUpload never sends anything itself.
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 };