@scalewing/react 1.39.0 → 1.41.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 (181) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +255 -5
  3. package/dist/calendar-labels.d.ts +2 -0
  4. package/dist/calendar-labels.js +7 -0
  5. package/dist/calendar-month.d.ts +6 -0
  6. package/dist/calendar-month.js +15 -0
  7. package/dist/components/BarChart.d.ts +20 -4
  8. package/dist/components/BarChart.js +13 -6
  9. package/dist/components/ButtonGroup.d.ts +8 -1
  10. package/dist/components/ButtonGroup.js +11 -2
  11. package/dist/components/DateField.d.ts +14 -2
  12. package/dist/components/DateField.js +14 -11
  13. package/dist/components/DateRangeButton.d.ts +65 -0
  14. package/dist/components/DateRangeButton.js +54 -0
  15. package/dist/components/DenominationGrid.d.ts +3 -2
  16. package/dist/components/DenominationGrid.js +21 -3
  17. package/dist/components/Dialog.d.ts +29 -13
  18. package/dist/components/Dialog.js +22 -20
  19. package/dist/components/Field.d.ts +13 -2
  20. package/dist/components/Field.js +14 -38
  21. package/dist/components/FilterChips.d.ts +7 -37
  22. package/dist/components/FilterChips.js +32 -24
  23. package/dist/components/ListGroup.d.ts +33 -0
  24. package/dist/components/ListGroup.js +19 -0
  25. package/dist/components/ListRow.d.ts +60 -0
  26. package/dist/components/ListRow.js +60 -0
  27. package/dist/components/NavCount.d.ts +21 -0
  28. package/dist/components/NavCount.js +23 -0
  29. package/dist/components/SectionNav.js +1 -8
  30. package/dist/components/SegmentedControl.d.ts +17 -1
  31. package/dist/components/SegmentedControl.js +11 -3
  32. package/dist/components/StatTile.d.ts +11 -4
  33. package/dist/components/StatTile.js +11 -4
  34. package/dist/components/Switch.d.ts +16 -1
  35. package/dist/components/Switch.js +17 -6
  36. package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
  37. package/dist/components/action-bar/ActionBarSummary.js +11 -2
  38. package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
  39. package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
  40. package/dist/components/calendar/CalendarPopover.d.ts +25 -0
  41. package/dist/components/calendar/CalendarPopover.js +22 -0
  42. package/dist/components/calendar/MonthStep.d.ts +16 -0
  43. package/dist/components/calendar/MonthStep.js +17 -0
  44. package/dist/components/calendar/use-grid-focus.d.ts +18 -0
  45. package/dist/components/calendar/use-grid-focus.js +35 -0
  46. package/dist/components/calendar/wrap-tab.d.ts +7 -0
  47. package/dist/components/calendar/wrap-tab.js +29 -0
  48. package/dist/components/date-field/CalendarDialog.js +9 -67
  49. package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
  50. package/dist/components/date-field/CalendarEntryButton.js +16 -0
  51. package/dist/components/date-field/CalendarHeader.js +2 -11
  52. package/dist/components/date-field/TypedEntry.d.ts +26 -0
  53. package/dist/components/date-field/TypedEntry.js +13 -0
  54. package/dist/components/date-range/DateRangePopover.d.ts +7 -0
  55. package/dist/components/date-range/DateRangePopover.js +17 -0
  56. package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
  57. package/dist/components/date-range/DateRangeSheet.js +15 -0
  58. package/dist/components/date-range/RangeMonths.d.ts +28 -0
  59. package/dist/components/date-range/RangeMonths.js +50 -0
  60. package/dist/components/date-range/RangePresets.d.ts +16 -0
  61. package/dist/components/date-range/RangePresets.js +10 -0
  62. package/dist/components/date-range/RangeSummary.d.ts +15 -0
  63. package/dist/components/date-range/RangeSummary.js +11 -0
  64. package/dist/components/date-range/range-panel-props.d.ts +31 -0
  65. package/dist/components/date-range/range-panel-props.js +1 -0
  66. package/dist/components/date-range/use-range-popup.d.ts +21 -0
  67. package/dist/components/date-range/use-range-popup.js +53 -0
  68. package/dist/components/denomination-grid/CountCell.d.ts +16 -0
  69. package/dist/components/denomination-grid/CountCell.js +14 -0
  70. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
  71. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  72. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  73. package/dist/components/denomination-grid/EntryCell.js +2 -2
  74. package/dist/components/denomination-grid/StripRow.js +1 -10
  75. package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
  76. package/dist/components/denomination-grid/TransposedStrip.js +43 -0
  77. package/dist/components/denomination-grid/types.d.ts +31 -0
  78. package/dist/components/denomination-grid/types.js +7 -1
  79. package/dist/components/denomination-grid/validate-options.d.ts +7 -0
  80. package/dist/components/denomination-grid/validate-options.js +27 -0
  81. package/dist/components/dialog/DialogFooter.d.ts +6 -5
  82. package/dist/components/dialog/DialogFooter.js +6 -6
  83. package/dist/components/dialog/validate.d.ts +24 -0
  84. package/dist/components/dialog/validate.js +23 -0
  85. package/dist/components/field/field-checks.d.ts +34 -0
  86. package/dist/components/field/field-checks.js +42 -0
  87. package/dist/components/filter-chips/FilterChip.d.ts +16 -0
  88. package/dist/components/filter-chips/FilterChip.js +20 -0
  89. package/dist/components/filter-chips/options.d.ts +75 -0
  90. package/dist/components/filter-chips/options.js +11 -0
  91. package/dist/components/filter-chips/validate.d.ts +9 -0
  92. package/dist/components/filter-chips/validate.js +48 -0
  93. package/dist/components/link-press.d.ts +7 -0
  94. package/dist/components/link-press.js +13 -0
  95. package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
  96. package/dist/components/list/ListRowAccessoryMark.js +7 -0
  97. package/dist/components/list/ListRowContent.d.ts +22 -0
  98. package/dist/components/list/ListRowContent.js +11 -0
  99. package/dist/components/list/list-row-container.d.ts +5 -0
  100. package/dist/components/list/list-row-container.js +6 -0
  101. package/dist/components/use-media-query.d.ts +5 -0
  102. package/dist/components/use-media-query.js +18 -0
  103. package/dist/css/chart-tones.d.ts +2 -0
  104. package/dist/css/chart-tones.js +5 -1
  105. package/dist/css/chevron.d.ts +6 -0
  106. package/dist/css/chevron.js +14 -0
  107. package/dist/css/css-bar-chart.d.ts +17 -2
  108. package/dist/css/css-bar-chart.js +38 -7
  109. package/dist/css/css-button-group-attached.d.ts +3 -0
  110. package/dist/css/css-button-group-attached.js +26 -5
  111. package/dist/css/css-button-group-track.js +1 -1
  112. package/dist/css/css-canvas-grouped.d.ts +17 -0
  113. package/dist/css/css-canvas-grouped.js +40 -0
  114. package/dist/css/css-checkbox.js +2 -2
  115. package/dist/css/css-components.js +15 -0
  116. package/dist/css/css-count-pill.d.ts +2 -0
  117. package/dist/css/css-count-pill.js +54 -0
  118. package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
  119. package/dist/css/css-date-field-calendar-entry.js +50 -0
  120. package/dist/css/css-date-field.js +7 -2
  121. package/dist/css/css-date-range-presets.d.ts +2 -0
  122. package/dist/css/css-date-range-presets.js +109 -0
  123. package/dist/css/css-date-range.d.ts +2 -0
  124. package/dist/css/css-date-range.js +197 -0
  125. package/dist/css/css-denomination-grid.js +4 -0
  126. package/dist/css/css-denomination-transposed.d.ts +4 -0
  127. package/dist/css/css-denomination-transposed.js +178 -0
  128. package/dist/css/css-dialog-canvas.d.ts +3 -0
  129. package/dist/css/css-dialog-canvas.js +20 -0
  130. package/dist/css/css-dialog-frame.js +19 -2
  131. package/dist/css/css-dialog-grabber.d.ts +4 -0
  132. package/dist/css/css-dialog-grabber.js +56 -0
  133. package/dist/css/css-dialog.js +3 -2
  134. package/dist/css/css-document.js +3 -0
  135. package/dist/css/css-field-width.d.ts +12 -0
  136. package/dist/css/css-field-width.js +49 -0
  137. package/dist/css/css-field.js +4 -0
  138. package/dist/css/css-filter-chips-layout.d.ts +2 -0
  139. package/dist/css/css-filter-chips-layout.js +42 -0
  140. package/dist/css/css-filter-chips.js +4 -0
  141. package/dist/css/css-list-row.d.ts +8 -0
  142. package/dist/css/css-list-row.js +209 -0
  143. package/dist/css/css-list.d.ts +3 -0
  144. package/dist/css/css-list.js +85 -0
  145. package/dist/css/css-scroll-region.d.ts +18 -0
  146. package/dist/css/css-scroll-region.js +41 -0
  147. package/dist/css/css-section-nav-filled.js +2 -25
  148. package/dist/css/css-segmented-sizing.d.ts +13 -0
  149. package/dist/css/css-segmented-sizing.js +66 -0
  150. package/dist/css/css-segmented.js +4 -0
  151. package/dist/css/css-stat-tile.d.ts +7 -0
  152. package/dist/css/css-stat-tile.js +16 -0
  153. package/dist/css/css-steps.js +2 -2
  154. package/dist/css/css-switch.d.ts +6 -0
  155. package/dist/css/css-switch.js +39 -0
  156. package/dist/css/css-table-bounded-edges.js +4 -27
  157. package/dist/css/range-band.d.ts +2 -0
  158. package/dist/css/range-band.js +7 -0
  159. package/dist/css/stacking.d.ts +1 -0
  160. package/dist/css/stacking.js +6 -3
  161. package/dist/css/stylesheet.js +10 -0
  162. package/dist/date-field-labels.d.ts +5 -0
  163. package/dist/date-field-labels.js +1 -0
  164. package/dist/date-range-draft.d.ts +26 -0
  165. package/dist/date-range-draft.js +36 -0
  166. package/dist/date-range-input.d.ts +26 -0
  167. package/dist/date-range-input.js +58 -0
  168. package/dist/date-range-labels.d.ts +22 -0
  169. package/dist/date-range-labels.js +26 -0
  170. package/dist/date-range-view.d.ts +12 -0
  171. package/dist/date-range-view.js +29 -0
  172. package/dist/date-range-words.d.ts +11 -0
  173. package/dist/date-range-words.js +40 -0
  174. package/dist/index.d.ts +16 -9
  175. package/dist/index.js +14 -7
  176. package/dist/list-options.d.ts +71 -0
  177. package/dist/list-options.js +84 -0
  178. package/dist/styles.css +863 -41
  179. package/dist/theme/ThemeProvider.d.ts +11 -1
  180. package/dist/theme/ThemeProvider.js +4 -2
  181. package/package.json +1 -1
@@ -4,45 +4,14 @@ import { cloneElement, isValidElement, useId, } from 'react';
4
4
  import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
6
  import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
7
+ import { fieldWidthClass } from '../css/css-field-width.js';
7
8
  import { SearchFrame } from './field/SearchFrame.js';
9
+ import { assertFieldProps, fieldVariants, passesInvalid, } from './field/field-checks.js';
8
10
  import { Stack } from './Stack.js';
9
- export const fieldVariants = ['outlined', 'search'];
10
- function assertSearch(props, child) {
11
- const { variant = 'outlined' } = props;
12
- if (!fieldVariants.includes(variant))
13
- throw new RangeError(`Unknown Field variant: ${String(variant)}`);
14
- if (variant !== 'search') {
15
- if (props.leading !== undefined ||
16
- props.clearLabel !== undefined ||
17
- props.onClear !== undefined)
18
- throw new TypeError('Field leading, clearLabel and onClear are for variant="search"');
19
- return;
20
- }
21
- if (!props.clearLabel?.trim())
22
- throw new RangeError('A search Field needs a nonempty clearLabel');
23
- if (typeof props.onClear !== 'function')
24
- throw new TypeError('A search Field needs onClear');
25
- if (props.prefix || props.suffix)
26
- throw new TypeError('A search Field takes leading, not prefix or suffix');
27
- if (!isValidElement(child) || child.type !== 'input')
28
- throw new TypeError('A search Field needs one native input child');
29
- if (child.props.value === undefined)
30
- throw new TypeError('A search Field needs a controlled input: pass value (and onChange)');
31
- }
32
- /**
33
- * `invalid` was passed, even as `false`, but not as `undefined` (a wrapper
34
- * forwarding an optional prop passes nothing): such a field needs one native
35
- * control from its first render, so a form learns it on the happy path
36
- * instead of on its first failed submit. `error` keeps its older rule (it
37
- * requires the control only while it holds a message), so a composed
38
- * child beside `error={undefined}` still renders.
39
- */
40
- function passesInvalid(props) {
41
- return props.invalid !== undefined || props.changed !== undefined;
42
- }
11
+ export { fieldVariants };
43
12
  export function Field(props) {
44
- const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, } = props;
45
- assertSearch(props, children);
13
+ const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, width, } = props;
14
+ assertFieldProps(props, children);
46
15
  const search = variant === 'search';
47
16
  const controlId = useId();
48
17
  const descriptionId = useId();
@@ -54,13 +23,20 @@ export function Field(props) {
54
23
  const message = error || description;
55
24
  const marksInvalid = Boolean(error) || invalid;
56
25
  const adorned = Boolean(prefix || suffix);
57
- const needsControl = Boolean(message || marksInvalid || passesInvalid(props) || required || adorned);
26
+ const needsControl = Boolean(message ||
27
+ marksInvalid ||
28
+ passesInvalid(props) ||
29
+ required ||
30
+ adorned ||
31
+ Boolean(width));
58
32
  const className = [
59
33
  'sw-field',
60
34
  size === 'xs' && 'sw-field-xs',
61
35
  marksInvalid && 'sw-field-invalid',
62
36
  search && 'sw-field-search-field',
63
37
  changed && !marksInvalid && 'sw-field-changed',
38
+ width && 'sw-field-sized',
39
+ width && fieldWidthClass(width),
64
40
  ]
65
41
  .filter(Boolean)
66
42
  .join(' ');
@@ -71,7 +47,7 @@ export function Field(props) {
71
47
  typeof children.type !== 'string' ||
72
48
  !['input', 'select', 'textarea'].includes(children.type)) {
73
49
  if (needsControl)
74
- throw new TypeError('Field validation and adornments require one native input, select, or textarea child');
50
+ throw new TypeError('Field validation, adornments and width require one native input, select, or textarea child');
75
51
  return (_jsxs(Stack, { as: "label", className: className, gap: labelHidden ? 0 : gap, children: [labelText, children] }));
76
52
  }
77
53
  if (adorned && children.type !== 'input')
@@ -1,37 +1,7 @@
1
- import { type RequiredMark } from './FieldLabelText.js';
2
- export type FilterChipOption = {
3
- value: string;
4
- label: string;
5
- count?: number;
6
- disabled?: boolean;
7
- };
8
- export type FilterChipsProps = {
9
- label: string;
10
- /** The chosen option's value, or `''` while nothing is chosen yet. */
11
- value: string;
12
- onChange: (value: string) => void;
13
- options: readonly FilterChipOption[];
14
- /**
15
- * A choice must be made, such as a reason: each radio is `required` and
16
- * the label shows the required mark (hidden from assistive technology,
17
- * which reads the radios' own state).
18
- */
19
- required?: boolean;
20
- /**
21
- * `hidden` leaves out the label's " *" while the control stays required
22
- * (and announced so); `visible` (default) draws it.
23
- * When hidden, say in visible text which fields must be filled; a
24
- * placeholder alone is not enough.
25
- */
26
- requiredMark?: RequiredMark;
27
- /**
28
- * A message under the chips, such as "Pick a reason": a polite live
29
- * region linked to the group by `aria-describedby`; the group is marked
30
- * invalid and every chip takes the danger edge. An empty string is no
31
- * error.
32
- */
33
- error?: string;
34
- /** More ids that describe the group, such as a hint beside it. */
35
- 'aria-describedby'?: string;
36
- };
37
- export declare function FilterChips({ label, value, onChange, options, required, requiredMark, error, 'aria-describedby': describedByProp, }: FilterChipsProps): import("react").JSX.Element;
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';
3
+ /**
4
+ * A labelled row of chips: one choice (radios), or several at once
5
+ * (`multiple`, checkboxes), each chip optionally counted.
6
+ */
7
+ export declare function FilterChips(props: FilterChipsProps): import("react").JSX.Element;
@@ -1,35 +1,43 @@
1
1
  'use client';
2
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
+ import { radioTabStopValue } from '../tab-stop.js';
5
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
- import { assertRequiredMark, showsRequiredMark, } from './FieldLabelText.js';
7
- import { radioTabStopValue, tabStop } from '../tab-stop.js';
8
- function isCount(count) {
9
- return count === undefined || (Number.isInteger(count) && count >= 0);
7
+ import { showsRequiredMark } from './FieldLabelText.js';
8
+ import { FilterChip } from './filter-chips/FilterChip.js';
9
+ import { assertFilterChips } from './filter-chips/validate.js';
10
+ export { filterChipCountAppearances, filterChipsOverflows, } from './filter-chips/options.js';
11
+ /** The values a change leaves on, in the options' order. */
12
+ function toggled(options, chosen, value, checked) {
13
+ return options
14
+ .map((option) => option.value)
15
+ .filter((each) => (each === value ? checked : chosen.includes(each)));
10
16
  }
11
- export function FilterChips({ label, value, onChange, options, required = false, requiredMark = 'visible', error, 'aria-describedby': describedByProp, }) {
17
+ /**
18
+ * A labelled row of chips: one choice (radios), or several at once
19
+ * (`multiple`, checkboxes), each chip optionally counted.
20
+ */
21
+ export function FilterChips(props) {
22
+ const { label, options, labelVisuallyHidden = false, requiredMark = 'visible', countAppearance = 'figure', overflow = 'wrap', error, 'aria-describedby': describedByProp, } = props;
12
23
  const name = useId();
13
24
  const errorId = useId();
25
+ assertFilterChips(props);
26
+ const required = props.multiple ? false : (props.required ?? false);
14
27
  const describedBy = [describedByProp?.trim() || null, error ? errorId : null]
15
28
  .filter((id) => id !== null)
16
29
  .join(' ');
17
- const values = options.map((option) => option.value);
18
- if (!label.trim())
19
- throw new RangeError('label must not be empty');
20
- assertRequiredMark(requiredMark);
21
- if (options.length === 0 ||
22
- options.some((option) => !option.value.trim() || !option.label.trim()))
23
- throw new RangeError('options must have nonempty values and labels');
24
- if (new Set(values).size !== values.length)
25
- throw new RangeError('options must have unique values');
26
- if (value !== '' && !values.includes(value))
27
- throw new RangeError('value must match an option or be empty');
28
- if (options.some((option) => !isCount(option.count)))
29
- throw new RangeError('count must be a non-negative integer');
30
- const entry = radioTabStopValue(options, value);
31
- return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": error ? true : undefined, className: "sw-filter-chips", children: [_jsxs("legend", { className: "sw-filter-chips-legend", children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
32
- if (event.currentTarget.checked && !option.disabled)
33
- onChange(option.value);
34
- }, required: required, tabIndex: option.value === entry ? tabStop : undefined, type: "radio", value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: option.count })] }))] })] }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-filter-chips-error", id: errorId, message: error })] }));
30
+ const entry = props.multiple
31
+ ? undefined
32
+ : radioTabStopValue(options, props.value);
33
+ function isChecked(value) {
34
+ return props.multiple ? props.value.includes(value) : props.value === value;
35
+ }
36
+ function change(value, checked) {
37
+ if (props.multiple)
38
+ props.onChange(toggled(options, props.value, value, checked));
39
+ else if (checked)
40
+ props.onChange(value);
41
+ }
42
+ return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": error ? true : undefined, className: cx('sw-filter-chips', overflow === 'scroll' && 'sw-filter-chips-scroll'), children: [_jsxs("legend", { className: cx('sw-filter-chips-legend', labelVisuallyHidden && 'sw-sr-only'), children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsx(FilterChip, { checked: isChecked(option.value), countAppearance: countAppearance, name: name, onCheckedChange: (checked) => change(option.value, checked), option: option, required: required, tabStopHere: props.multiple ? true : option.value === entry, type: props.multiple ? 'checkbox' : 'radio' }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-filter-chips-error", id: errorId, message: error })] }));
35
43
  }
@@ -0,0 +1,33 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ListGroupMaxHeight, type ListGroupVariant } from '../list-options.js';
3
+ export type ListGroupProps = {
4
+ /** `ListRow` elements; the group draws a hairline between each pair. */
5
+ children: ReactNode;
6
+ /** Names the list, such as its card's heading. Required with `maxHeight`. */
7
+ 'aria-label'?: string;
8
+ 'aria-labelledby'?: string;
9
+ /**
10
+ * `panel` (the default) is one bordered panel; `plain` draws no panel
11
+ * and a hairline above the first row too, for rows inside a Card.
12
+ */
13
+ variant?: ListGroupVariant;
14
+ /**
15
+ * Bounds the list to a row count of its own region, `{ rows }` rows of a
16
+ * title and one detail line, where the rest scroll; the region is a named
17
+ * keyboard stop and shades an edge with more rows past it.
18
+ */
19
+ maxHeight?: ListGroupMaxHeight;
20
+ /**
21
+ * Pinned under the rows, outside the scroll, under a hairline: a total
22
+ * the rows add up to, such as a `ListRow` with the count and the sum.
23
+ */
24
+ footer?: ReactNode;
25
+ className?: string;
26
+ id?: string;
27
+ };
28
+ /**
29
+ * Rows in one list (`ul`, its list role kept) with hairlines between them:
30
+ * a panel of its own, or plain rows inside a surface. The ref reaches the
31
+ * outer box.
32
+ */
33
+ export declare const ListGroup: import("react").ForwardRefExoticComponent<ListGroupProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { assertListGroup, listMaxHeightStyle, } from '../list-options.js';
6
+ import { ListRowContainer } from './list/list-row-container.js';
7
+ import { ScrollRegion } from './ScrollRegion.js';
8
+ /**
9
+ * Rows in one list (`ul`, its list role kept) with hairlines between them:
10
+ * a panel of its own, or plain rows inside a surface. The ref reaches the
11
+ * outer box.
12
+ */
13
+ export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', maxHeight, footer, className, id, }, ref) {
14
+ assertListGroup({ label, labelledBy, variant, maxHeight });
15
+ const list = (_jsx("ul", { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list",
16
+ // A list styled without bullets loses its role in Safari.
17
+ role: "list", children: _jsx(ListRowContainer.Provider, { value: "li", children: children }) }));
18
+ return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
19
+ });
@@ -0,0 +1,60 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type BadgeTone } from '../css/css-data.js';
3
+ import { type ListRowTitleAppearance, type ListRowTone, type ListRowValueTone } from '../list-options.js';
4
+ export type ListRowProps = {
5
+ title: string;
6
+ /** A muted line under the title. */
7
+ detail?: string;
8
+ /** A figure or word at the row's end, such as "+$1.48". */
9
+ value?: string;
10
+ /** A muted line under the value, such as "checks $3,427". */
11
+ valueDetail?: string;
12
+ /**
13
+ * Colours the value only, as a table cell's tone, or `warning` for a
14
+ * figure that needs a look; the words say why.
15
+ */
16
+ valueTone?: ListRowValueTone;
17
+ /**
18
+ * Draws the value as a `Badge` in this tone, such as a status ("Open",
19
+ * "Blocked") at the row's end. Not with `valueTone`.
20
+ */
21
+ valueBadge?: BadgeTone;
22
+ /**
23
+ * A consumer mark at the row's start, such as a status dot or a Lucide
24
+ * glyph (ADR 0008). It is hidden from assistive technology, so the words
25
+ * must say what it means.
26
+ */
27
+ leading?: ReactNode;
28
+ /**
29
+ * Makes the whole row a button (or, with `href`, a link's plain press
30
+ * for a client router) and adds the chevron.
31
+ */
32
+ onPress?: () => void;
33
+ /** Makes the whole row a link to `href`, with the chevron. */
34
+ href?: string;
35
+ /**
36
+ * Makes the row a choice: a check while true, nothing while false, and
37
+ * the state (`aria-pressed` on a button, `aria-current` on a link). Only
38
+ * on a row with `onPress` or `href`.
39
+ */
40
+ selected?: boolean;
41
+ /** Dims the row and blocks presses; a link row drops its `href`. */
42
+ disabled?: boolean;
43
+ /** `danger` draws the title in the danger colour, for a row that ends something. */
44
+ tone?: ListRowTone;
45
+ /** `plain` sets the title at the body weight, for a list of plain statements. */
46
+ titleAppearance?: ListRowTitleAppearance;
47
+ /** `none` hides the chevron of a pressable row that acts in place. */
48
+ accessory?: 'chevron' | 'none';
49
+ /**
50
+ * Replaces a pressable row's name, which is its title and value; its
51
+ * detail lines are its description.
52
+ */
53
+ 'aria-label'?: string;
54
+ };
55
+ /**
56
+ * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
57
+ * a chevron, the whole row pressing), picks one option in place
58
+ * (`selected`), or only states something (neither).
59
+ */
60
+ export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useContext, useId } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { assertListRow, listRowAccessory, } from '../list-options.js';
6
+ import { tabStop } from '../tab-stop.js';
7
+ import { plainPress } from './link-press.js';
8
+ import { ListRowContent } from './list/ListRowContent.js';
9
+ import { ListRowContainer } from './list/list-row-container.js';
10
+ function ids(...parts) {
11
+ const joined = parts.filter(Boolean).join(' ');
12
+ return joined === '' ? undefined : joined;
13
+ }
14
+ /**
15
+ * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
16
+ * a chevron, the whole row pressing), picks one option in place
17
+ * (`selected`), or only states something (neither).
18
+ */
19
+ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', 'aria-label': ariaLabel, }) {
20
+ const pressable = onPress !== undefined || href !== undefined;
21
+ assertListRow({
22
+ title,
23
+ tone,
24
+ titleAppearance,
25
+ valueTone,
26
+ accessory,
27
+ value,
28
+ valueDetail,
29
+ valueBadge,
30
+ selected,
31
+ pressable,
32
+ });
33
+ const container = useContext(ListRowContainer);
34
+ const id = useId();
35
+ const kind = listRowAccessory({ pressable, selected, accessory });
36
+ const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, title: title, titleAppearance: titleAppearance, tone: tone, 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');
38
+ const naming = {
39
+ 'aria-label': ariaLabel,
40
+ 'aria-labelledby': ariaLabel
41
+ ? undefined
42
+ : ids(`${id}-title`, value !== undefined && `${id}-value`),
43
+ 'aria-describedby': ids(Boolean(detail) && `${id}-detail`, Boolean(valueDetail) && `${id}-value-detail`),
44
+ };
45
+ let row;
46
+ if (href !== undefined)
47
+ row = (_jsx("a", { ...naming, "aria-current": selected ? 'true' : undefined, "aria-disabled": disabled || undefined, className: className, href: disabled ? undefined : href, onClick: onPress && !disabled
48
+ ? (event) => {
49
+ if (!plainPress(event))
50
+ return;
51
+ event.preventDefault();
52
+ onPress();
53
+ }
54
+ : undefined, role: disabled ? 'link' : undefined, tabIndex: disabled ? undefined : tabStop, children: content }));
55
+ else if (onPress !== undefined)
56
+ row = (_jsx("button", { ...naming, "aria-pressed": selected, className: className, disabled: disabled, onClick: onPress, tabIndex: tabStop, type: "button", children: content }));
57
+ else
58
+ row = (_jsx("div", { "aria-label": ariaLabel, className: className, role: ariaLabel ? 'group' : undefined, children: content }));
59
+ return container === 'li' ? (_jsx("li", { className: "sw-list-item", children: row })) : (_jsx("div", { className: "sw-list-item", children: row }));
60
+ }
@@ -0,0 +1,21 @@
1
+ export type NavCountProps = {
2
+ /**
3
+ * How many things wait behind the destination, such as tasks to do: a
4
+ * filled pill after its words, read in the link's name ("Tasks 8"). A
5
+ * whole number; 0 shows nothing.
6
+ */
7
+ count: number;
8
+ /**
9
+ * What the count means, in the consumer's words ("8 tasks to do"): read
10
+ * in the name in place of the bare number ("Tasks 8 tasks to do"), the
11
+ * pill then hidden from assistive technology.
12
+ */
13
+ countLabel?: string;
14
+ };
15
+ /**
16
+ * A count after a Nav destination's words, placed inside its link or
17
+ * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
18
+ * pill SectionNav's item count draws, and it is part of the destination's
19
+ * accessible name, as there.
20
+ */
21
+ export declare function NavCount({ count, countLabel }: NavCountProps): import("react").JSX.Element | null;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Fails closed on a count that is not a whole number or an empty label. */
3
+ function assertNavCount({ count, countLabel }) {
4
+ if (!Number.isInteger(count) || count < 0)
5
+ throw new RangeError(`NavCount count must be a whole number from 0: ${String(count)}`);
6
+ if (countLabel !== undefined &&
7
+ (typeof countLabel !== 'string' || !countLabel.trim()))
8
+ throw new RangeError('NavCount countLabel must be non-empty text');
9
+ }
10
+ /**
11
+ * A count after a Nav destination's words, placed inside its link or
12
+ * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
13
+ * pill SectionNav's item count draws, and it is part of the destination's
14
+ * accessible name, as there.
15
+ */
16
+ export function NavCount({ count, countLabel }) {
17
+ assertNavCount({ count, countLabel });
18
+ if (count === 0)
19
+ return null;
20
+ // The space is its own text node, so every browser and the name's
21
+ // computation keep it between the words and the count.
22
+ return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: "sw-nav-count", children: count })] }));
23
+ }
@@ -4,15 +4,8 @@ import { breakpoints } from '../css/breakpoints.js';
4
4
  import { sectionNavVerticalClass } from '../css/css-section-nav.js';
5
5
  import { assertSectionNavAppearance, } from '../css/css-section-nav-filled.js';
6
6
  import { tabStop } from '../tab-stop.js';
7
+ import { plainPress } from './link-press.js';
7
8
  export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
8
- function plainPress(event) {
9
- return (event.button === 0 &&
10
- !event.defaultPrevented &&
11
- !event.metaKey &&
12
- !event.ctrlKey &&
13
- !event.shiftKey &&
14
- !event.altKey);
15
- }
16
9
  function ariaCurrent(current) {
17
10
  if (current === true)
18
11
  return 'true';
@@ -1,5 +1,8 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type AccessibleName } from '../accessible-name.js';
3
+ import { type Breakpoint } from '../css/breakpoints.js';
4
+ import { type SegmentedSize, segmentedSizes } from '../css/css-segmented-sizing.js';
5
+ export { segmentedSizes, type SegmentedSize };
3
6
  export type SegmentedItem = {
4
7
  id: string;
5
8
  label: ReactNode;
@@ -46,6 +49,19 @@ export type SegmentedControlProps = AccessibleName & SegmentedChange & {
46
49
  error?: string;
47
50
  /** Sets `aria-required` on the group; the visible mark belongs to the element that labels it. */
48
51
  required?: boolean;
52
+ /**
53
+ * Below this breakpoint the control fills its column and its segments
54
+ * share the width equally, whatever the variant, such as a period
55
+ * switch across a phone. From it up it keeps its own width.
56
+ */
57
+ fullWidthBelow?: Breakpoint;
58
+ /**
59
+ * `sm` pads every segment a step less and keeps each label on one line
60
+ * (ending in an ellipsis past its share; its name keeps the whole
61
+ * label), so three or four segments with counts fit a phone's card.
62
+ * Keep the labels short enough to fit: the hidden part cannot be read.
63
+ * `md` (the default) is the variant's own padding.
64
+ */
65
+ size?: SegmentedSize;
49
66
  };
50
67
  export declare const SegmentedControl: import("react").ForwardRefExoticComponent<SegmentedControlProps & import("react").RefAttributes<HTMLDivElement>>;
51
- export {};
@@ -2,11 +2,19 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
+ import { breakpoints } from '../css/breakpoints.js';
5
6
  import { segmentedRaisedClass } from '../css/css-segmented-raised.js';
7
+ import { segmentedFullWidthClass, segmentedSizes, segmentedSmClass, } from '../css/css-segmented-sizing.js';
6
8
  import { FieldErrorRegion } from './FieldErrorRegion.js';
7
9
  import { tabStop } from '../tab-stop.js';
8
- export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
10
+ export { segmentedSizes };
11
+ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, fullWidthBelow, items, onChange, required = false, size = 'md', value, variant = 'compact', ...labelProps }, ref) {
9
12
  const errorId = useId();
13
+ if (fullWidthBelow !== undefined &&
14
+ !breakpoints.includes(fullWidthBelow))
15
+ throw new RangeError(`Unknown SegmentedControl fullWidthBelow: ${String(fullWidthBelow)}`);
16
+ if (!segmentedSizes.includes(size))
17
+ throw new RangeError(`Unknown SegmentedControl size: ${String(size)}`);
10
18
  const withIcons = items.some((item) => item.icon);
11
19
  function onKeyDown(event) {
12
20
  if (disabled || (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft')) {
@@ -25,7 +33,7 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
25
33
  }
26
34
  // The wrapper is always rendered, so an error that appears later swaps
27
35
  // text into a live region that already exists and the group keeps focus.
28
- return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', variant === 'raised' && segmentedRaisedClass, withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
36
+ return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled', fullWidthBelow && segmentedFullWidthClass(fullWidthBelow)), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', variant === 'raised' && segmentedRaisedClass, withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled', size === 'sm' && segmentedSmClass), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
29
37
  const selected = item.id === value;
30
38
  return (_jsxs("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
31
39
  if (!selected) {
@@ -34,6 +42,6 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
34
42
  }, role: "radio",
35
43
  // Every item stays a tab stop, as each already was outside
36
44
  // Safari: the arrow keys change the value, not the focus.
37
- tabIndex: tabStop, type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, item.label] }, item.id));
45
+ tabIndex: tabStop, type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, size === 'sm' ? (_jsx("span", { className: "sw-segmented-item-label", children: item.label })) : (item.label)] }, item.id));
38
46
  }) }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
39
47
  });
@@ -1,7 +1,7 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
- import { type StatTileTone, statTileTones } from '../css/css-stat-tile.js';
3
- export type { StatTileTone };
4
- export { statTileTones };
2
+ import { type StatTileAppearance, statTileAppearances, type StatTileTone, statTileTones } from '../css/css-stat-tile.js';
3
+ export type { StatTileAppearance, StatTileTone };
4
+ export { statTileAppearances, statTileTones };
5
5
  export type StatTileEmphasis = 'default' | 'primary';
6
6
  export type StatTileProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
7
7
  /** A short name for the figure, set in the label variant. */
@@ -16,10 +16,17 @@ export type StatTileProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
16
16
  caption?: ReactNode;
17
17
  /** `primary` fills the tile in the accent for the one figure a page leads with. */
18
18
  emphasis?: StatTileEmphasis;
19
+ /**
20
+ * `plain` drops the tile's surface, edge and padding, so the figure sits
21
+ * inside a card that already has them, such as four answers in a row
22
+ * with hairlines between them. A plain tile cannot be `primary`, which
23
+ * is a fill. Default `card`.
24
+ */
25
+ appearance?: StatTileAppearance;
19
26
  };
20
27
  /**
21
28
  * One prominent figure with its label: the tiles a report or a list opens
22
29
  * with. Presentation only; the consumer formats the number and picks the
23
30
  * glyph.
24
31
  */
25
- export declare function StatTile({ caption, className, emphasis, glyph, label, tone, value, ...rest }: StatTileProps): import("react").JSX.Element;
32
+ export declare function StatTile({ appearance, caption, className, emphasis, glyph, label, tone, value, ...rest }: StatTileProps): import("react").JSX.Element;
@@ -1,14 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../class-names.js';
3
- import { statTileTones } from '../css/css-stat-tile.js';
3
+ import { statTileAppearances, statTileTones, } from '../css/css-stat-tile.js';
4
4
  import { Text } from './Text.js';
5
- export { statTileTones };
5
+ export { statTileAppearances, statTileTones };
6
+ function assertStatTile(appearance, emphasis) {
7
+ if (!statTileAppearances.includes(appearance))
8
+ throw new RangeError(`Unknown StatTile appearance: ${String(appearance)}`);
9
+ if (appearance === 'plain' && emphasis === 'primary')
10
+ throw new RangeError('A plain StatTile has no surface to fill; emphasis primary needs appearance card');
11
+ }
6
12
  /**
7
13
  * One prominent figure with its label: the tiles a report or a list opens
8
14
  * with. Presentation only; the consumer formats the number and picks the
9
15
  * glyph.
10
16
  */
11
- export function StatTile({ caption, className, emphasis = 'default', glyph, label, tone = 'default', value, ...rest }) {
17
+ export function StatTile({ appearance = 'card', caption, className, emphasis = 'default', glyph, label, tone = 'default', value, ...rest }) {
18
+ assertStatTile(appearance, emphasis);
12
19
  const primary = emphasis === 'primary';
13
- return (_jsxs("section", { className: cx('sw-stat-tile', primary && 'sw-stat-tile-primary', className), ...rest, children: [glyph ? (_jsx("span", { "aria-hidden": "true", className: "sw-stat-tile-glyph", children: glyph })) : null, _jsxs("span", { className: "sw-stat-tile-body", children: [_jsx(Text, { as: "span", className: "sw-stat-tile-label", color: primary ? 'onAccent' : 'muted', variant: "label", children: label }), _jsx(Text, { as: "strong", className: "sw-stat-tile-value", color: primary ? 'onAccent' : tone === 'default' ? 'text' : tone, variant: "heading", children: value }), caption ? (_jsx(Text, { as: "span", className: "sw-stat-tile-caption", color: primary ? 'onAccent' : 'muted', variant: "caption", children: caption })) : null] })] }));
20
+ return (_jsxs("section", { className: cx('sw-stat-tile', appearance === 'plain' && 'sw-stat-tile-plain', primary && 'sw-stat-tile-primary', className), ...rest, children: [glyph ? (_jsx("span", { "aria-hidden": "true", className: "sw-stat-tile-glyph", children: glyph })) : null, _jsxs("span", { className: "sw-stat-tile-body", children: [_jsx(Text, { as: "span", className: "sw-stat-tile-label", color: primary ? 'onAccent' : 'muted', variant: "label", children: label }), _jsx(Text, { as: "strong", className: "sw-stat-tile-value", color: primary ? 'onAccent' : tone === 'default' ? 'text' : tone, variant: "heading", children: value }), caption ? (_jsx(Text, { as: "span", className: "sw-stat-tile-caption", color: primary ? 'onAccent' : 'muted', variant: "caption", children: caption })) : null] })] }));
14
21
  }
@@ -1,8 +1,23 @@
1
+ import { type SwitchCheckedTone, switchCheckedTones, type SwitchLayout, switchLayouts } from '../css/css-switch.js';
2
+ export { switchCheckedTones, switchLayouts, type SwitchCheckedTone, type SwitchLayout, };
1
3
  export type SwitchProps = {
2
4
  label: string;
3
5
  checked: boolean;
4
6
  onCheckedChange: (checked: boolean) => void;
5
7
  disabled?: boolean;
6
8
  description?: string;
9
+ /**
10
+ * `leading` (the default) puts the control before its label;
11
+ * `trailing` puts the label and description first and the control at
12
+ * the row's end, the row as wide as its column, as a settings list
13
+ * draws it.
14
+ */
15
+ layout?: SwitchLayout;
16
+ /**
17
+ * The track's colour while on: `accent` (the default) or `success`, for
18
+ * a switch whose on state is the healthy one, such as a feature turned
19
+ * on. The thumb's place says the state either way.
20
+ */
21
+ checkedTone?: SwitchCheckedTone;
7
22
  };
8
- export declare function Switch({ label, checked, onCheckedChange, disabled, description, }: SwitchProps): import("react").JSX.Element;
23
+ export declare function Switch({ label, checked, onCheckedChange, disabled, description, layout, checkedTone, }: SwitchProps): import("react").JSX.Element;
@@ -1,12 +1,23 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { switchCheckedTones, switchLayouts, } from '../css/css-switch.js';
4
6
  import { tabStop } from '../tab-stop.js';
5
- export function Switch({ label, checked, onCheckedChange, disabled = false, description, }) {
7
+ export { switchCheckedTones, switchLayouts, };
8
+ function assertOneOf(values, value, prop) {
9
+ if (!values.includes(value))
10
+ throw new RangeError(`Unknown Switch ${prop}: ${String(value)}`);
11
+ }
12
+ export function Switch({ label, checked, onCheckedChange, disabled = false, description, layout = 'leading', checkedTone = 'accent', }) {
13
+ assertOneOf(switchLayouts, layout, 'layout');
14
+ assertOneOf(switchCheckedTones, checkedTone, 'checkedTone');
6
15
  const labelId = useId();
7
16
  const descriptionId = useId();
8
- return (_jsxs("label", { className: "sw-switch", children: [_jsxs("span", { className: "sw-switch-control", children: [_jsx("input", { "aria-checked": checked, "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, checked: checked, className: "sw-switch-input", disabled: disabled, onChange: (event) => {
9
- if (!disabled)
10
- onCheckedChange(event.currentTarget.checked);
11
- }, role: "switch", tabIndex: tabStop, type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-switch-track", children: _jsx("span", { className: "sw-switch-thumb" }) })] }), _jsxs("span", { className: "sw-switch-copy", children: [_jsx("span", { className: "sw-switch-label", id: labelId, children: label }), description ? (_jsx("span", { className: "sw-switch-description", id: descriptionId, children: description })) : null] })] }));
17
+ const control = (_jsxs("span", { className: "sw-switch-control", children: [_jsx("input", { "aria-checked": checked, "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, checked: checked, className: "sw-switch-input", disabled: disabled, onChange: (event) => {
18
+ if (!disabled)
19
+ onCheckedChange(event.currentTarget.checked);
20
+ }, role: "switch", tabIndex: tabStop, type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-switch-track", children: _jsx("span", { className: "sw-switch-thumb" }) })] }));
21
+ const copy = (_jsxs("span", { className: "sw-switch-copy", children: [_jsx("span", { className: "sw-switch-label", id: labelId, children: label }), description ? (_jsx("span", { className: "sw-switch-description", id: descriptionId, children: description })) : null] }));
22
+ return (_jsx("label", { className: cx('sw-switch', layout === 'trailing' && 'sw-switch-trailing', checkedTone === 'success' && 'sw-switch-success'), children: layout === 'trailing' ? (_jsxs(_Fragment, { children: [copy, control] })) : (_jsxs(_Fragment, { children: [control, copy] })) }));
12
23
  }