@scalewing/react 1.42.0 → 1.44.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 (60) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +60 -0
  3. package/dist/components/ActionMenu.d.ts +4 -2
  4. package/dist/components/ActionMenu.js +4 -8
  5. package/dist/components/DescriptionList.d.ts +12 -2
  6. package/dist/components/DescriptionList.js +7 -3
  7. package/dist/components/FileField.d.ts +15 -4
  8. package/dist/components/FileField.js +13 -4
  9. package/dist/components/FilterChips.d.ts +1 -1
  10. package/dist/components/FilterChips.js +1 -1
  11. package/dist/components/FilterChipsRow.d.ts +24 -0
  12. package/dist/components/FilterChipsRow.js +14 -0
  13. package/dist/components/ListRow.d.ts +10 -2
  14. package/dist/components/ListRow.js +5 -4
  15. package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
  16. package/dist/components/action-menu/assert-action-menu.js +21 -0
  17. package/dist/components/description-list-layout.d.ts +10 -0
  18. package/dist/components/description-list-layout.js +13 -0
  19. package/dist/components/file-field/file-field-checks.d.ts +2 -2
  20. package/dist/components/file-field/file-field-checks.js +4 -2
  21. package/dist/components/filter-chips/FilterChip.js +1 -1
  22. package/dist/components/filter-chips/options.d.ts +10 -0
  23. package/dist/components/filter-chips/options.js +8 -0
  24. package/dist/components/filter-chips/validate.d.ts +3 -3
  25. package/dist/components/filter-chips/validate.js +8 -4
  26. package/dist/components/list/ListRowContent.d.ts +5 -2
  27. package/dist/components/list/ListRowContent.js +5 -4
  28. package/dist/components/list/ListRowItem.js +1 -1
  29. package/dist/components/list/row-naming.js +1 -1
  30. package/dist/components/slot.d.ts +9 -0
  31. package/dist/components/slot.js +13 -0
  32. package/dist/css/css-action-menu-trigger.d.ts +6 -2
  33. package/dist/css/css-action-menu-trigger.js +50 -2
  34. package/dist/css/css-action-menu.js +4 -2
  35. package/dist/css/css-components.js +3 -0
  36. package/dist/css/css-description-list.d.ts +1 -0
  37. package/dist/css/css-description-list.js +24 -0
  38. package/dist/css/css-file-field.js +15 -1
  39. package/dist/css/css-filter-chip-tones.d.ts +3 -0
  40. package/dist/css/css-filter-chip-tones.js +28 -0
  41. package/dist/css/css-filter-chips-layout.d.ts +1 -0
  42. package/dist/css/css-filter-chips-layout.js +1 -1
  43. package/dist/css/css-filter-chips-row.d.ts +2 -0
  44. package/dist/css/css-filter-chips-row.js +79 -0
  45. package/dist/css/css-filter-chips.js +8 -0
  46. package/dist/css/css-font-mono.d.ts +3 -0
  47. package/dist/css/css-font-mono.js +23 -0
  48. package/dist/css/css-list-row-truncate.d.ts +2 -0
  49. package/dist/css/css-list-row-truncate.js +29 -0
  50. package/dist/css/css-list.js +5 -1
  51. package/dist/css/css-utilities.js +3 -0
  52. package/dist/css/spacing-classes.d.ts +1 -1
  53. package/dist/css/spacing-classes.js +1 -0
  54. package/dist/css/stylesheet.js +2 -0
  55. package/dist/index.d.ts +6 -4
  56. package/dist/index.js +3 -2
  57. package/dist/styles.css +145 -0
  58. package/package.json +1 -1
  59. package/dist/components/list/row-slot.d.ts +0 -8
  60. package/dist/components/list/row-slot.js +0 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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`.
8
+ - 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`.
9
+ - 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`.
10
+ - 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`.
11
+ - 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`.
12
+ - 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`.
13
+ - 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`.
14
+ - 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`.
15
+
16
+ ### Patch Changes
17
+
18
+ - 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.
19
+
20
+ ## 1.43.0
21
+
22
+ 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.
23
+
3
24
  ## 1.42.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -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={
@@ -790,6 +794,8 @@ const { headings, omissions } = inspectMarkdown(page.body, {
790
794
 
791
795
  `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
796
 
797
+ `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.
798
+
793
799
  ```tsx
794
800
  <DescriptionList>
795
801
  <DescriptionItem
@@ -836,6 +842,8 @@ const { headings, omissions } = inspectMarkdown(page.body, {
836
842
 
837
843
  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
844
 
845
+ `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
+
839
847
  ```tsx
840
848
  <ListRow
841
849
  leading={
@@ -902,6 +910,10 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
902
910
 
903
911
  `Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
904
912
 
913
+ `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.
914
+
915
+ `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.
916
+
905
917
  `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
918
 
907
919
  `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 +1005,8 @@ A strip row with `entry` makes its counts editable: each column holds a `Stepper
993
1005
 
994
1006
  `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
1007
 
1008
+ 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`.
1009
+
996
1010
  ```tsx
997
1011
  <FilterChips
998
1012
  label="Nocturnal surveys"
@@ -1017,6 +1031,52 @@ A strip row with `entry` makes its counts editable: each column holds a `Stepper
1017
1031
  { value: 'sat', label: 'Sat' },
1018
1032
  ]}
1019
1033
  />
1034
+
1035
+ <FilterChips
1036
+ label="Camera traps to check"
1037
+ labelVisuallyHidden
1038
+ multiple
1039
+ value={unchecked}
1040
+ onChange={setUnchecked}
1041
+ options={[
1042
+ { value: 'unchecked', label: 'Not checked this week', count: 2, tone: 'warning' },
1043
+ ]}
1044
+ />
1045
+ ```
1046
+
1047
+ `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`.
1048
+
1049
+ ```tsx
1050
+ <FilterChipsRow
1051
+ overflow="scroll"
1052
+ end={
1053
+ <FilterChips
1054
+ label="Not translated"
1055
+ labelVisuallyHidden
1056
+ multiple
1057
+ countAppearance="inline"
1058
+ value={untranslated}
1059
+ onChange={setUntranslated}
1060
+ options={[
1061
+ {
1062
+ value: 'untranslated',
1063
+ label: 'Not translated',
1064
+ count: 2,
1065
+ tone: 'warning',
1066
+ },
1067
+ ]}
1068
+ />
1069
+ }
1070
+ >
1071
+ <FilterChips
1072
+ label="Kinds"
1073
+ labelVisuallyHidden
1074
+ countAppearance="inline"
1075
+ value={kind}
1076
+ onChange={setKind}
1077
+ options={kinds}
1078
+ />
1079
+ </FilterChipsRow>
1020
1080
  ```
1021
1081
 
1022
1082
  `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
  }
@@ -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
+ }
@@ -64,6 +64,13 @@ export type ListRowProps = {
64
64
  * `ActionMenu` of the row's own actions. It needs its own name.
65
65
  */
66
66
  action?: ReactNode;
67
+ /**
68
+ * Keeps the title and the detail on one line each, cut with "…" where
69
+ * they do not fit, for a long list read at a glance; the full words stay
70
+ * in the row's name and description, and in a `title` tooltip. The
71
+ * value, its detail and the tag keep their words whole.
72
+ */
73
+ truncate?: boolean;
67
74
  /** `none` hides the chevron of a pressable row that acts in place. */
68
75
  accessory?: 'chevron' | 'none';
69
76
  /**
@@ -76,6 +83,7 @@ export type ListRowProps = {
76
83
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
77
84
  * a chevron, the whole row pressing), picks one option in place
78
85
  * (`selected`), or only states something (neither). `unread` marks it new,
79
- * `tag` adds a status before its end, and `action` a control beside it.
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.
80
88
  */
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;
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;
@@ -12,9 +12,10 @@ 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, and
16
+ * `truncate` keeps its title and detail to one line each.
16
17
  */
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, }) {
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, }) {
18
19
  const pressable = onPress !== undefined || href !== undefined;
19
20
  assertListRow({
20
21
  title,
@@ -32,8 +33,8 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
32
33
  });
33
34
  const id = useId();
34
35
  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');
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 }));
37
+ const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled', truncate && 'sw-list-row-truncate');
37
38
  const naming = rowNaming(id, {
38
39
  'aria-label': ariaLabel,
39
40
  value,
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ActionMenuTriggerAppearance } from '../../css/css-action-menu-trigger.js';
3
+ import { type ActionMenuItem } from '../ActionMenu.js';
4
+ /**
5
+ * Fails closed on a menu it cannot draw: an item's blank detail or
6
+ * detailLang (or a detailLang with no detail), an unknown trigger
7
+ * appearance, or an icon trigger with no glyph.
8
+ */
9
+ export declare function assertActionMenu({ items, trigger, triggerAppearance, }: {
10
+ items: readonly ActionMenuItem[];
11
+ trigger: ReactNode;
12
+ triggerAppearance: ActionMenuTriggerAppearance;
13
+ }): void;
@@ -0,0 +1,21 @@
1
+ import { actionMenuTriggerAppearances, } from '../../css/css-action-menu-trigger.js';
2
+ import { hasSlot } from '../slot.js';
3
+ /** Whether an icon trigger has a glyph to draw (blank words are none). */
4
+ function hasGlyph(trigger) {
5
+ return (hasSlot(trigger) && !(typeof trigger === 'string' && trigger.trim() === ''));
6
+ }
7
+ /**
8
+ * Fails closed on a menu it cannot draw: an item's blank detail or
9
+ * detailLang (or a detailLang with no detail), an unknown trigger
10
+ * appearance, or an icon trigger with no glyph.
11
+ */
12
+ export function assertActionMenu({ items, trigger, triggerAppearance, }) {
13
+ if (items.some((item) => (item.detail !== undefined && !item.detail.trim()) ||
14
+ (item.detailLang !== undefined &&
15
+ (!item.detailLang.trim() || item.detail === undefined))))
16
+ throw new RangeError('ActionMenu item detail and detailLang must not be blank; detailLang needs a detail');
17
+ if (!actionMenuTriggerAppearances.includes(triggerAppearance))
18
+ throw new RangeError(`Unknown ActionMenu triggerAppearance: ${String(triggerAppearance)}`);
19
+ if (triggerAppearance === 'icon' && !hasGlyph(trigger))
20
+ throw new RangeError('An icon ActionMenu trigger needs its glyph');
21
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * How a DescriptionList lays out its rows: `columns` (the default), the
3
+ * terms in a start column and the details beside them, each term over its
4
+ * detail below `md`; `spread`, each row's term at its start and its detail
5
+ * at its end, side by side at every width.
6
+ */
7
+ export declare const descriptionListLayouts: readonly ["columns", "spread"];
8
+ export type DescriptionListLayout = (typeof descriptionListLayouts)[number];
9
+ /** Fails closed on a layout the list cannot draw. */
10
+ export declare function assertDescriptionListLayout(layout: unknown): void;
@@ -0,0 +1,13 @@
1
+ import { isOneOf } from '../is-one-of.js';
2
+ /**
3
+ * How a DescriptionList lays out its rows: `columns` (the default), the
4
+ * terms in a start column and the details beside them, each term over its
5
+ * detail below `md`; `spread`, each row's term at its start and its detail
6
+ * at its end, side by side at every width.
7
+ */
8
+ export const descriptionListLayouts = ['columns', 'spread'];
9
+ /** Fails closed on a layout the list cannot draw. */
10
+ export function assertDescriptionListLayout(layout) {
11
+ if (!isOneOf(descriptionListLayouts, layout))
12
+ throw new RangeError(`Unknown DescriptionList layout: ${String(layout)}`);
13
+ }
@@ -1,8 +1,8 @@
1
1
  import type { FileFieldValue } from '../FileField.js';
2
2
  /**
3
3
  * Fails closed on a FileField it cannot name or show: an empty label,
4
- * choose or change words, a blank drop hint, or a chosen file with no name
5
- * or a blank detail.
4
+ * choose or change words, a blank drop hint, or a chosen file with no name,
5
+ * a blank detail or a blank preview address.
6
6
  */
7
7
  export declare function assertFileField(props: {
8
8
  label: string;
@@ -3,8 +3,8 @@ function isBlank(text) {
3
3
  }
4
4
  /**
5
5
  * Fails closed on a FileField it cannot name or show: an empty label,
6
- * choose or change words, a blank drop hint, or a chosen file with no name
7
- * or a blank detail.
6
+ * choose or change words, a blank drop hint, or a chosen file with no name,
7
+ * a blank detail or a blank preview address.
8
8
  */
9
9
  export function assertFileField(props) {
10
10
  for (const key of ['label', 'chooseLabel', 'changeLabel'])
@@ -18,4 +18,6 @@ export function assertFileField(props) {
18
18
  throw new RangeError('FileField value name must be non-empty text');
19
19
  if (props.value.detail !== undefined && isBlank(props.value.detail))
20
20
  throw new RangeError('FileField value detail must be non-empty text');
21
+ if (props.value.preview !== undefined && isBlank(props.value.preview))
22
+ throw new RangeError('FileField value preview must be an image address');
21
23
  }
@@ -13,7 +13,7 @@ function ChipCount({ count, appearance, }) {
13
13
  }
14
14
  /** One chip: a native radio or checkbox under its face. */
15
15
  export function FilterChip({ option, name, type, checked, required, tabStopHere, countAppearance, onCheckedChange, }) {
16
- return (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: checked, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
16
+ return (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet', option.tone === 'warning' && 'sw-filter-chip-warning'), children: [_jsx("input", { checked: checked, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
17
17
  if (!option.disabled)
18
18
  onCheckedChange(event.currentTarget.checked);
19
19
  }, required: required, tabIndex: tabStopHere ? tabStop : undefined, type: type, value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsx(ChipCount, { appearance: countAppearance, count: option.count }))] })] }));
@@ -1,9 +1,19 @@
1
1
  import { type RequiredMark } from '../FieldLabelText.js';
2
+ /**
3
+ * A chip's colour while it is not chosen: `neutral` (the default), the
4
+ * glass chip; `warning`, the warning tint with its words in the warning
5
+ * ink, for a choice that shows what needs a look ("Missing a
6
+ * translation"). Its words must say so; the tint only draws the eye. A
7
+ * chosen chip is the accent, whatever its tone.
8
+ */
9
+ export declare const filterChipTones: readonly ["neutral", "warning"];
10
+ export type FilterChipTone = (typeof filterChipTones)[number];
2
11
  export type FilterChipOption = {
3
12
  value: string;
4
13
  label: string;
5
14
  count?: number;
6
15
  disabled?: boolean;
16
+ tone?: FilterChipTone;
7
17
  };
8
18
  /**
9
19
  * How a chip's count is drawn: `figure` (the default), a muted tabular
@@ -1,3 +1,11 @@
1
+ /**
2
+ * A chip's colour while it is not chosen: `neutral` (the default), the
3
+ * glass chip; `warning`, the warning tint with its words in the warning
4
+ * ink, for a choice that shows what needs a look ("Missing a
5
+ * translation"). Its words must say so; the tint only draws the eye. A
6
+ * chosen chip is the accent, whatever its tone.
7
+ */
8
+ export const filterChipTones = ['neutral', 'warning'];
1
9
  /**
2
10
  * How a chip's count is drawn: `figure` (the default), a muted tabular
3
11
  * figure after a space; `inline`, the label's own words and type, after a
@@ -2,8 +2,8 @@ import { type FilterChipsProps } from './options.js';
2
2
  /**
3
3
  * Fails closed on chips it cannot draw: an empty label, no options, blank
4
4
  * or repeated values, a chosen value that is no option, a count that is
5
- * not a whole number from 0, an unknown count appearance or overflow, and
6
- * a multiple choice asked to be required or given a value that is not a
7
- * list.
5
+ * not a whole number from 0, an unknown chip tone, count appearance or
6
+ * overflow, and a multiple choice asked to be required or given a value
7
+ * that is not a list.
8
8
  */
9
9
  export declare function assertFilterChips(props: FilterChipsProps): void;