@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.
- package/README.md +153 -38
- package/dist/components/molecules/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/molecules/FileUpload/FileUpload.d.ts +20 -3
- package/dist/components/molecules/FileUpload/FileUpload.js +266 -216
- package/dist/components/molecules/FileUpload/file-upload-rejection.cjs +1 -0
- package/dist/components/molecules/FileUpload/file-upload-rejection.d.ts +24 -0
- package/dist/components/molecules/FileUpload/file-upload-rejection.js +11 -0
- package/dist/components/molecules/LiquidTabs/LiquidCount.d.ts +2 -2
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.d.ts +10 -49
- package/dist/components/molecules/LiquidTabs/LiquidTabPanel.js +5 -28
- package/dist/components/molecules/LiquidTabs/LiquidTabs.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/LiquidTabs.d.ts +13 -40
- package/dist/components/molecules/LiquidTabs/LiquidTabs.js +5 -95
- package/dist/components/molecules/LiquidTabs/liquid-count.d.ts +7 -5
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.cjs +1 -1
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.d.ts +4 -9
- package/dist/components/molecules/LiquidTabs/liquid-tab-id.js +3 -4
- package/dist/components/molecules/Tabs/TabPanel.cjs +1 -0
- package/dist/components/molecules/Tabs/TabPanel.d.ts +53 -0
- package/dist/components/molecules/Tabs/TabPanel.js +32 -0
- package/dist/components/molecules/Tabs/Tabs.cjs +1 -0
- package/dist/components/molecules/Tabs/Tabs.d.ts +118 -0
- package/dist/components/molecules/Tabs/Tabs.js +145 -0
- package/dist/components/molecules/Tabs/tab-id.cjs +1 -0
- package/dist/components/molecules/Tabs/tab-id.d.ts +13 -0
- package/dist/components/molecules/Tabs/tab-id.js +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/meta.json +3 -3
- package/dist/roster.cjs +1 -1
- package/dist/roster.css +1 -1
- package/dist/roster.js +150 -142
- 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`, `
|
|
273
|
-
| `--roster-radius-xl` | `0.75rem` | `Card`, `
|
|
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
|
|
481
|
-
which defaults to `--roster-primary-500-ink`,
|
|
482
|
-
fill. A rule naming `rst:text-white` on a tab
|
|
483
|
-
`[role="tab"][aria-selected="true"]` still does. If
|
|
484
|
-
`--roster-lt-pill`, set `--roster-lt-text-active` to
|
|
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,
|
|
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 `
|
|
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
|
-
###
|
|
2039
|
+
### Tabs
|
|
1987
2040
|
|
|
1988
|
-
A tab strip
|
|
1989
|
-
that wrap, Home and End, and `activation="manual"` for tabs that
|
|
1990
|
-
switch.
|
|
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
|
-
|
|
1993
|
-
|
|
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
|
-
<
|
|
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,
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
|
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.** `
|
|
2027
|
-
with it by construction: its `id` is the tab's `panelId`, and
|
|
2028
|
-
`aria-labelledby` points at the tab through `
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
-
<
|
|
2035
|
-
<
|
|
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
|
-
<
|
|
2039
|
-
<
|
|
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
|
|
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` |
|
|
2214
|
-
| `LiquidTabs` |
|
|
2215
|
-
| `LiquidTabPanel` |
|
|
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.
|
|
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 };
|