@scalewing/react 1.44.0 → 1.46.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 (49) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +51 -2
  3. package/dist/components/Article.d.ts +19 -2
  4. package/dist/components/Article.js +9 -5
  5. package/dist/components/Button.d.ts +14 -1
  6. package/dist/components/Button.js +5 -2
  7. package/dist/components/FileField.d.ts +18 -2
  8. package/dist/components/FileField.js +26 -22
  9. package/dist/components/ListGroup.d.ts +8 -3
  10. package/dist/components/ListGroup.js +13 -5
  11. package/dist/components/ListRow.d.ts +11 -3
  12. package/dist/components/ListRow.js +6 -4
  13. package/dist/components/Select.d.ts +8 -5
  14. package/dist/components/Select.js +7 -4
  15. package/dist/components/file-field/ChosenFile.d.ts +45 -0
  16. package/dist/components/file-field/ChosenFile.js +48 -0
  17. package/dist/components/file-field/file-field-checks.d.ts +5 -2
  18. package/dist/components/file-field/file-field-checks.js +7 -2
  19. package/dist/components/list/ListRowContent.d.ts +5 -1
  20. package/dist/components/list/ListRowContent.js +4 -2
  21. package/dist/components/select/select-naming.d.ts +34 -0
  22. package/dist/components/select/select-naming.js +22 -0
  23. package/dist/components/steps/Step.d.ts +10 -1
  24. package/dist/components/steps/Step.js +15 -19
  25. package/dist/components/steps/StepTitleLine.d.ts +19 -0
  26. package/dist/components/steps/StepTitleLine.js +13 -0
  27. package/dist/components/steps/StepTitleWords.d.ts +27 -0
  28. package/dist/components/steps/StepTitleWords.js +21 -0
  29. package/dist/components/steps/assert-step.d.ts +7 -0
  30. package/dist/components/steps/assert-step.js +19 -0
  31. package/dist/css/css-button-width.d.ts +11 -0
  32. package/dist/css/css-button-width.js +36 -0
  33. package/dist/css/css-button.js +3 -0
  34. package/dist/css/css-file-field-compact.d.ts +2 -0
  35. package/dist/css/css-file-field-compact.js +106 -0
  36. package/dist/css/css-file-field.js +4 -0
  37. package/dist/css/css-list-tiles.d.ts +6 -0
  38. package/dist/css/css-list-tiles.js +50 -0
  39. package/dist/css/css-list.js +5 -1
  40. package/dist/css/css-steps-title-detail.d.ts +2 -0
  41. package/dist/css/css-steps-title-detail.js +36 -0
  42. package/dist/css/css-steps.d.ts +2 -2
  43. package/dist/css/css-steps.js +6 -2
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.js +2 -0
  46. package/dist/list-options.d.ts +19 -11
  47. package/dist/list-options.js +27 -11
  48. package/dist/styles.css +134 -0
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.46.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f93ec57: `Button` `width`: `'content'` (the default, as before) or `'full'`, which fills the container's inline size in a `Stack`, a `Grid` cell or a column, and in an `Inline` row takes the space its siblings leave (as a full `Select` does; in a wrapping row it grows from its words and wraps), such as an Edit beside a ••• `ActionMenu` that keeps its own square at the row's end. The words stay centred. An unknown width throws a `RangeError`. New class `sw-button-full`; `ButtonWidth` and `buttonWidths` are exported. No new tokens or dependencies. Request: `docs/requests/teisoro-button-full-width.md`.
8
+ - 58bcede: `FileField` `onRemove` and `removeLabel`: Remove beside a chosen file, a neutral button after the change button in the full field, named by its words and the label ("Remove Cover photo"); the consumer clears `value` and focus moves to the empty field. `FileField` `compact`: the field at a text field's height for a form row; chosen, the thumbnail (or mark) and the name cut to one line are the change button, named by the label and `changeLabel` and described by the name and detail (read, not drawn), with Remove as a quiet button at the row's end. Set `onRemove` and `removeLabel` together (a `RangeError` otherwise, and for blank words). New classes `sw-file-field-compact`, `sw-file-field-pick`, `sw-file-field-remove` and `sw-file-field-actions`; disabled, the compact row and its Remove fade alike. A field without them renders as before. No new tokens or dependencies. Request: `docs/requests/teisoro-file-field-compact.md`.
9
+ - 0441a50: `ListGroup` `variant="tiles"`: the same one list (its name, its items and their count) with its rows laid out as tiles, each row its own bordered panel on the surface and no hairline between them, in `tileColumns` equal columns (2, 3 by default, or 4) a space-3 apart from `md` up, and one a row below it whatever `tileColumns` says, such as a strip of new arrivals. A row's press fill and inset focus ring stay inside its tile. Tiles take no `maxHeight` or `footer`, and `tileColumns` needs `tiles` (a `RangeError`, as for a count other than 2, 3 or 4). New classes `sw-list-group-tiles` and `sw-list-group-tiles-2`, `-3`, `-4`; `listTileColumnCounts` and `ListGroupTileColumns` are exported. No new tokens or dependencies. Request: `docs/requests/teisoro-list-group-tiles.md`.
10
+ - ccc2f79: `Select` `aria-labelledby`: in place of `label`, the id of words the page already draws, such as a label above a choice that is a `SegmentedControl` on a wide screen and a `Select` on a phone. The select then draws no label of its own, so a screen reader meets the words once instead of the page's words followed by the select's hidden copy; the trigger and its open list are named by them. `required` still sets `aria-required` and draws no mark. `label` and `aria-labelledby` are exclusive (a type union, and a `RangeError` for neither, a blank id, both, or `requiredMark` with `aria-labelledby`). A select with `label` renders exactly as before. **Types:** `SelectProps` is now a union (`SelectNaming`), so `label` is optional on the type. Code that reads `props.label` from a `SelectProps` narrows it first, for example a wrapper typed `SelectProps & { label: string }`, or a check `if (props.label !== undefined)`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-select-labelledby.md`.
11
+
12
+ ## 1.45.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 4170f23: `Article` `contentLang`: the language of the title and the body (a BCP 47 tag such as `es`) when it is not the page's, set as `lang` on the title's heading and the body's box only, never the eyebrow, meta line, actions or cover, which are the app's words (WCAG 3.1.2). The article's name is unchanged. A blank `contentLang` throws a `RangeError`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-article-content-lang.md`.
17
+ - fada31f: `ListRow` `titleLang`: the title's language (a BCP 47 tag such as `es`) when it is not the page's, set as `lang` on the title's box only (its words and the wordless unread dot), never the detail, value, tag or unread words (WCAG 3.1.2). A pressable row's name and description are unchanged; it works with `truncate`. A blank `titleLang` throws a `RangeError`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-list-row-title-lang.md`.
18
+ - 8ed264e: `Step` `titleDetail`: muted words after an open step's title, on its line, such as "Optional" or "Wetlands done · Woodlands empty", at the label size and body weight in the muted colour, a space-2 after the title on its baseline or on their own line under it where they do not fit. Drawn while the step is open (current, error, or any step of `Steps open`), outside the heading, so the heading and the region keep the title's name; an open list's region is described by the words. A blank `titleDetail` throws a `RangeError`. New classes `sw-steps-title-line` and `sw-steps-title-detail`. No new tokens, no new dependency. Request: `docs/requests/teisoro-step-title-detail.md`.
19
+
20
+ ### Patch Changes
21
+
22
+ - 54907b3: `Steps`: a folded step's heading reads "The site, Done", not "The site , Done" (checked against Chromium's accessibility tree; the name is one text box, so there are no two boxes to space apart). Its name is now one visually hidden line (the title, a comma and the status word) and the drawn title is `aria-hidden` on its own box; the step's region and its Edit button are still named by the drawn title, and nothing drawn changes. A current step, and a skipped step showing its reason, are unchanged. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-steps-status-words.md`.
23
+
3
24
  ## 1.44.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -78,7 +78,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
78
78
 
79
79
  `AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. An unknown `appearance` throws a `RangeError`.
80
80
 
81
- `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
81
+ `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words): its name is one visually hidden line, the title, a comma and the word ("The site, Done"), and the drawn title is `aria-hidden`, because a browser lays a hidden box out as a block and would put a space before the comma ("The site , Done"). The step's region and its Edit button are still named by the drawn title. A current step, and a skipped step showing its reason, draw and read their words as they are. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. The comma after the title is Scalewing's, as before; the word is yours. When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. `titleDetail` (opt-in) is muted words after an open step's title, on its line, such as "Optional" on a step a person may skip, or "Wetlands done · Woodlands empty" on a step with a part per habitat: the label size at the body's weight in the muted colour, a space-2 after the title on its baseline, or on a line of their own under the title where they do not fit beside it (a long title on a phone), never past the panel (a long word breaks). They are drawn while the step is open (current or error, or every step of an open list, with fields or without) and stay as the step turns done in an open list; a folded step draws its summary or reason instead. They sit outside the heading, so the heading's name stays the title (and its status word) and a heading list reads the titles alone; an open list's region keeps the title as its name and is described by the words, read as a person enters it, and in a stepped list they follow the heading in reading order. Write your own separators inside the words; Scalewing draws only the gap after the title. The words share a skipped step's reason's muted colour, not its size: a reason is part of the title, these are a note beside it. Give a step its `titleDetail` from its first render: setting or clearing it later moves the heading into or out of the title line, which remounts it (and drops its focus if it has it). A blank `titleDetail` throws a `RangeError`. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
82
82
 
83
83
  ```tsx
84
84
  <Steps aria-label="Heron survey">
@@ -144,6 +144,23 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
144
144
 
145
145
  `FileField` is one file chosen with the system picker or dropped on the field, with `Field`'s `label` (above it, or hidden with `labelVisuallyHidden`), `description` and `error` (a polite live region in place of the hint, the frame's edge in the danger color, `Mark` in forced colors). Empty, it is one wide zone on the subtle fill, a button named by the label and `chooseLabel` ("Field guide Choose a PDF"), with `dropLabel` ("or drop it here") muted after it where the pointer can drag (left out on a coarse pointer). Chosen (`value={{ name, detail? }}`), it shows the consumer's `icon` (hidden from assistive technology), the file's name over the muted `detail` and a neutral button named by the label and `changeLabel`, described by the name and detail. Either button opens the picker (the native input is hidden and out of the Tab order, its `accept` the list the picker offers); `onChange` receives the `File` chosen or dropped, the first of several, and when a choice or a drop swaps the zone and the chosen row while focus is in the field, focus moves to the button now there (focus the person moved elsewhere is never taken back). A file dragged over the field draws the accent edge and tint, its words in the text color. It never reads, checks or uploads the file, and a dropped file is not filtered by `accept`: check what you receive (type, size, its header) and say why in `error`, keeping the earlier `value` or none. `disabled` blocks the picker, refuses a dropped file and dashes the frame. `id` prefixes the ids of the field's parts. An empty `label`, `chooseLabel` or `changeLabel`, a blank `dropLabel`, or a `value` with no name or a blank detail throws a `RangeError`.
146
146
 
147
+ `FileField` `onRemove` with `removeLabel` (set both or neither) adds Remove beside a chosen file: a neutral button after the change button in the full field (the two together at the row's end), named by its words and the label ("Remove Cover photo"), so two fields' Removes are told apart. It calls `onRemove`, and the consumer clears `value`; focus then moves to the empty field's button. Nothing is removed while the field is empty, and `disabled` disables it too. A lone `onRemove` or `removeLabel`, or blank `removeLabel` words, throws a `RangeError`.
148
+
149
+ `FileField` `compact` draws the field at a text field's height (the md control, 44 px) for a form row beside other fields, such as a cover photo beside a title. Empty, the zone holds the choose words at its start. Chosen, the row is one button, the thumbnail (or the mark) and the file's name in the body weight cut to one line with an ellipsis, named by the label and `changeLabel` ("Cover photo Choose another") and described by the name and `detail` (read, not drawn); Remove, a quiet ghost button, ends the row. The label, description and error are as in the full field.
150
+
151
+ ```tsx
152
+ <FileField
153
+ label="Cover photo"
154
+ compact
155
+ chooseLabel="Choose a photo"
156
+ changeLabel="Choose another"
157
+ removeLabel="Remove"
158
+ value={cover}
159
+ onChange={choose}
160
+ onRemove={() => setCover(null)}
161
+ />
162
+ ```
163
+
147
164
  A chosen photo can show itself: `value.preview` is the address of a small image of it, such as an object URL the consumer made from the `File` (and revokes when another is chosen), drawn as a 3:2 thumbnail (a space-8 wide, a space-6 tall, cropped to fill, at the small radius) before the name in place of `icon`. It is decorative (`alt=""`): the name beside it says which file it is. A blank `preview` throws a `RangeError`.
148
165
 
149
166
  ```tsx
@@ -178,6 +195,18 @@ A chosen photo can show itself: `value.preview` is the address of a small image
178
195
 
179
196
  `Select` is a labeled listbox menu: supply `label`, `options` (`value` and `label`), a controlled `value` and `onChange`; `size="xs"` and `labelVisuallyHidden` are for toolbar chrome, and `action` is a last listbox command that never becomes the value. The closed trigger matches `Field`: its text is centred in the control, it ends in the `Accordion` chevron, and it is as wide as its longest option (as a native select is), so it keeps its width when the value changes; past the available width the label ellipsizes. The option labels that size it are drawn by CSS from `data-label`, so the trigger's text content and accessible value are only the current label. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so a `Select` beside a `Field` lines up at the label and the control.
180
197
 
198
+ `Select` `aria-labelledby`, in place of `label`, names the select by words the page already draws, such as a label above a choice that is a `SegmentedControl` on a wide screen and a `Select` on a phone. The select then draws no label of its own, hidden or shown, so a screen reader meets the words once (as the page's text, then as the combobox's name) instead of the page's words followed by the select's hidden copy of them; the open list is named by the same words. `required` still sets `aria-required` but draws no mark, since the page owns the words. `label` and `aria-labelledby` are exclusive: a blank `label` with no `aria-labelledby`, a blank `aria-labelledby`, or `aria-labelledby` with `label`, `labelVisuallyHidden` or `requiredMark` throws a `RangeError`. The id must name an element in the page while the select is shown: a missing one leaves the select unnamed, which neither the types nor the select can check.
199
+
200
+ ```tsx
201
+ <Text as="span" id="den-habitat" variant="label">Den habitat</Text>
202
+ <Select
203
+ aria-labelledby="den-habitat"
204
+ value={habitat}
205
+ onChange={setHabitat}
206
+ options={habitats}
207
+ />
208
+ ```
209
+
181
210
  `Select` `placeholder` shows in the closed trigger, in the muted color, while `value` matches no option (such as `''`); it is not an option, is never selected in the list and never becomes the value, and it counts toward the trigger's width. `required` marks the label with `Field`'s `aria-hidden` asterisk and sets `aria-required` on the trigger. `error` renders a message under the control (`sw-field-error`), links it by `aria-describedby`, sets `aria-invalid` and the danger border; like `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. A blank `placeholder` throws a `RangeError`.
182
211
 
183
212
  ```tsx
@@ -269,6 +298,22 @@ A `Button`'s own children are laid out in a row one token gap apart (spacing ste
269
298
 
270
299
  On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, the fill of an open menu row) with its words in the text color and no border: an action beside the primary that should not draw the eye, such as a sheet's Cancel. It is web only (`webButtonVariants`; the shared `buttonVariants` in `@scalewing/tokens` and React Native do not have it). `disabledAppearance="neutral"` (default `"faded"`) draws a disabled button on that grey with its words in the muted color at 48 % (about #AEAEAE on the grey) and full opacity, whatever its variant, instead of fading its own fill (a disabled accent reads as a paler accent); a `ghost` button, which has no fill, still fades. The button is still a disabled `button`; forced colors draw it as any disabled button. An unknown `disabledAppearance` throws a `RangeError`.
271
300
 
301
+ `Button` `width` is `'content'` (the default: as wide as its words) or `'full'`: the button fills its container's inline size in a `Stack`, a `Grid` cell or any column, and in an `Inline` row takes the space its siblings leave, as a full `Select` does, so an Edit beside a ••• `ActionMenu` fills the row and the menu keeps its own square at the end. The row rule holds for a `Button` that is a direct child of the `Inline`; wrapped in another box it fills that box instead. In a wrapping `Inline` (`wrap`) a full button grows from its words' width and moves to a line of its own when the row runs out, rather than shrinking past its words. Inside a `ButtonGroup` it is not needed: the group already stacks its buttons full width below `md`. The words stay centred. An unknown width throws a `RangeError`. The class is `sw-button-full`.
302
+
303
+ ```tsx
304
+ <Inline gap={2}>
305
+ <Button variant="neutral" width="full" onPress={edit}>
306
+ Edit
307
+ </Button>
308
+ <ActionMenu
309
+ label="More for Heron log"
310
+ items={items}
311
+ trigger={<Dots />}
312
+ triggerAppearance="icon"
313
+ />
314
+ </Inline>
315
+ ```
316
+
272
317
  A `Button` with `aria-disabled="true"` (not `disabled`) looks disabled, faded or, with `disabledAppearance="neutral"`, on the neutral grey, with `GrayText` in forced colors, yet stays a focusable, pressable `button`: its `onPress` still runs, so a submit pressed while off can take the person to what is missing. Its look alone does not say what is missing: a press while off should show the message of the first thing to fix. Use `disabled` when a press should do nothing.
273
318
 
274
319
  A `Button` that toggles something passes `aria-pressed`. The pressed button gets a 2 px accent ring outside its fill, past a 2 px gap in `--sw-color-background`, so the ring meets only the page and keeps the accent's contrast on the canvas (4.5:1 or more in every palette, at least the 3:1 a state indicator needs) whatever its `variant` or fill. A focused pressed button moves its focus outline out past the ring. In forced colors, which drop shadows, the pressed button keeps the forced button colors, so its label and any glyph or badge inside it stay readable, and draws the same ring and its own border in the system highlight (`Highlight`); nothing opts out of forced colors. An unpressed button is drawn at full strength, so its label keeps its contrast. To make the choice stand out further, give the pressed button `primary` and the others `secondary`.
@@ -750,6 +795,8 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
750
795
 
751
796
  `Article` is a page that reads like a post, on a card (`variant`, as `Card`'s) no wider than the reading width (`--sw-reading-max`, 46rem): an optional `cover` photo (`{ src, alt }`, `alt` required, `''` when it only decorates) (or `null`) edge to edge across the top at 3:1 (2:1 below `md`) with the card's top corners; then, a space-6 from the top and bottom and a space-8 from the sides (space-5 all round below `md`), the `eyebrow` (muted caption type at the label's weight) with your `actions` at its row's end (such as a language switch; under the eyebrow, at its start, when the row is too narrow), the `title` in heading type (`titleAs`: `h1` by default, on the article's own page; `h2` or `h3` under other headings), the muted `meta` line (such as "Manager 1 · Oct 16 · 4 min read"), a hairline, and the body (`children`, your own content, such as paragraphs or Markdown your app draws). It is an `article` landmark named by its title, and its ref is that element; the eyebrow and meta are `div`s, so any content fits. The card sits at the start of a wider column. Empty parts draw nothing. A blank `title`, an unknown `titleAs` or `variant`, or a `cover` with a blank `src` or no `alt` throws a `RangeError`.
752
797
 
798
+ `contentLang` (opt-in) is the language of the title and the body, a BCP 47 tag such as `es`, when it is not the page's, such as a guide shown in Spanish on an English screen because it exists only in Spanish or the reader chose Español (WCAG 3.1.2, Language of Parts). It sets `lang` on the title's heading and on the body's box only; the eyebrow, the meta line, the actions and the cover's `alt` stay in the page's language, since they are the app's words (write the cover's `alt` in the page's language, or mark it yourself). The article's name is unchanged. Pass `lang` on the `Article` instead when every word in it, the app's included, is in that language. A blank `contentLang` throws a `RangeError`.
799
+
753
800
  ```tsx
754
801
  <Article
755
802
  cover={{ src: cover.url, alt: cover.alt }}
@@ -838,12 +885,14 @@ const { headings, omissions } = inspectMarkdown(page.body, {
838
885
  </Card>
839
886
  ```
840
887
 
841
- `ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted`, `accent` or `warning`, the warning colour's ink moved halfway to the text colour so it keeps 4.5:1 on the surface in every palette; `valueBadge` draws the value as a `Badge` in that tone instead, such as a status, still read in the row's name, never with `valueTone`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant, tone or badge tone, a `valueDetail`, `valueTone` or `valueBadge` without a value, a value both toned and a badge, or a bad row count throws a `RangeError`.
888
+ `ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. `variant="tiles"` keeps one list (its name, its list items, a screen reader's count of them) and lays its rows out as tiles: each row its own bordered panel on the surface with no hairline between them, in `tileColumns` equal columns (`2`, `3` the default, or `4`; `listTileColumnCounts`, `ListGroupTileColumns`) a spacing step 3 apart from `md` up, one a row below it, such as a strip of new arrivals under its heading. A row's press fill and inset focus ring stay inside its tile; `truncate` keeps a long title to one line in a narrow tile. Tiles take no `maxHeight` or `footer`, and `tileColumns` needs `tiles`; either throws a `RangeError`, as does a count other than 2, 3 or 4. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted`, `accent` or `warning`, the warning colour's ink moved halfway to the text colour so it keeps 4.5:1 on the surface in every palette; `valueBadge` draws the value as a `Badge` in that tone instead, such as a status, still read in the row's name, never with `valueTone`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant, tone or badge tone, a `valueDetail`, `valueTone` or `valueBadge` without a value, a value both toned and a badge, or a bad row count throws a `RangeError`.
842
889
 
843
890
  A row can also carry three trailing parts. `unread` marks it new for the person, such as a document they have not opened: an accent dot after the title's last word (drawn as a border, so forced colors keep it in the text color), hidden from assistive technology, and `unreadLabel` (required while `unread`, such as "New") is read after the title, in a pressable row's name ("Heron calls New"). `tag` is a short status before the row's end, such as a `Badge` ("Only in English"), centred on the row and never wrapped; a pressable row is described by it after its detail lines. `action` is a control after the row and outside its press area, such as an `ActionMenu` of the row's own commands named by the consumer ("More for Heron calls"): it is the row's sibling in the list item, centred on the row, after it in the Tab order, and a press on it never opens the row; it keeps the row's end inset (a space-4 in a panel or outside a ListGroup, none in a plain list), and where `:has` is supported the hover fill spans the row and its control. An empty slot (`undefined`, `null`, `false` or `''`, as conditional JSX gives) draws nothing, for `leading` too. Pair `action` with `accessory="none"` when the row's own chevron would sit beside it. An unread row without `unreadLabel`, or a blank one, throws a `RangeError`. An `aria-label` replaces the whole name, unread words included.
844
891
 
845
892
  `truncate` (opt-in) keeps a row's title and its detail line to one line each, cut with "…" where they do not fit, for a long list read at a glance such as a shelf of documents. The title's words sit in their own box, so the unread dot follows the cut words and is never hidden; the whole words stay in the row's name and description and in a `title` tooltip on the words and the detail. The value, its detail, the tag and the action keep their words whole. A tooltip needs a pointer, so on a touch screen or from the keyboard the cut words are whole only in what the row opens and in what assistive technology reads; keep `truncate` for rows that open their whole content, or whose words a person does not need in full to choose.
846
893
 
894
+ `titleLang` (opt-in) is the title's language, a BCP 47 tag such as `es`, when it is not the page's, such as a document's Spanish title in a list on an English page (WCAG 3.1.2, Language of Parts). It sets `lang` on the title's box, which holds the title's words and the wordless unread dot, so a screen reader speaks the title in that language and the browser hyphenates and shapes it as such; the detail, the value and its detail, the tag, the unread words and the action stay in the page's language (give a tag its own `lang` inside it if it needs one). A pressable row's name and description are unchanged: the title is still read first. It works with `truncate` (the words box sits inside the marked title) and on every kind of row. A blank `titleLang` throws a `RangeError`.
895
+
847
896
  ```tsx
848
897
  <ListRow
849
898
  leading={
@@ -26,6 +26,14 @@ export type ArticleProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title
26
26
  actions?: ReactNode;
27
27
  /** The card look, as `Card`'s. */
28
28
  variant?: CardVariant;
29
+ /**
30
+ * The language of the title and the body (a BCP 47 tag such as `es`)
31
+ * when it is not the page's, such as a document shown in Spanish on an
32
+ * English screen (WCAG 3.1.2): `lang` on the title's heading and the
33
+ * body only, never the eyebrow, meta line, actions or cover, which are
34
+ * the app's own words.
35
+ */
36
+ contentLang?: string;
29
37
  /** The article's body, under a hairline. */
30
38
  children?: ReactNode;
31
39
  };
@@ -34,8 +42,9 @@ export type ArticleProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title
34
42
  * cover photo edge to edge across the top, then the eyebrow (with the
35
43
  * consumer's actions at its row's end), the title in heading type, the
36
44
  * muted meta line, a hairline, and the body. The article is named by its
37
- * title (an `aria-labelledby` you pass replaces that). The ref is the
38
- * `article` element.
45
+ * title (an `aria-labelledby` you pass replaces that). `contentLang` marks
46
+ * the title and the body as another language than the page's. The ref is
47
+ * the `article` element.
39
48
  */
40
49
  export declare const Article: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLElement>, "title" | "children"> & {
41
50
  /** The article's title, in heading type. */
@@ -55,6 +64,14 @@ export declare const Article: import("react").ForwardRefExoticComponent<Omit<HTM
55
64
  actions?: ReactNode;
56
65
  /** The card look, as `Card`'s. */
57
66
  variant?: CardVariant;
67
+ /**
68
+ * The language of the title and the body (a BCP 47 tag such as `es`)
69
+ * when it is not the page's, such as a document shown in Spanish on an
70
+ * English screen (WCAG 3.1.2): `lang` on the title's heading and the
71
+ * body only, never the eyebrow, meta line, actions or cover, which are
72
+ * the app's own words.
73
+ */
74
+ contentLang?: string;
58
75
  /** The article's body, under a hairline. */
59
76
  children?: ReactNode;
60
77
  } & import("react").RefAttributes<HTMLElement>>;
@@ -11,9 +11,12 @@ function hasContent(node) {
11
11
  typeof node !== 'boolean' &&
12
12
  node !== '');
13
13
  }
14
- function assertArticle({ title, titleAs, cover, variant }) {
14
+ function assertArticle({ title, titleAs, cover, variant, contentLang, }) {
15
15
  if (typeof title !== 'string' || title.trim() === '')
16
16
  throw new RangeError('Article title must be non-empty text');
17
+ if (contentLang !== undefined &&
18
+ (typeof contentLang !== 'string' || contentLang.trim() === ''))
19
+ throw new RangeError('Article contentLang must be a non-empty language tag');
17
20
  if (titleAs !== undefined && !articleTitleElements.includes(titleAs))
18
21
  throw new RangeError(`Unknown Article titleAs: ${String(titleAs)}`);
19
22
  if (variant !== undefined && !cardVariants.includes(variant))
@@ -30,13 +33,14 @@ function assertArticle({ title, titleAs, cover, variant }) {
30
33
  * cover photo edge to edge across the top, then the eyebrow (with the
31
34
  * consumer's actions at its row's end), the title in heading type, the
32
35
  * muted meta line, a hairline, and the body. The article is named by its
33
- * title (an `aria-labelledby` you pass replaces that). The ref is the
34
- * `article` element.
36
+ * title (an `aria-labelledby` you pass replaces that). `contentLang` marks
37
+ * the title and the body as another language than the page's. The ref is
38
+ * the `article` element.
35
39
  */
36
40
  export const Article = forwardRef(function Article(props, ref) {
37
41
  assertArticle(props);
38
- const { title, titleAs = 'h1', eyebrow, meta, cover, actions, variant = 'glass', children, className, ...rest } = props;
42
+ const { title, titleAs = 'h1', eyebrow, meta, cover, actions, variant = 'glass', contentLang, children, className, ...rest } = props;
39
43
  const titleId = useId();
40
44
  const top = hasContent(eyebrow) || hasContent(actions);
41
- return (_jsxs("article", { "aria-labelledby": titleId, className: cx('sw-card', `sw-card-${variant}`, 'sw-article', className), ref: ref, ...rest, children: [cover ? (_jsx("img", { alt: cover.alt, className: "sw-article-cover", src: cover.src })) : null, _jsxs("div", { className: "sw-article-body", children: [_jsxs("header", { className: "sw-article-header", children: [top ? (_jsxs("div", { className: "sw-article-top", children: [hasContent(eyebrow) ? (_jsx("div", { className: "sw-article-eyebrow", children: eyebrow })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-article-actions", children: actions })) : null] })) : null, _jsx(Text, { as: titleAs, className: "sw-article-title", id: titleId, variant: "heading", children: title }), hasContent(meta) ? (_jsx("div", { className: "sw-article-meta", children: meta })) : null] }), _jsx(Separator, { decorative: true }), hasContent(children) ? (_jsx("div", { className: "sw-article-content", children: children })) : null] })] }));
45
+ return (_jsxs("article", { "aria-labelledby": titleId, className: cx('sw-card', `sw-card-${variant}`, 'sw-article', className), ref: ref, ...rest, children: [cover ? (_jsx("img", { alt: cover.alt, className: "sw-article-cover", src: cover.src })) : null, _jsxs("div", { className: "sw-article-body", children: [_jsxs("header", { className: "sw-article-header", children: [top ? (_jsxs("div", { className: "sw-article-top", children: [hasContent(eyebrow) ? (_jsx("div", { className: "sw-article-eyebrow", children: eyebrow })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-article-actions", children: actions })) : null] })) : null, _jsx(Text, { as: titleAs, className: "sw-article-title", id: titleId, lang: contentLang, variant: "heading", children: title }), hasContent(meta) ? (_jsx("div", { className: "sw-article-meta", children: meta })) : null] }), _jsx(Separator, { decorative: true }), hasContent(children) ? (_jsx("div", { className: "sw-article-content", lang: contentLang, children: children })) : null] })] }));
42
46
  });
@@ -1,9 +1,10 @@
1
1
  import { type ButtonSize } from '@scalewing/tokens';
2
2
  import { type ButtonDisabledAppearance, type WebButtonVariant } from '../css/css-button.js';
3
+ import { type ButtonWidth } from '../css/css-button-width.js';
3
4
  import { type ButtonHTMLAttributes, type ReactNode } from 'react';
4
5
  /** The web variants: the shared tiers plus `neutral`. */
5
6
  export type ButtonVariant = WebButtonVariant;
6
- export type { ButtonDisabledAppearance, ButtonSize };
7
+ export type { ButtonDisabledAppearance, ButtonSize, ButtonWidth };
7
8
  export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'type'> & {
8
9
  children: ReactNode;
9
10
  onPress: () => void;
@@ -21,6 +22,12 @@ export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick
21
22
  * whatever its variant (a ghost button still fades).
22
23
  */
23
24
  disabledAppearance?: ButtonDisabledAppearance;
25
+ /**
26
+ * `content` (default) is as wide as the words; `full` fills the
27
+ * container, and in an Inline row the space its siblings leave, such as
28
+ * an Edit beside a ••• menu.
29
+ */
30
+ width?: ButtonWidth;
24
31
  };
25
32
  export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "type"> & {
26
33
  children: ReactNode;
@@ -39,4 +46,10 @@ export declare const Button: import("react").ForwardRefExoticComponent<Omit<Butt
39
46
  * whatever its variant (a ghost button still fades).
40
47
  */
41
48
  disabledAppearance?: ButtonDisabledAppearance;
49
+ /**
50
+ * `content` (default) is as wide as the words; `full` fills the
51
+ * container, and in an Inline row the space its siblings leave, such as
52
+ * an Edit beside a ••• menu.
53
+ */
54
+ width?: ButtonWidth;
42
55
  } & import("react").RefAttributes<HTMLButtonElement>>;
@@ -1,10 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { buttonClassNames, buttonDisabledAppearances, } from '../css/css-button.js';
3
+ import { buttonWidthClass, buttonWidths, } from '../css/css-button-width.js';
3
4
  import { forwardRef } from 'react';
4
5
  import { cx } from '../class-names.js';
5
6
  import { tabStop } from '../tab-stop.js';
6
- export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', tabIndex, type = 'button', variant = 'primary', ...rest }, ref) {
7
+ export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', tabIndex, type = 'button', variant = 'primary', width = 'content', ...rest }, ref) {
7
8
  if (!buttonDisabledAppearances.includes(disabledAppearance))
8
9
  throw new RangeError(`Unknown Button disabledAppearance: ${String(disabledAppearance)}`);
9
- return (_jsx("button", { ref: ref, className: cx(...buttonClassNames({ size, variant, disabledAppearance }), className), disabled: disabled, tabIndex: tabIndex ?? tabStop, type: type, ...rest, onClick: onPress, children: children }));
10
+ if (!buttonWidths.includes(width))
11
+ throw new RangeError(`Unknown Button width: ${String(width)}`);
12
+ return (_jsx("button", { ref: ref, className: cx(...buttonClassNames({ size, variant, disabledAppearance }), buttonWidthClass(width), className), disabled: disabled, tabIndex: tabIndex ?? tabStop, type: type, ...rest, onClick: onPress, children: children }));
10
13
  });
@@ -42,6 +42,21 @@ export type FileFieldProps = {
42
42
  */
43
43
  icon?: ReactNode;
44
44
  disabled?: boolean;
45
+ /**
46
+ * The field at a text field's height, for a form row beside other
47
+ * fields: empty, one button with `chooseLabel`; chosen, the file's mark
48
+ * and its name cut to one line, pressed to choose another (named by
49
+ * `label` and `changeLabel`), the detail read but not drawn. Default
50
+ * false: the wide zone, and the chosen row with its change button.
51
+ */
52
+ compact?: boolean;
53
+ /**
54
+ * Called by Remove, drawn beside a chosen file when set with
55
+ * `removeLabel`: the consumer clears `value`. Set both or neither.
56
+ */
57
+ onRemove?: () => void;
58
+ /** Remove's words, such as "Remove"; named with `label` ("Remove Cover photo"). */
59
+ removeLabel?: string;
45
60
  /** Prefixes the ids the field gives its parts (its button is `${id}-button`). */
46
61
  id?: string;
47
62
  };
@@ -49,7 +64,8 @@ export type FileFieldProps = {
49
64
  * One file chosen by a press or dropped on the field: empty, a wide zone
50
65
  * that opens the system picker; chosen, the file's name and detail (after
51
66
  * a thumbnail for a photo with a `preview`) with a button to choose
52
- * another. It hands the `File` to the consumer and shows what the
67
+ * another, and Remove with `onRemove`. `compact` draws both at a text
68
+ * field's height. It hands the `File` to the consumer and shows what the
53
69
  * consumer passes back as `value`; it never reads or uploads it.
54
70
  */
55
- export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, id, }: FileFieldProps): import("react").JSX.Element;
71
+ export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, compact, onRemove, removeLabel, id, }: FileFieldProps): import("react").JSX.Element;
@@ -1,39 +1,33 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId, useRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { tabStop } from '../tab-stop.js';
6
- import { Button } from './Button.js';
7
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
8
7
  import { FieldLabelText } from './FieldLabelText.js';
8
+ import { ChosenFileCompact, ChosenFileRow } from './file-field/ChosenFile.js';
9
9
  import { assertFileField } from './file-field/file-field-checks.js';
10
10
  import { useFileDrop } from './file-field/use-file-drop.js';
11
11
  import { useSwapFocus } from './file-field/use-swap-focus.js';
12
12
  import { Stack } from './Stack.js';
13
- /** Whether the consumer passed a mark to draw (not empty JSX). */
14
- function hasMark(icon) {
15
- return (icon !== undefined &&
16
- icon !== null &&
17
- icon !== '' &&
18
- typeof icon !== 'boolean');
19
- }
20
- /** Before a chosen file's name: its photo's thumbnail, else the consumer's mark. */
21
- function ChosenMark({ icon, preview }) {
22
- if (preview !== undefined)
23
- return _jsx("img", { alt: "", className: "sw-file-field-preview", src: preview });
24
- if (!hasMark(icon))
25
- return null;
26
- return (_jsx("span", { "aria-hidden": "true", className: "sw-file-field-icon", children: icon }));
27
- }
28
13
  /**
29
14
  * One file chosen by a press or dropped on the field: empty, a wide zone
30
15
  * that opens the system picker; chosen, the file's name and detail (after
31
16
  * a thumbnail for a photo with a `preview`) with a button to choose
32
- * another. It hands the `File` to the consumer and shows what the
17
+ * another, and Remove with `onRemove`. `compact` draws both at a text
18
+ * field's height. It hands the `File` to the consumer and shows what the
33
19
  * consumer passes back as `value`; it never reads or uploads it.
34
20
  */
35
- export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, id, }) {
36
- assertFileField({ label, chooseLabel, changeLabel, dropLabel, value });
21
+ export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, compact = false, onRemove, removeLabel, id, }) {
22
+ assertFileField({
23
+ label,
24
+ chooseLabel,
25
+ changeLabel,
26
+ dropLabel,
27
+ value,
28
+ onRemove,
29
+ removeLabel,
30
+ });
37
31
  const ownId = useId();
38
32
  const baseId = id ?? ownId;
39
33
  const inputRef = useRef(null);
@@ -55,11 +49,17 @@ export function FileField({ label, labelVisuallyHidden = false, description, err
55
49
  detail: `${baseId}-detail`,
56
50
  hint: `${baseId}-hint`,
57
51
  error: `${baseId}-error`,
52
+ remove: `${baseId}-remove`,
58
53
  };
54
+ const compactChosen = compact && chosen;
59
55
  // The error is in the button's description and the polite region; the
60
56
  // button role takes no aria-invalid.
57
+ // Compact and chosen, the button holds the file's name: it is named by
58
+ // the label and its hidden change words, the name only describing it.
61
59
  const naming = {
62
- 'aria-labelledby': `${ids.label} ${ids.button}`,
60
+ 'aria-labelledby': compactChosen
61
+ ? `${ids.label} ${ids.button}-words`
62
+ : `${ids.label} ${ids.button}`,
63
63
  'aria-describedby': [
64
64
  chosen && ids.name,
65
65
  chosen && value.detail && ids.detail,
@@ -71,5 +71,9 @@ export function FileField({ label, labelVisuallyHidden = false, description, err
71
71
  id: ids.button,
72
72
  };
73
73
  const open = () => inputRef.current?.click();
74
- return (_jsxs(Stack, { className: cx('sw-field', 'sw-file-field', error && 'sw-field-invalid'), gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("span", { id: ids.label, children: _jsx(FieldLabelText, { label: label, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: cx('sw-file-field-frame', chosen ? 'sw-file-field-chosen' : 'sw-file-field-empty', drop.dragging && 'sw-file-field-dragging', disabled && 'sw-file-field-disabled'), ...drop.handlers, ...focusHandlers, children: [_jsx("input", { accept: accept, "aria-label": label, disabled: disabled, hidden: true, onChange: onInput, ref: inputRef, tabIndex: -1, type: "file" }), chosen ? (_jsxs(_Fragment, { children: [_jsx(ChosenMark, { icon: icon, preview: value.preview }), _jsxs("span", { className: "sw-file-field-file", children: [_jsx("span", { className: "sw-file-field-name", id: ids.name, children: value.name }), value.detail ? (_jsx("span", { className: "sw-file-field-detail", id: ids.detail, children: value.detail })) : null] }), _jsx(Button, { ...naming, disabled: disabled, disabledAppearance: "neutral", onPress: open, ref: buttonRef, variant: "neutral", children: changeLabel })] })) : (_jsxs("button", { ...naming, className: "sw-file-field-zone", disabled: disabled, onClick: open, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-file-field-choose", children: chooseLabel }), dropLabel ? ' ' : null, dropLabel ? (_jsx("span", { className: "sw-file-field-drop", children: dropLabel })) : null] }))] }), description && !error ? (_jsx("span", { className: "sw-field-description", id: ids.hint, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: ids.error, message: error })] }));
74
+ const remove = onRemove !== undefined && removeLabel !== undefined
75
+ ? { label: removeLabel, onRemove }
76
+ : null;
77
+ const ChosenFile = compact ? ChosenFileCompact : ChosenFileRow;
78
+ return (_jsxs(Stack, { className: cx('sw-field', 'sw-file-field', error && 'sw-field-invalid'), gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("span", { id: ids.label, children: _jsx(FieldLabelText, { label: label, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: cx('sw-file-field-frame', chosen ? 'sw-file-field-chosen' : 'sw-file-field-empty', compact && 'sw-file-field-compact', drop.dragging && 'sw-file-field-dragging', disabled && 'sw-file-field-disabled'), ...drop.handlers, ...focusHandlers, children: [_jsx("input", { accept: accept, "aria-label": label, disabled: disabled, hidden: true, onChange: onInput, ref: inputRef, tabIndex: -1, type: "file" }), chosen ? (_jsx(ChosenFile, { buttonRef: buttonRef, changeLabel: changeLabel, disabled: disabled, icon: icon, ids: ids, naming: naming, onChange: open, remove: remove, value: value })) : (_jsxs("button", { ...naming, className: "sw-file-field-zone", disabled: disabled, onClick: open, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-file-field-choose", children: chooseLabel }), dropLabel ? ' ' : null, dropLabel ? (_jsx("span", { className: "sw-file-field-drop", children: dropLabel })) : null] }))] }), description && !error ? (_jsx("span", { className: "sw-field-description", id: ids.hint, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: ids.error, message: error })] }));
75
79
  }
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { type ListGroupTileColumns } from '../css/css-list-tiles.js';
2
3
  import { type ListGroupMaxHeight, type ListGroupVariant } from '../list-options.js';
3
4
  export type ListGroupProps = {
4
5
  /** `ListRow` elements; the group draws a hairline between each pair. */
@@ -8,9 +9,13 @@ export type ListGroupProps = {
8
9
  'aria-labelledby'?: string;
9
10
  /**
10
11
  * `panel` (the default) is one bordered panel; `plain` draws no panel
11
- * and a hairline above the first row too, for rows inside a Card.
12
+ * and a hairline above the first row too, for rows inside a Card;
13
+ * `tiles` sets each row on a panel of its own, in `tileColumns` equal
14
+ * columns from `md` up and one a row below it, still one list.
12
15
  */
13
16
  variant?: ListGroupVariant;
17
+ /** How many tiles share a row from `md` up (`tiles` only): 2, 3 (default) or 4. */
18
+ tileColumns?: ListGroupTileColumns;
14
19
  /**
15
20
  * Bounds the list to a row count of its own region, `{ rows }` rows of a
16
21
  * title and one detail line, where the rest scroll; the region is a named
@@ -27,7 +32,7 @@ export type ListGroupProps = {
27
32
  };
28
33
  /**
29
34
  * Rows in one list (`ul`, its list role kept) with hairlines between them:
30
- * a panel of its own, or plain rows inside a surface. The ref reaches the
31
- * outer box.
35
+ * a panel of its own, or plain rows inside a surface; or the same list's
36
+ * rows as tiles, each a panel. The ref reaches the outer box.
32
37
  */
33
38
  export declare const ListGroup: import("react").ForwardRefExoticComponent<ListGroupProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -2,18 +2,26 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
+ import { listTileColumnsClass, } from '../css/css-list-tiles.js';
5
6
  import { assertListGroup, listMaxHeightStyle, } from '../list-options.js';
6
7
  import { ListRowContainer } from './list/list-row-container.js';
7
8
  import { ScrollRegion } from './ScrollRegion.js';
8
9
  /**
9
10
  * Rows in one list (`ul`, its list role kept) with hairlines between them:
10
- * a panel of its own, or plain rows inside a surface. The ref reaches the
11
- * outer box.
11
+ * a panel of its own, or plain rows inside a surface; or the same list's
12
+ * rows as tiles, each a panel. The ref reaches the outer box.
12
13
  */
13
- export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', maxHeight, footer, className, id, }, ref) {
14
- assertListGroup({ label, labelledBy, variant, maxHeight });
14
+ export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', tileColumns, maxHeight, footer, className, id, }, ref) {
15
+ assertListGroup({
16
+ label,
17
+ labelledBy,
18
+ variant,
19
+ maxHeight,
20
+ tileColumns,
21
+ hasFooter: footer !== undefined,
22
+ });
15
23
  const list = (_jsx("ul", { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list",
16
24
  // A list styled without bullets loses its role in Safari.
17
25
  role: "list", children: _jsx(ListRowContainer.Provider, { value: "li", children: children }) }));
18
- return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
26
+ return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, variant === 'tiles' && listTileColumnsClass(tileColumns ?? 3), className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
19
27
  });
@@ -3,6 +3,13 @@ import { type BadgeTone } from '../css/css-data.js';
3
3
  import { type ListRowTitleAppearance, type ListRowTone, type ListRowValueTone } from '../list-options.js';
4
4
  export type ListRowProps = {
5
5
  title: string;
6
+ /**
7
+ * The title's language (a BCP 47 tag such as `es`) when it is not the
8
+ * page's, such as a document's Spanish title on an English screen (WCAG
9
+ * 3.1.2): `lang` on the title's box only (its words and the wordless
10
+ * unread dot), never the detail, value, tag or unread words.
11
+ */
12
+ titleLang?: string;
6
13
  /** A muted line under the title. */
7
14
  detail?: string;
8
15
  /** A figure or word at the row's end, such as "+$1.48". */
@@ -83,7 +90,8 @@ export type ListRowProps = {
83
90
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
84
91
  * a chevron, the whole row pressing), picks one option in place
85
92
  * (`selected`), or only states something (neither). `unread` marks it new,
86
- * `tag` adds a status before its end, `action` a control beside it, and
87
- * `truncate` keeps its title and detail to one line each.
93
+ * `tag` adds a status before its end, `action` a control beside it,
94
+ * `truncate` keeps its title and detail to one line each, and `titleLang`
95
+ * marks a title in another language than the page's.
88
96
  */
89
- export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, truncate, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
97
+ export declare function ListRow({ title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, truncate, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
@@ -12,13 +12,15 @@ import { rowNaming } from './list/row-naming.js';
12
12
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
13
13
  * a chevron, the whole row pressing), picks one option in place
14
14
  * (`selected`), or only states something (neither). `unread` marks it new,
15
- * `tag` adds a status before its end, `action` a control beside it, and
16
- * `truncate` keeps its title and detail to one line each.
15
+ * `tag` adds a status before its end, `action` a control beside it,
16
+ * `truncate` keeps its title and detail to one line each, and `titleLang`
17
+ * marks a title in another language than the page's.
17
18
  */
18
- export function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', unread = false, unreadLabel, tag, action, truncate = false, 'aria-label': ariaLabel, }) {
19
+ export function ListRow({ title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', unread = false, unreadLabel, tag, action, truncate = false, 'aria-label': ariaLabel, }) {
19
20
  const pressable = onPress !== undefined || href !== undefined;
20
21
  assertListRow({
21
22
  title,
23
+ titleLang,
22
24
  tone,
23
25
  titleAppearance,
24
26
  valueTone,
@@ -33,7 +35,7 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
33
35
  });
34
36
  const id = useId();
35
37
  const kind = listRowAccessory({ pressable, selected, accessory });
36
- const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, tone: tone, truncate: truncate, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
38
+ const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, titleLang: titleLang, tone: tone, truncate: truncate, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
37
39
  const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled', truncate && 'sw-list-row-truncate');
38
40
  const naming = rowNaming(id, {
39
41
  'aria-label': ariaLabel,
@@ -1,6 +1,7 @@
1
1
  import { type SelectOption } from '../select-list.js';
2
2
  import { type FieldSize } from './Field.js';
3
3
  import { type RequiredMark } from './FieldLabelText.js';
4
+ import { type SelectNaming } from './select/select-naming.js';
4
5
  export type { SelectOption };
5
6
  /**
6
7
  * `content` (the default) sizes the field to its longest option, as a native
@@ -12,10 +13,9 @@ export type SelectAction = {
12
13
  label: string;
13
14
  onPress: () => void;
14
15
  };
15
- export type SelectProps = {
16
+ export type { SelectNaming };
17
+ export type SelectProps = SelectNaming & {
16
18
  action?: SelectAction;
17
- label: string;
18
- labelVisuallyHidden?: boolean;
19
19
  onChange: (value: string) => void;
20
20
  options: readonly SelectOption[];
21
21
  size?: FieldSize;
@@ -25,7 +25,10 @@ export type SelectProps = {
25
25
  * (such as ''). It is not an option and never becomes the value.
26
26
  */
27
27
  placeholder?: string;
28
- /** Marks the label as Field does and sets `aria-required`. */
28
+ /**
29
+ * Marks the label as Field does and sets `aria-required` (named by
30
+ * `aria-labelledby`, only `aria-required`: the page draws the words).
31
+ */
29
32
  required?: boolean;
30
33
  /**
31
34
  * `hidden` leaves out the label's " *" while the control stays required
@@ -43,4 +46,4 @@ export type SelectProps = {
43
46
  */
44
47
  width?: SelectWidth;
45
48
  };
46
- export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;
49
+ export declare function Select({ action, label, labelVisuallyHidden, 'aria-labelledby': labelledBy, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;