@scalewing/react 1.42.0 → 1.45.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 (76) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +65 -1
  3. package/dist/components/ActionMenu.d.ts +4 -2
  4. package/dist/components/ActionMenu.js +4 -8
  5. package/dist/components/Article.d.ts +19 -2
  6. package/dist/components/Article.js +9 -5
  7. package/dist/components/DescriptionList.d.ts +12 -2
  8. package/dist/components/DescriptionList.js +7 -3
  9. package/dist/components/FileField.d.ts +15 -4
  10. package/dist/components/FileField.js +13 -4
  11. package/dist/components/FilterChips.d.ts +1 -1
  12. package/dist/components/FilterChips.js +1 -1
  13. package/dist/components/FilterChipsRow.d.ts +24 -0
  14. package/dist/components/FilterChipsRow.js +14 -0
  15. package/dist/components/ListRow.d.ts +18 -2
  16. package/dist/components/ListRow.js +7 -4
  17. package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
  18. package/dist/components/action-menu/assert-action-menu.js +21 -0
  19. package/dist/components/description-list-layout.d.ts +10 -0
  20. package/dist/components/description-list-layout.js +13 -0
  21. package/dist/components/file-field/file-field-checks.d.ts +2 -2
  22. package/dist/components/file-field/file-field-checks.js +4 -2
  23. package/dist/components/filter-chips/FilterChip.js +1 -1
  24. package/dist/components/filter-chips/options.d.ts +10 -0
  25. package/dist/components/filter-chips/options.js +8 -0
  26. package/dist/components/filter-chips/validate.d.ts +3 -3
  27. package/dist/components/filter-chips/validate.js +8 -4
  28. package/dist/components/list/ListRowContent.d.ts +9 -2
  29. package/dist/components/list/ListRowContent.js +7 -4
  30. package/dist/components/list/ListRowItem.js +1 -1
  31. package/dist/components/list/row-naming.js +1 -1
  32. package/dist/components/slot.d.ts +9 -0
  33. package/dist/components/slot.js +13 -0
  34. package/dist/components/steps/Step.d.ts +10 -1
  35. package/dist/components/steps/Step.js +15 -19
  36. package/dist/components/steps/StepTitleLine.d.ts +19 -0
  37. package/dist/components/steps/StepTitleLine.js +13 -0
  38. package/dist/components/steps/StepTitleWords.d.ts +27 -0
  39. package/dist/components/steps/StepTitleWords.js +21 -0
  40. package/dist/components/steps/assert-step.d.ts +7 -0
  41. package/dist/components/steps/assert-step.js +19 -0
  42. package/dist/css/css-action-menu-trigger.d.ts +6 -2
  43. package/dist/css/css-action-menu-trigger.js +50 -2
  44. package/dist/css/css-action-menu.js +4 -2
  45. package/dist/css/css-components.js +3 -0
  46. package/dist/css/css-description-list.d.ts +1 -0
  47. package/dist/css/css-description-list.js +24 -0
  48. package/dist/css/css-file-field.js +15 -1
  49. package/dist/css/css-filter-chip-tones.d.ts +3 -0
  50. package/dist/css/css-filter-chip-tones.js +28 -0
  51. package/dist/css/css-filter-chips-layout.d.ts +1 -0
  52. package/dist/css/css-filter-chips-layout.js +1 -1
  53. package/dist/css/css-filter-chips-row.d.ts +2 -0
  54. package/dist/css/css-filter-chips-row.js +79 -0
  55. package/dist/css/css-filter-chips.js +8 -0
  56. package/dist/css/css-font-mono.d.ts +3 -0
  57. package/dist/css/css-font-mono.js +23 -0
  58. package/dist/css/css-list-row-truncate.d.ts +2 -0
  59. package/dist/css/css-list-row-truncate.js +29 -0
  60. package/dist/css/css-list.js +5 -1
  61. package/dist/css/css-steps-title-detail.d.ts +2 -0
  62. package/dist/css/css-steps-title-detail.js +36 -0
  63. package/dist/css/css-steps.d.ts +2 -2
  64. package/dist/css/css-steps.js +6 -2
  65. package/dist/css/css-utilities.js +3 -0
  66. package/dist/css/spacing-classes.d.ts +1 -1
  67. package/dist/css/spacing-classes.js +1 -0
  68. package/dist/css/stylesheet.js +2 -0
  69. package/dist/index.d.ts +6 -4
  70. package/dist/index.js +3 -2
  71. package/dist/list-options.d.ts +7 -6
  72. package/dist/list-options.js +8 -6
  73. package/dist/styles.css +164 -0
  74. package/package.json +1 -1
  75. package/dist/components/list/row-slot.d.ts +0 -8
  76. package/dist/components/list/row-slot.js +0 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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`.
8
+ - 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`.
9
+ - 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`.
10
+
11
+ ### Patch Changes
12
+
13
+ - 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`.
14
+
15
+ ## 1.44.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 2af39a5: `ActionMenu` `triggerAppearance="icon"`: the consumer's `trigger` glyph alone in a round quiet button as wide as it is tall (the sm control height, 44 px on a coarse pointer), muted with no fill, outline or chevron, the text colour on the quiet fill on hover and while open; named by `label`, for a row's own ••• menu. An icon trigger without a glyph throws a `RangeError`. New class `sw-action-menu-trigger-icon`; `actionMenuTriggerAppearances` gains `icon`. No new tokens, no new dependency. Request: `docs/requests/teisoro-action-menu-icon-trigger.md`.
20
+ - 38a1715: `DescriptionList` `layout`: `columns` (the default, unchanged) or `spread`, each row's term at its start and its detail at its end, end-aligned, side by side at every width, for a short box of facts. New class `sw-description-list-spread`, new exports `descriptionListLayouts` and `DescriptionListLayout`; an unknown layout throws a `RangeError`. No new tokens, no new dependency. Request: `docs/requests/teisoro-description-list-spread.md`.
21
+ - f426251: `FileField` `value.preview`: the address of a small image of a chosen photo, such as an object URL the consumer made from the `File`, drawn as a decorative 3:2 thumbnail (a space-8 by a space-6, cropped to fill, at the small radius) before the file's name in place of `icon`. A blank preview throws a `RangeError`. New class `sw-file-field-preview`. No new tokens, no new dependency. Request: `docs/requests/teisoro-file-field-preview.md`.
22
+ - 76ae2b4: `FilterChips` option `tone`: `neutral` (the default, unchanged) or `warning`, which draws that chip, while it is not chosen, in the warning tint with its words and count in the warning ink (a warning `Notice`'s, 4.5:1 in every palette and scheme); chosen it is the accent and at a zero count quiet, like every chip. New class `sw-filter-chip-warning`, new exports `filterChipTones` and `FilterChipTone`; an unknown tone throws a `RangeError`. No new tokens, no new dependency. Request: `docs/requests/teisoro-filter-chip-tone.md`.
23
+ - 49f90af: `FilterChipsRow`: several `FilterChips` groups on one row, each keeping its own name, choices and Tab stops, an optional `end` group at the row's end (at its own line's end when the row wraps), and `overflow` `wrap` (the default) or `scroll`, where every group's chips stay on one row that scrolls sideways together inside the row's own box, never the page, a group's own scroll giving way. New classes `sw-filter-chips-row`, `sw-filter-chips-row-end`, `sw-filter-chips-row-scroll`; new exports `FilterChipsRow` and `FilterChipsRowProps`; an unknown overflow throws a `RangeError`. The private slot check `hasSlot` moved from the list rows to `components/slot.ts`, shared by both. No new tokens, no new dependency. Request: `docs/requests/teisoro-filter-chips-row.md`.
24
+ - a6c790c: `ListRow` `truncate`: opt-in, keeps the title and the detail line to one line each, cut with "…" where they do not fit; the unread dot follows the cut words, the whole words stay in the row's name, description and a `title` tooltip, and the value, tag and action keep their words whole. New classes `sw-list-row-truncate` and `sw-list-row-title-text`. Rows without it are unchanged. No new tokens, no new dependency. Request: `docs/requests/teisoro-list-row-truncate.md`.
25
+ - d4647b8: `sw-container-reading`: a utility class that centres a column at the reading width (`--sw-reading-max`, 46rem, `Article`'s), as `sw-container` does at the container width, and full width below it; for a form or text that reads better narrower than the page. No new tokens, no new dependency. Request: `docs/requests/teisoro-container-reading.md`.
26
+ - 7da3f25: `sw-font-mono`: a utility class that sets an element, or a native text control such as a `Field`'s `textarea`, in the platform's monospace face (`ui-monospace, monospace`, the generic CSS names only, so no face is chosen) at the size it already has, outranking the canvas's text control rules. For words read as typed code or marks, such as a Markdown source being written. No new tokens, no new dependency. Request: `docs/requests/teisoro-font-mono.md`.
27
+
28
+ ### Patch Changes
29
+
30
+ - 68be1ea: `ListRow` `truncate`: the one-line rule for the title's words box (`sw-list-row-title-text`) is scoped to truncated rows (`.sw-list-row-truncate .sw-list-row-title-text`), as the detail's already was; a truncated row looks the same.
31
+
32
+ ## 1.43.0
33
+
34
+ Not published: its release validation failed before publication (a gallery test timed out, fixed in #211). Its changes ship in 1.44.0, listed there.
35
+
3
36
  ## 1.42.0
4
37
 
5
38
  ### 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,8 @@ 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
+ 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
+
147
149
  ```tsx
148
150
  <FileField
149
151
  label="Field guide"
@@ -285,6 +287,8 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
285
287
 
286
288
  `ActionMenu` `triggerAppearance="text"` (opt-in; default `"framed"`, `actionMenuTriggerAppearances`) draws the trigger as its words (`trigger`, or `label`) in label type at medium weight in the text color, with the shared muted chevron pointing down after them and no fill or outline; the quiet grey fill shows on hover and while the menu is open. It is the sm control height (the 44 px target on a coarse pointer). Name the trigger with `label` as before, and include the words shown in it (WCAG 2.5.3, so voice control can say them): a person's name that opens their account menu takes a `label` such as "Account, Ada Okafor". The trigger's words are one flex item (mixed inline content keeps its spaces), then the chevron. Forced colors draw its border in the system's border color, the chevron in CanvasText, and an open trigger's border in Highlight. An unknown value throws a `RangeError`.
287
289
 
290
+ `triggerAppearance="icon"` (opt-in) draws the `trigger` glyph alone in a round quiet button, as wide as it is tall (the sm control height, 44 px on a coarse pointer), in the muted colour with no fill, outline or chevron; it turns to the text colour on the quiet fill on hover and while the menu is open. Use it for a row's own ••• menu, named by `label` ("More for Heron calls"); the glyph says nothing on its own. An icon trigger without a `trigger` glyph throws a `RangeError`. Forced colors draw it in ButtonText with its kept border, Highlight while open.
291
+
288
292
  ```tsx
289
293
  <ActionMenu
290
294
  header={
@@ -746,6 +750,8 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
746
750
 
747
751
  `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`.
748
752
 
753
+ `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`.
754
+
749
755
  ```tsx
750
756
  <Article
751
757
  cover={{ src: cover.url, alt: cover.alt }}
@@ -790,6 +796,8 @@ const { headings, omissions } = inspectMarkdown(page.body, {
790
796
 
791
797
  `DescriptionList` pairs terms with what each says, one `DescriptionItem` per row: a `dl` whose items each group a `dt` (`term`) with its `dd` (`children`). From `md` up the term sits in a start column as wide as the widest term, up to 40% of the list, and the detail beside it; every row aligns to its top, so a term and its detail's first line start level, and a hairline divides the rows, with no space above the first row or below the last. Below `md` each term sits over its detail. The list sets only the layout: pass your own `Text` for the term and the detail. Each child of a term or a detail is its own line with its own line height (a column), as wide as its content unless it sets its own width (a `Progress` fills the detail; a `Field` keeps its input's width): wrap inline content, such as words and a link on one line, in one element such as an `Inline`. Long unbreakable words wrap inside their column. A `dl` has no ARIA role, so name a list with a visible heading, and `aria-labelledby` on the `section` around it, rather than an `aria-label` on the list.
792
798
 
799
+ `layout="spread"` (opt-in; the default is `columns`) sets each row on its own two columns at every width, for a short box of facts such as a side panel's details: the term at the row's start, as wide as its words up to half the row, and the detail at the row's end, its lines end-aligned. The hairlines, padding and top alignment are the same, and a phone keeps each term beside its detail. An unknown `layout` throws a `RangeError`; `descriptionListLayouts` lists the layouts.
800
+
793
801
  ```tsx
794
802
  <DescriptionList>
795
803
  <DescriptionItem
@@ -836,6 +844,10 @@ const { headings, omissions } = inspectMarkdown(page.body, {
836
844
 
837
845
  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.
838
846
 
847
+ `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.
848
+
849
+ `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`.
850
+
839
851
  ```tsx
840
852
  <ListRow
841
853
  leading={
@@ -902,6 +914,10 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
902
914
 
903
915
  `Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
904
916
 
917
+ `sw-container-reading` centres a column at the reading width (`--sw-reading-max`, 46rem, `Article`'s), as `sw-container` centres a page's column at the container width (72rem): `width: 100%`, the max width and auto inline margins, full width below it. Use it for a form or text that reads better narrower than the page, such as an open form of one column. As with `sw-container`, the width is the content box's, so put padding or a border on a child.
918
+
919
+ `sw-font-mono` sets an element, or a native text control such as the `textarea` in a `Field`, in the platform's monospace face (`ui-monospace, monospace`, the generic names only) at the size it already has, for words read as typed code or marks, such as a Markdown source being written. It outranks the canvas's text control rules.
920
+
905
921
  `Text` has no margin of its own. Its default `p` and `h1`–`h4` elements drop the browser's block margins, so the `gap` of the `Stack` or `Inline` around it alone sets the spacing; put more space in that `gap`, not in a margin. The reset has zero specificity (`:where(.sw-text-*)`), so an authored margin, such as `sw-sr-only` or a consumer class, still applies.
906
922
 
907
923
  `Text` takes an optional `align` (`start`, `center`, `end`) mapped to generated `sw-text-align-*` classes, for a heading that must stay centered when it wraps.
@@ -993,6 +1009,8 @@ A strip row with `entry` makes its counts editable: each column holds a `Stepper
993
1009
 
994
1010
  `FilterChips` options for chips that sit above a list, all opt-in: `labelVisuallyHidden` keeps `label` as the group's name without drawing it, for chips whose words and place already say what they filter; `countAppearance="inline"` (`filterChipCountAppearances`, default `"figure"`) writes the count in the chip's own words after a middle dot ("Open · 3"), the dot hidden so the name reads "Open 3"; `overflow="scroll"` (`filterChipsOverflows`, default `"wrap"`) keeps the chips on one row that scrolls sideways inside its own box, never the page, each chip on one line, a focused chip scrolled into view and the focus ring unclipped. `multiple` turns several chips on at once, such as the days of a week: each chip is a native checkbox and its own Tab stop, `value` is a list of the chosen values (empty for none) and `onChange` receives every chosen value in the options' order. A multiple group cannot be `required` (nothing makes a checkbox group required); say what is needed in `error`. A value that is no option, a multiple `value` that is not a list, `required` with `multiple`, or an unknown count appearance or overflow throws a `RangeError`.
995
1011
 
1012
+ An option's `tone` (`filterChipTones`, `FilterChipTone`; default `"neutral"`) set to `"warning"` draws that chip, while it is not chosen, in the warning tint with its words and count in the warning ink (a warning `Notice`'s fill and words, 4.5:1 in every palette and scheme), for a choice that shows what needs a look, such as documents missing a translation. Its words must say why; the tint only draws the eye. Chosen, it is the accent like every chip; with a zero count it is quiet like every chip; it keeps the chips' hairline, so an `error`'s danger edge still shows; forced colours draw it as every chip. An unknown tone throws a `RangeError`.
1013
+
996
1014
  ```tsx
997
1015
  <FilterChips
998
1016
  label="Nocturnal surveys"
@@ -1017,6 +1035,52 @@ A strip row with `entry` makes its counts editable: each column holds a `Stepper
1017
1035
  { value: 'sat', label: 'Sat' },
1018
1036
  ]}
1019
1037
  />
1038
+
1039
+ <FilterChips
1040
+ label="Camera traps to check"
1041
+ labelVisuallyHidden
1042
+ multiple
1043
+ value={unchecked}
1044
+ onChange={setUnchecked}
1045
+ options={[
1046
+ { value: 'unchecked', label: 'Not checked this week', count: 2, tone: 'warning' },
1047
+ ]}
1048
+ />
1049
+ ```
1050
+
1051
+ `FilterChipsRow` lays several `FilterChips` groups on one row, for filters that need more than one kind of choice, such as kinds (one choice) and a toggle that narrows them (a checkbox). Each group keeps its own name, choices and Tab stops; the row is a plain `div` that lays them out a space-2 apart, their chips level along the bottom (an error under one group lifts its chips above the others'). `end` sets a group apart at the row's end, and at its own line's end when the row wraps. `overflow` (`"wrap"` by default, or `"scroll"`) wraps the groups onto more lines, or keeps every group's chips on one row that scrolls sideways together inside the row's own box, never the page, the focus ring unclipped and a focused chip scrolled into view; inside a scrolling row a group's own `overflow` gives way, and a group is as wide as its chips, so a drawn label or an `error` wraps under them. An empty `end` draws nothing; an unknown `overflow` throws a `RangeError`.
1052
+
1053
+ ```tsx
1054
+ <FilterChipsRow
1055
+ overflow="scroll"
1056
+ end={
1057
+ <FilterChips
1058
+ label="Not translated"
1059
+ labelVisuallyHidden
1060
+ multiple
1061
+ countAppearance="inline"
1062
+ value={untranslated}
1063
+ onChange={setUntranslated}
1064
+ options={[
1065
+ {
1066
+ value: 'untranslated',
1067
+ label: 'Not translated',
1068
+ count: 2,
1069
+ tone: 'warning',
1070
+ },
1071
+ ]}
1072
+ />
1073
+ }
1074
+ >
1075
+ <FilterChips
1076
+ label="Kinds"
1077
+ labelVisuallyHidden
1078
+ countAppearance="inline"
1079
+ value={kind}
1080
+ onChange={setKind}
1081
+ options={kinds}
1082
+ />
1083
+ </FilterChipsRow>
1020
1084
  ```
1021
1085
 
1022
1086
  `DateField` is a controlled date-only field that draws its own calendar. Supply `value`, `min`, and `max` as valid `YYYY-MM-DD` dates; use an empty `value` for a blank field. Its callback returns a date-only string or an empty string, without timezone conversion. Invalid serialized dates and inverted bounds throw; a value outside the bounds remains visible with invalid styling. `required` marks the label with the same `aria-hidden` asterisk as `Field` and sets the entry's native `required`. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so its label row and its gap to the entry match a `Field` beside it. For a blank required field, supply `error` when form validation runs to show the message and invalid styling.
@@ -51,8 +51,10 @@ export type ActionMenuProps = {
51
51
  /**
52
52
  * `text` draws the trigger as its words with a chevron after them, no
53
53
  * fill or outline until hovered or open, such as a person's name opening
54
- * their account menu; `framed` (the default) is the small outlined
55
- * control.
54
+ * their account menu; `icon` draws the `trigger` glyph alone in a round
55
+ * quiet button with no chevron, such as a row's ••• menu, named by
56
+ * `label` (an icon trigger needs a `trigger`); `framed` (the default) is
57
+ * the small outlined control.
56
58
  */
57
59
  triggerAppearance?: ActionMenuTriggerAppearance;
58
60
  };
@@ -2,8 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId, useLayoutEffect, useRef, useState, } from 'react';
4
4
  import { cx } from '../class-names.js';
5
- import { actionMenuTriggerAppearances, actionMenuTriggerTextClass, } from '../css/css-action-menu-trigger.js';
5
+ import { actionMenuTriggerAppearances, actionMenuTriggerIconClass, actionMenuTriggerTextClass, } from '../css/css-action-menu-trigger.js';
6
6
  import { ActionMenuList } from './action-menu/ActionMenuList.js';
7
+ import { assertActionMenu } from './action-menu/assert-action-menu.js';
7
8
  import { tabStop } from '../tab-stop.js';
8
9
  export { actionMenuTriggerAppearances };
9
10
  function nextEnabled(items, current, direction) {
@@ -22,12 +23,7 @@ export function ActionMenu({ label, items, header, trigger, disabled = false, al
22
23
  const openRef = useRef(false);
23
24
  const [open, setOpen] = useState(false);
24
25
  const [focusIndex, setFocusIndex] = useState(-1);
25
- if (items.some((item) => (item.detail !== undefined && !item.detail.trim()) ||
26
- (item.detailLang !== undefined &&
27
- (!item.detailLang.trim() || item.detail === undefined))))
28
- throw new RangeError('ActionMenu item detail and detailLang must not be blank; detailLang needs a detail');
29
- if (!actionMenuTriggerAppearances.includes(triggerAppearance))
30
- throw new RangeError(`Unknown ActionMenu triggerAppearance: ${String(triggerAppearance)}`);
26
+ assertActionMenu({ items, trigger, triggerAppearance });
31
27
  const enabled = !disabled && items.some((item) => !item.disabled);
32
28
  useLayoutEffect(() => {
33
29
  if (!enabled && open)
@@ -94,5 +90,5 @@ export function ActionMenu({ label, items, header, trigger, disabled = false, al
94
90
  close(true);
95
91
  item.onSelect();
96
92
  }
97
- return (_jsxs("div", { className: "sw-action-menu", children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: cx('sw-action-menu-trigger', triggerAppearance === 'text' && actionMenuTriggerTextClass), disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, tabIndex: tabStop, type: "button", children: triggerAppearance === 'text' ? (_jsx("span", { children: trigger ?? label })) : ((trigger ?? label)) }), open ? (_jsx(ActionMenuList, { align: align, focusIndex: focusIndex, header: header, id: menuId, items: items, label: label, onDismiss: () => close(), onFocusIndex: setFocusIndex, onKeyDown: onMenuKeyDown, onSelect: select, triggerRef: triggerRef })) : null] }));
93
+ return (_jsxs("div", { className: "sw-action-menu", children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: cx('sw-action-menu-trigger', triggerAppearance === 'text' && actionMenuTriggerTextClass, triggerAppearance === 'icon' && actionMenuTriggerIconClass), disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, tabIndex: tabStop, type: "button", children: triggerAppearance === 'text' ? (_jsx("span", { children: trigger ?? label })) : ((trigger ?? label)) }), open ? (_jsx(ActionMenuList, { align: align, focusIndex: focusIndex, header: header, id: menuId, items: items, label: label, onDismiss: () => close(), onFocusIndex: setFocusIndex, onKeyDown: onMenuKeyDown, onSelect: select, triggerRef: triggerRef })) : null] }));
98
94
  }
@@ -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,15 +1,25 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type DescriptionListLayout } from './description-list-layout.js';
3
+ export { descriptionListLayouts, type DescriptionListLayout, } from './description-list-layout.js';
2
4
  export type DescriptionListProps = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
3
5
  /** The list's `DescriptionItem`s. */
4
6
  children: ReactNode;
7
+ /**
8
+ * `columns` (the default): the terms in a start column and the details
9
+ * beside them, each term over its detail below `md`. `spread`: each
10
+ * row's term at its start and its detail at its end, end-aligned, side
11
+ * by side at every width, for a short box of facts.
12
+ */
13
+ layout?: DescriptionListLayout;
5
14
  };
6
15
  /**
7
16
  * Terms and what each one says, one item per row: the term in a start
8
17
  * column as wide as the widest term (up to 40% of the list), the detail
9
18
  * beside it, a hairline between rows, every row aligned to its top. Below
10
- * the layout breakpoint (`md`) each term sits over its detail.
19
+ * the layout breakpoint (`md`) each term sits over its detail. `spread`
20
+ * sets each detail at its row's end instead, beside its term at every width.
11
21
  */
12
- export declare function DescriptionList({ children, className, ...rest }: DescriptionListProps): import("react").JSX.Element;
22
+ export declare function DescriptionList({ children, className, layout, ...rest }: DescriptionListProps): import("react").JSX.Element;
13
23
  export type DescriptionItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
14
24
  /**
15
25
  * What the row describes, such as a name; the consumer's own `Text`. Each
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../class-names.js';
3
+ import { assertDescriptionListLayout, } from './description-list-layout.js';
4
+ export { descriptionListLayouts, } from './description-list-layout.js';
3
5
  /**
4
6
  * Terms and what each one says, one item per row: the term in a start
5
7
  * column as wide as the widest term (up to 40% of the list), the detail
6
8
  * beside it, a hairline between rows, every row aligned to its top. Below
7
- * the layout breakpoint (`md`) each term sits over its detail.
9
+ * the layout breakpoint (`md`) each term sits over its detail. `spread`
10
+ * sets each detail at its row's end instead, beside its term at every width.
8
11
  */
9
- export function DescriptionList({ children, className, ...rest }) {
10
- return (_jsx("dl", { className: cx('sw-description-list', className), ...rest, children: children }));
12
+ export function DescriptionList({ children, className, layout = 'columns', ...rest }) {
13
+ assertDescriptionListLayout(layout);
14
+ return (_jsx("dl", { className: cx('sw-description-list', layout === 'spread' && 'sw-description-list-spread', className), ...rest, children: children }));
11
15
  }
12
16
  /** One row of a `DescriptionList`: a `dt` and its `dd` in one group. */
13
17
  export function DescriptionItem({ term, children, className, ...rest }) {
@@ -4,6 +4,13 @@ export type FileFieldValue = {
4
4
  name: string;
5
5
  /** A muted line under the name, such as "4 pages · 1.6 MB". */
6
6
  detail?: string;
7
+ /**
8
+ * The address of a small image of a chosen photo, such as an object URL
9
+ * the consumer made from the `File` (and revokes): drawn as a thumbnail
10
+ * before the name in place of `icon`, hidden from assistive technology,
11
+ * so the name says which file it is.
12
+ */
13
+ preview?: string;
7
14
  };
8
15
  export type FileFieldProps = {
9
16
  /** The field's label, drawn above it as Field's, and part of its button's name. */
@@ -29,7 +36,10 @@ export type FileFieldProps = {
29
36
  changeLabel: string;
30
37
  /** Muted words after `chooseLabel` for a fine pointer, such as "or drop it here". */
31
38
  dropLabel?: string;
32
- /** A consumer mark before the chosen file's name (Lucide), hidden from assistive technology. */
39
+ /**
40
+ * A consumer mark before the chosen file's name (Lucide), hidden from
41
+ * assistive technology; not drawn when `value.preview` is set.
42
+ */
33
43
  icon?: ReactNode;
34
44
  disabled?: boolean;
35
45
  /** Prefixes the ids the field gives its parts (its button is `${id}-button`). */
@@ -37,8 +47,9 @@ export type FileFieldProps = {
37
47
  };
38
48
  /**
39
49
  * One file chosen by a press or dropped on the field: empty, a wide zone
40
- * that opens the system picker; chosen, the file's name and detail with a
41
- * button to choose another. It hands the `File` to the consumer and shows
42
- * what the consumer passes back as `value`; it never reads or uploads it.
50
+ * that opens the system picker; chosen, the file's name and detail (after
51
+ * 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
53
+ * consumer passes back as `value`; it never reads or uploads it.
43
54
  */
44
55
  export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, id, }: FileFieldProps): import("react").JSX.Element;
@@ -17,11 +17,20 @@ function hasMark(icon) {
17
17
  icon !== '' &&
18
18
  typeof icon !== 'boolean');
19
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
+ }
20
28
  /**
21
29
  * One file chosen by a press or dropped on the field: empty, a wide zone
22
- * that opens the system picker; chosen, the file's name and detail with a
23
- * button to choose another. It hands the `File` to the consumer and shows
24
- * what the consumer passes back as `value`; it never reads or uploads it.
30
+ * that opens the system picker; chosen, the file's name and detail (after
31
+ * 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
33
+ * consumer passes back as `value`; it never reads or uploads it.
25
34
  */
26
35
  export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, id, }) {
27
36
  assertFileField({ label, chooseLabel, changeLabel, dropLabel, value });
@@ -62,5 +71,5 @@ export function FileField({ label, labelVisuallyHidden = false, description, err
62
71
  id: ids.button,
63
72
  };
64
73
  const open = () => inputRef.current?.click();
65
- 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: [!hasMark(icon) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-file-field-icon", children: icon })), _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
+ 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 })] }));
66
75
  }
@@ -1,5 +1,5 @@
1
1
  import { type FilterChipsProps } from './filter-chips/options.js';
2
- export { filterChipCountAppearances, filterChipsOverflows, type FilterChipCountAppearance, type FilterChipOption, type FilterChipsOverflow, type FilterChipsProps, } from './filter-chips/options.js';
2
+ export { filterChipCountAppearances, filterChipsOverflows, filterChipTones, type FilterChipCountAppearance, type FilterChipOption, type FilterChipsOverflow, type FilterChipsProps, type FilterChipTone, } from './filter-chips/options.js';
3
3
  /**
4
4
  * A labelled row of chips: one choice (radios), or several at once
5
5
  * (`multiple`, checkboxes), each chip optionally counted.
@@ -7,7 +7,7 @@ import { FieldErrorRegion } from './FieldErrorRegion.js';
7
7
  import { showsRequiredMark } from './FieldLabelText.js';
8
8
  import { FilterChip } from './filter-chips/FilterChip.js';
9
9
  import { assertFilterChips } from './filter-chips/validate.js';
10
- export { filterChipCountAppearances, filterChipsOverflows, } from './filter-chips/options.js';
10
+ export { filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './filter-chips/options.js';
11
11
  /** The values a change leaves on, in the options' order. */
12
12
  function toggled(options, chosen, value, checked) {
13
13
  return options
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type FilterChipsOverflow } from './filter-chips/options.js';
3
+ export type FilterChipsRowProps = {
4
+ /** The row's chip groups, each a `FilterChips` with its own name. */
5
+ children: ReactNode;
6
+ /**
7
+ * A group set apart at the row's end, such as one toggle of another kind
8
+ * after the filters ("Missing a translation"); on its own line's end when
9
+ * the row wraps.
10
+ */
11
+ end?: ReactNode;
12
+ /**
13
+ * `wrap` (the default): the groups wrap onto more lines. `scroll`: every
14
+ * group's chips stay on one row that scrolls sideways together inside
15
+ * the row's own box, never the page; a group's own `overflow` gives way.
16
+ */
17
+ overflow?: FilterChipsOverflow;
18
+ };
19
+ /**
20
+ * Several chip groups on one row: the groups keep their own names and
21
+ * choices (a radio group and a checkbox, say), and the row lays them out
22
+ * together, wrapping or scrolling as one, with an optional group at its end.
23
+ */
24
+ export declare function FilterChipsRow({ children, end, overflow, }: FilterChipsRowProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../class-names.js';
3
+ import { hasSlot } from './slot.js';
4
+ import { filterChipsOverflows, } from './filter-chips/options.js';
5
+ /**
6
+ * Several chip groups on one row: the groups keep their own names and
7
+ * choices (a radio group and a checkbox, say), and the row lays them out
8
+ * together, wrapping or scrolling as one, with an optional group at its end.
9
+ */
10
+ export function FilterChipsRow({ children, end, overflow = 'wrap', }) {
11
+ if (!filterChipsOverflows.includes(overflow))
12
+ throw new RangeError(`Unknown FilterChipsRow overflow: ${String(overflow)}`);
13
+ return (_jsxs("div", { className: cx('sw-filter-chips-row', overflow === 'scroll' && 'sw-filter-chips-row-scroll'), children: [children, hasSlot(end) ? (_jsx("div", { className: "sw-filter-chips-row-end", children: end })) : null] }));
14
+ }
@@ -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". */
@@ -64,6 +71,13 @@ export type ListRowProps = {
64
71
  * `ActionMenu` of the row's own actions. It needs its own name.
65
72
  */
66
73
  action?: ReactNode;
74
+ /**
75
+ * Keeps the title and the detail on one line each, cut with "…" where
76
+ * they do not fit, for a long list read at a glance; the full words stay
77
+ * in the row's name and description, and in a `title` tooltip. The
78
+ * value, its detail and the tag keep their words whole.
79
+ */
80
+ truncate?: boolean;
67
81
  /** `none` hides the chevron of a pressable row that acts in place. */
68
82
  accessory?: 'chevron' | 'none';
69
83
  /**
@@ -76,6 +90,8 @@ export type ListRowProps = {
76
90
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
77
91
  * a chevron, the whole row pressing), picks one option in place
78
92
  * (`selected`), or only states something (neither). `unread` marks it new,
79
- * `tag` adds a status before its end, and `action` a control beside it.
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.
80
96
  */
81
- export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, '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,12 +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, and `action` a control beside it.
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.
16
18
  */
17
- 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, '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, }) {
18
20
  const pressable = onPress !== undefined || href !== undefined;
19
21
  assertListRow({
20
22
  title,
23
+ titleLang,
21
24
  tone,
22
25
  titleAppearance,
23
26
  valueTone,
@@ -32,8 +35,8 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
32
35
  });
33
36
  const id = useId();
34
37
  const kind = listRowAccessory({ pressable, selected, accessory });
35
- const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, tone: tone, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
36
- const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled');
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 }));
39
+ const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled', truncate && 'sw-list-row-truncate');
37
40
  const naming = rowNaming(id, {
38
41
  'aria-label': ariaLabel,
39
42
  value,