@scalewing/react 1.30.0 → 1.32.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 (100) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +23 -5
  3. package/dist/components/ActionMenu.d.ts +10 -1
  4. package/dist/components/ActionMenu.js +7 -2
  5. package/dist/components/AppHeader.d.ts +9 -1
  6. package/dist/components/AppHeader.js +6 -2
  7. package/dist/components/Box.d.ts +10 -2
  8. package/dist/components/Button.d.ts +27 -2
  9. package/dist/components/Button.js +5 -3
  10. package/dist/components/ButtonGroup.d.ts +9 -1
  11. package/dist/components/ButtonGroup.js +8 -2
  12. package/dist/components/Combobox.d.ts +14 -0
  13. package/dist/components/Combobox.js +125 -0
  14. package/dist/components/DateField.d.ts +9 -1
  15. package/dist/components/DateField.js +4 -3
  16. package/dist/components/DenominationGrid.d.ts +5 -3
  17. package/dist/components/DenominationGrid.js +21 -11
  18. package/dist/components/Dialog.d.ts +9 -1
  19. package/dist/components/Dialog.js +8 -4
  20. package/dist/components/Field.d.ts +25 -0
  21. package/dist/components/Field.js +56 -6
  22. package/dist/components/FieldLabelText.d.ts +21 -3
  23. package/dist/components/FieldLabelText.js +16 -4
  24. package/dist/components/FilterChips.d.ts +9 -1
  25. package/dist/components/FilterChips.js +4 -2
  26. package/dist/components/Nav.d.ts +14 -2
  27. package/dist/components/Nav.js +6 -2
  28. package/dist/components/SegmentedControl.d.ts +5 -2
  29. package/dist/components/SegmentedControl.js +2 -1
  30. package/dist/components/Select.d.ts +9 -1
  31. package/dist/components/Select.js +4 -3
  32. package/dist/components/Stepper.d.ts +13 -0
  33. package/dist/components/Stepper.js +8 -2
  34. package/dist/components/combobox/ComboboxPopup.d.ts +25 -0
  35. package/dist/components/combobox/ComboboxPopup.js +68 -0
  36. package/dist/components/combobox/entries.d.ts +18 -0
  37. package/dist/components/combobox/entries.js +34 -0
  38. package/dist/components/combobox/types.d.ts +71 -0
  39. package/dist/components/combobox/types.js +1 -0
  40. package/dist/components/combobox/validate.d.ts +3 -0
  41. package/dist/components/combobox/validate.js +44 -0
  42. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -2
  43. package/dist/components/denomination-grid/DenominationStrip.js +3 -2
  44. package/dist/components/denomination-grid/DenominationTiles.d.ts +1 -1
  45. package/dist/components/denomination-grid/EntryCell.js +1 -1
  46. package/dist/components/denomination-grid/StripRow.js +5 -2
  47. package/dist/components/denomination-grid/types.d.ts +35 -1
  48. package/dist/components/dialog/DialogTitle.d.ts +5 -3
  49. package/dist/components/dialog/DialogTitle.js +4 -4
  50. package/dist/components/field/SearchFrame.d.ts +16 -0
  51. package/dist/components/field/SearchFrame.js +32 -0
  52. package/dist/css/css-action-menu-trigger.d.ts +11 -0
  53. package/dist/css/css-action-menu-trigger.js +66 -0
  54. package/dist/css/css-action-menu.js +7 -2
  55. package/dist/css/css-app-header-bar.d.ts +10 -0
  56. package/dist/css/css-app-header-bar.js +26 -0
  57. package/dist/css/css-box-border.d.ts +6 -1
  58. package/dist/css/css-box-border.js +8 -4
  59. package/dist/css/css-button-group-attached.js +5 -2
  60. package/dist/css/css-button-group-track.d.ts +11 -0
  61. package/dist/css/css-button-group-track.js +48 -0
  62. package/dist/css/css-button.d.ts +19 -2
  63. package/dist/css/css-button.js +31 -1
  64. package/dist/css/css-chrome.js +14 -1
  65. package/dist/css/css-combobox.d.ts +2 -0
  66. package/dist/css/css-combobox.js +136 -0
  67. package/dist/css/css-components.js +6 -0
  68. package/dist/css/css-data.js +3 -2
  69. package/dist/css/css-date-field.js +2 -1
  70. package/dist/css/css-denomination-appearance.d.ts +21 -0
  71. package/dist/css/css-denomination-appearance.js +114 -0
  72. package/dist/css/css-denomination-cell-tones.d.ts +8 -0
  73. package/dist/css/css-denomination-cell-tones.js +20 -1
  74. package/dist/css/css-denomination-grid.js +4 -0
  75. package/dist/css/css-dialog-banded.d.ts +11 -0
  76. package/dist/css/css-dialog-banded.js +84 -0
  77. package/dist/css/css-field-search.d.ts +2 -0
  78. package/dist/css/css-field-search.js +118 -0
  79. package/dist/css/css-field.js +4 -0
  80. package/dist/css/css-multi-select.js +2 -1
  81. package/dist/css/css-nav-words.d.ts +13 -0
  82. package/dist/css/css-nav-words.js +51 -0
  83. package/dist/css/css-notice.d.ts +4 -11
  84. package/dist/css/css-notice.js +6 -22
  85. package/dist/css/css-segmented-raised.d.ts +3 -0
  86. package/dist/css/css-segmented-raised.js +72 -0
  87. package/dist/css/css-segmented.js +9 -3
  88. package/dist/css/css-select.js +2 -1
  89. package/dist/css/css-split.js +2 -1
  90. package/dist/css/css-stepper.js +7 -0
  91. package/dist/css/quiet-fill.d.ts +7 -0
  92. package/dist/css/quiet-fill.js +7 -0
  93. package/dist/css/stacking.js +6 -2
  94. package/dist/css/stylesheet.js +4 -0
  95. package/dist/css/tone-tint.d.ts +21 -0
  96. package/dist/css/tone-tint.js +23 -0
  97. package/dist/index.d.ts +13 -10
  98. package/dist/index.js +11 -8
  99. package/dist/styles.css +505 -12
  100. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { type WeekStart } from '../calendar-month.js';
2
2
  import { type DateFieldLabels } from '../date-field-labels.js';
3
+ import { type RequiredMark } from './FieldLabelText.js';
3
4
  export type { DateFieldLabels, WeekStart };
4
5
  export type DateFieldProps = {
5
6
  label: string;
@@ -18,6 +19,13 @@ export type DateFieldProps = {
18
19
  today?: string;
19
20
  disabled?: boolean;
20
21
  required?: boolean;
22
+ /**
23
+ * `hidden` leaves out the label's " *" while the control stays required
24
+ * (and announced so); `visible` (default) draws it.
25
+ * When hidden, say in visible text which fields must be filled; a
26
+ * placeholder alone is not enough.
27
+ */
28
+ requiredMark?: RequiredMark;
21
29
  description?: string;
22
30
  error?: string;
23
31
  /**
@@ -30,4 +38,4 @@ export type DateFieldProps = {
30
38
  /** The control's own words; English by default. */
31
39
  labels?: Partial<DateFieldLabels>;
32
40
  };
33
- export declare function DateField({ label, value, onChange, min, max, today, disabled, required, description, error, locale, weekStartsOn, labels, }: DateFieldProps): import("react").JSX.Element;
41
+ export declare function DateField({ label, value, onChange, min, max, today, disabled, required, requiredMark, description, error, locale, weekStartsOn, labels, }: DateFieldProps): import("react").JSX.Element;
@@ -7,13 +7,14 @@ import { dateEntryPattern, dateEntryPlaceholder } from '../date-entry.js';
7
7
  import { dateEntryValidity } from '../date-entry-validity.js';
8
8
  import { assertDateBounds, isOutsideDateRange } from '../date-only.js';
9
9
  import { FieldErrorRegion } from './FieldErrorRegion.js';
10
- import { FieldLabelText } from './FieldLabelText.js';
10
+ import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
11
11
  import { CalendarDialog } from './date-field/CalendarDialog.js';
12
12
  import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
13
13
  import { useCalendarPopup } from './date-field/use-calendar-popup.js';
14
14
  import { useDateEntry } from './date-field/use-date-entry.js';
15
15
  import { useLangLocale } from './date-field/use-lang-locale.js';
16
- export function DateField({ label, value, onChange, min, max, today, disabled = false, required = false, description, error, locale, weekStartsOn = 0, labels, }) {
16
+ export function DateField({ label, value, onChange, min, max, today, disabled = false, required = false, requiredMark = 'visible', description, error, locale, weekStartsOn = 0, labels, }) {
17
+ assertRequiredMark(requiredMark);
17
18
  assertDateBounds(value, min, max);
18
19
  assertWeekStart(weekStartsOn);
19
20
  const words = resolveDateFieldLabels(labels);
@@ -51,7 +52,7 @@ export function DateField({ label, value, onChange, min, max, today, disabled =
51
52
  .filter((id) => id !== null)
52
53
  .join(' ');
53
54
  const invalid = Boolean(error) || entry.invalid || isOutsideDateRange(value, min, max);
54
- return (_jsxs("div", { className: "sw-date-field", ref: rootRef, children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required }) }), _jsxs("div", { className: "sw-date-field-control", ref: controlRef, children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, autoComplete: "off", className: "sw-date-field-input", disabled: disabled, id: inputId, onBlur: entry.check, onChange: (event) => entry.change(event.currentTarget.value), onKeyDown: (event) => {
55
+ return (_jsxs("div", { className: "sw-date-field", ref: rootRef, children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark }) }), _jsxs("div", { className: "sw-date-field-control", ref: controlRef, children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, autoComplete: "off", className: "sw-date-field-input", disabled: disabled, id: inputId, onBlur: entry.check, onChange: (event) => entry.change(event.currentTarget.value), onKeyDown: (event) => {
55
56
  if (event.key === 'Enter')
56
57
  entry.check();
57
58
  }, placeholder: dateEntryPlaceholder(pattern, {
@@ -1,6 +1,8 @@
1
+ import { denominationGridFrames, denominationTotalEmphases } from '../css/css-denomination-appearance.js';
2
+ import { denominationCellToneAppearances } from '../css/css-denomination-cell-tones.js';
1
3
  import { denominationLabelAppearances } from '../css/css-denomination-label.js';
2
4
  import { type DenominationGridProps } from './denomination-grid/types.js';
3
- export type { DenominationGridColumn, DenominationGridEntry, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, } from './denomination-grid/types.js';
5
+ export type { DenominationCellToneAppearance, DenominationGridColumn, DenominationGridEntry, DenominationGridFrame, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, DenominationTotalEmphasis, } from './denomination-grid/types.js';
4
6
  export type { DenominationLabelAppearance } from '../css/css-denomination-label.js';
5
- export { denominationLabelAppearances };
6
- export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, labelAppearance, }: DenominationGridProps): import("react").JSX.Element;
7
+ export { denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, };
8
+ export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, labelAppearance, frame, totalEmphasis, cellToneAppearance, }: DenominationGridProps): import("react").JSX.Element;
@@ -1,27 +1,37 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../class-names.js';
2
3
  import { denominationLabelWidths } from '../css/css-denomination-aligned.js';
4
+ import { denominationGridFrames, denominationTotalEmphases, denominationTotalsStrongClass, } from '../css/css-denomination-appearance.js';
5
+ import { denominationCellToneAppearances, denominationCellTonesPillClass, } from '../css/css-denomination-cell-tones.js';
3
6
  import { denominationLabelAppearances, denominationLabelsPlainClass, } from '../css/css-denomination-label.js';
4
7
  import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
5
8
  import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
6
9
  import { assertDenominationGrid } from './denomination-grid/validate.js';
7
10
  import { assertDenominationEntries, assertDenominationExtras, } from './denomination-grid/validate-entry.js';
8
- export { denominationLabelAppearances };
9
- export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, labelAppearance = 'tone', }) {
11
+ export { denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, };
12
+ /** Fails closed on a value outside a closed set, naming the prop. */
13
+ function assertOneOf(values, value, prop) {
14
+ if (!values.includes(value))
15
+ throw new RangeError(`Unknown ${prop}: ${String(value)}`);
16
+ }
17
+ export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, labelAppearance = 'tone', frame = 'none', totalEmphasis = 'default', cellToneAppearance = 'text', }) {
10
18
  assertDenominationGrid(label, columns, rows);
11
19
  assertDenominationExtras(layout, columns, rows);
12
20
  assertDenominationEntries(layout, columns, rows);
13
- if (!zeroLabel)
14
- throw new RangeError('zeroLabel must not be empty');
21
+ if (typeof zeroLabel !== 'string')
22
+ throw new RangeError('zeroLabel must be a string');
15
23
  if (totalLabel !== undefined && !totalLabel.trim())
16
24
  throw new RangeError('totalLabel must not be empty');
17
25
  if (rowRole !== 'region' && rowRole !== 'group')
18
26
  throw new RangeError("rowRole must be 'region' or 'group'");
19
- if (labelWidth !== undefined && !denominationLabelWidths.includes(labelWidth))
20
- throw new RangeError(`Unknown labelWidth: ${String(labelWidth)}`);
21
- if (!denominationLabelAppearances.includes(labelAppearance))
22
- throw new RangeError(`Unknown labelAppearance: ${String(labelAppearance)}`);
23
- const labelClass = labelAppearance === 'plain' ? denominationLabelsPlainClass : undefined;
27
+ if (labelWidth !== undefined)
28
+ assertOneOf(denominationLabelWidths, labelWidth, 'labelWidth');
29
+ assertOneOf(denominationLabelAppearances, labelAppearance, 'labelAppearance');
30
+ assertOneOf(denominationGridFrames, frame, 'frame');
31
+ assertOneOf(denominationTotalEmphases, totalEmphasis, 'totalEmphasis');
32
+ assertOneOf(denominationCellToneAppearances, cellToneAppearance, 'cellToneAppearance');
33
+ const appearanceClass = cx(labelAppearance === 'plain' && denominationLabelsPlainClass, totalEmphasis === 'strong' && denominationTotalsStrongClass, cellToneAppearance === 'pill' && denominationCellTonesPillClass);
24
34
  if (layout === 'tiles')
25
- return (_jsx(DenominationTiles, { className: labelClass, columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
26
- return (_jsx(DenominationStrip, { className: labelClass, columns: columns, label: label, labelWidth: labelWidth, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
35
+ return (_jsx(DenominationTiles, { className: appearanceClass || undefined, columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
36
+ return (_jsx(DenominationStrip, { className: appearanceClass || undefined, columns: columns, framed: frame === 'table', label: label, labelWidth: labelWidth, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
27
37
  }
@@ -1,6 +1,8 @@
1
1
  import { type DialogHTMLAttributes, type ReactNode } from 'react';
2
2
  import { type Breakpoint } from '../css/breakpoints.js';
3
+ import { type DialogAppearance, dialogAppearances } from '../css/css-dialog-banded.js';
3
4
  import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
5
+ export { dialogAppearances, type DialogAppearance };
4
6
  export declare const dialogSizes: readonly ["md", "lg", "xl"];
5
7
  /**
6
8
  * `md` is the reading width (32rem) for a message or a short form; `lg`
@@ -74,6 +76,12 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
74
76
  summary?: ActionBarSummary;
75
77
  /** `alertdialog` for a warning that interrupts. Default `dialog`. */
76
78
  role?: DialogRole;
79
+ /**
80
+ * `banded` draws the dialog on the solid surface with its title row and
81
+ * footer as full-width bands over hairlines, and the close button on a
82
+ * quiet round fill; `glass` (the default) is one frosted panel.
83
+ */
84
+ appearance?: DialogAppearance;
77
85
  children: ReactNode;
78
86
  };
79
- export declare function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
87
+ export declare function Dialog({ 'aria-describedby': describedBy, appearance, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
@@ -3,17 +3,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { dialogSheetClass } from '../css/css-dialog.js';
6
+ import { dialogAppearances, dialogBandedClass, } from '../css/css-dialog-banded.js';
6
7
  import { spacingClassNames } from '../spacing-classes.js';
7
8
  import { useDialogCloseRequests } from './dialog-close-requests.js';
8
9
  import { assertActionBarSummary, } from './action-bar/ActionBarSummary.js';
9
10
  import { DialogFooter } from './dialog/DialogFooter.js';
10
11
  import { DialogTitle } from './dialog/DialogTitle.js';
11
12
  import { Stack } from './Stack.js';
13
+ export { dialogAppearances };
12
14
  export const dialogSizes = ['md', 'lg', 'xl'];
13
15
  const dialogRoles = ['dialog', 'alertdialog'];
14
- function assertDialogProps(size, role, description, summary) {
16
+ function assertDialogProps(size, role, description, summary, appearance) {
15
17
  if (!dialogSizes.includes(size))
16
18
  throw new RangeError(`Unknown Dialog size: ${String(size)}`);
19
+ if (!dialogAppearances.includes(appearance))
20
+ throw new RangeError(`Unknown Dialog appearance: ${String(appearance)}`);
17
21
  if (role !== undefined && !dialogRoles.includes(role))
18
22
  throw new RangeError(`Unknown Dialog role: ${String(role)}`);
19
23
  if (description !== undefined &&
@@ -22,8 +26,8 @@ function assertDialogProps(size, role, description, summary) {
22
26
  if (summary)
23
27
  assertActionBarSummary(summary);
24
28
  }
25
- export function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
26
- assertDialogProps(size, role, description, summary);
29
+ export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass', children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
30
+ assertDialogProps(size, role, description, summary, appearance);
27
31
  const titleId = useId();
28
32
  const descriptionId = useId();
29
33
  const hasFooter = Boolean(footer) || Boolean(summary);
@@ -35,5 +39,5 @@ export function Dialog({ 'aria-describedby': describedBy, children, className, c
35
39
  onPointerDown,
36
40
  onSubmit,
37
41
  });
38
- return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { summary: summary, children: footer })) : null] }) }));
42
+ return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { summary: summary, children: footer })) : null] }) }));
39
43
  }
@@ -1,6 +1,9 @@
1
1
  import { type SpacingStep } from '@scalewing/tokens';
2
2
  import { type ReactNode } from 'react';
3
+ import { type RequiredMark } from './FieldLabelText.js';
3
4
  export type FieldSize = 'xs' | 'md';
5
+ export declare const fieldVariants: readonly ["outlined", "search"];
6
+ export type FieldVariant = (typeof fieldVariants)[number];
4
7
  export type FieldProps = {
5
8
  children: ReactNode;
6
9
  gap?: SpacingStep;
@@ -31,9 +34,31 @@ export type FieldProps = {
31
34
  */
32
35
  changed?: boolean;
33
36
  required?: boolean;
37
+ /**
38
+ * `hidden` leaves out the label's " *" while the control stays required
39
+ * (and announced so); `visible` (default) draws it.
40
+ * When hidden, say in visible text which fields must be filled; a
41
+ * placeholder alone is not enough.
42
+ */
43
+ requiredMark?: RequiredMark;
34
44
  /** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
35
45
  prefix?: string;
36
46
  /** Short text inside the control frame after the value, such as a unit. Not part of the value. */
37
47
  suffix?: string;
48
+ /**
49
+ * `outlined` (default) is the labelled form field. `search` is a search
50
+ * box: the label is not drawn (it stays the input's name), a `leading`
51
+ * glyph sits at the frame's start, the input is `type="search"`, and a
52
+ * clear button named `clearLabel` shows while the input holds text and
53
+ * is enabled; it calls `onClear` (set the value to `''`) and puts focus
54
+ * back in the input. Needs one native `input` child with a `value`.
55
+ */
56
+ variant?: FieldVariant;
57
+ /** A search field's glyph at its start, such as a magnifier; hidden from assistive technology. */
58
+ leading?: ReactNode;
59
+ /** A search field's clear button name, such as "Clear search". */
60
+ clearLabel?: string;
61
+ /** Called by a search field's clear button; the consumer empties the value. */
62
+ onClear?: () => void;
38
63
  };
39
64
  export declare function Field(props: FieldProps): import("react").JSX.Element;
@@ -1,10 +1,34 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { cloneElement, isValidElement, useId } from 'react';
3
+ import { cloneElement, isValidElement, useId, } from 'react';
4
4
  import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
- import { FieldLabelText } from './FieldLabelText.js';
6
+ import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
7
+ import { SearchFrame } from './field/SearchFrame.js';
7
8
  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
+ }
8
32
  /**
9
33
  * `invalid` was passed, even as `false`, but not as `undefined` (a wrapper
10
34
  * forwarding an optional prop passes nothing): such a field needs one native
@@ -17,13 +41,16 @@ function passesInvalid(props) {
17
41
  return props.invalid !== undefined || props.changed !== undefined;
18
42
  }
19
43
  export function Field(props) {
20
- const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, prefix, suffix, } = 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);
46
+ const search = variant === 'search';
21
47
  const controlId = useId();
22
48
  const descriptionId = useId();
23
49
  const errorId = useId();
24
50
  const labelId = useId();
25
51
  const prefixId = useId();
26
52
  const suffixId = useId();
53
+ assertRequiredMark(requiredMark);
27
54
  const message = error || description;
28
55
  const marksInvalid = Boolean(error) || invalid;
29
56
  const adorned = Boolean(prefix || suffix);
@@ -32,17 +59,20 @@ export function Field(props) {
32
59
  'sw-field',
33
60
  size === 'xs' && 'sw-field-xs',
34
61
  marksInvalid && 'sw-field-invalid',
62
+ search && 'sw-field-search-field',
35
63
  changed && !marksInvalid && 'sw-field-changed',
36
64
  ]
37
65
  .filter(Boolean)
38
66
  .join(' ');
39
- const labelText = (_jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }));
67
+ // A search field hides its whole label element instead (see below).
68
+ const labelHidden = labelVisuallyHidden && !search;
69
+ const labelText = (_jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark, size: size, visuallyHidden: labelHidden }));
40
70
  if (!isValidElement(children) ||
41
71
  typeof children.type !== 'string' ||
42
72
  !['input', 'select', 'textarea'].includes(children.type)) {
43
73
  if (needsControl)
44
74
  throw new TypeError('Field validation and adornments require one native input, select, or textarea child');
45
- return (_jsxs(Stack, { as: "label", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [labelText, children] }));
75
+ return (_jsxs(Stack, { as: "label", className: className, gap: labelHidden ? 0 : gap, children: [labelText, children] }));
46
76
  }
47
77
  if (adorned && children.type !== 'input')
48
78
  throw new TypeError('Field prefix and suffix require a native input child');
@@ -63,6 +93,26 @@ export function Field(props) {
63
93
  'aria-invalid': marksInvalid ? true : children.props['aria-invalid'],
64
94
  'aria-labelledby': adornment.labelledBy,
65
95
  required: required || children.props.required,
96
+ ...(search
97
+ ? {
98
+ type: 'search',
99
+ enterKeyHint: children.props.enterKeyHint ?? 'search',
100
+ // Escape empties the box through onClear, the one clearing path
101
+ // (WebKit and Blink would otherwise clear it themselves).
102
+ onKeyDown: (event) => {
103
+ children.props.onKeyDown?.(event);
104
+ if (event.key === 'Escape' &&
105
+ !event.defaultPrevented &&
106
+ !event.currentTarget.readOnly &&
107
+ event.currentTarget.value !== '') {
108
+ event.preventDefault();
109
+ onClear?.();
110
+ }
111
+ },
112
+ }
113
+ : {}),
66
114
  });
67
- return (_jsxs(Stack, { as: "div", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [_jsx("label", { htmlFor: children.props.id || controlId, id: labelId, children: labelText }), adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx("span", { className: "sw-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
115
+ const value = children.props.value;
116
+ const hasText = value !== undefined && value !== null && String(value) !== '';
117
+ return (_jsxs(Stack, { as: "div", className: className, gap: labelHidden ? 0 : gap, children: [_jsx("label", { className: search ? 'sw-sr-only' : undefined, htmlFor: children.props.id || controlId, id: labelId, children: labelText }), search ? (_jsx(SearchFrame, { clearLabel: clearLabel, leading: leading, onClear: onClear, showClear: hasText && !children.props.disabled && !children.props.readOnly, children: control })) : adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx("span", { className: "sw-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
68
118
  }
@@ -1,13 +1,31 @@
1
+ export declare const requiredMarks: readonly ["visible", "hidden"];
2
+ /**
3
+ * Whether a required control's label shows the required mark: `visible`
4
+ * (the default) draws " *" after the label; `hidden` leaves it out, for a
5
+ * form that marks nothing as required (or marks what is optional instead).
6
+ * The control stays required either way, so assistive technology still
7
+ * hears it. When hidden, say in visible text which fields must be filled
8
+ * (a form-level note, or "Required" in the description); a placeholder
9
+ * alone is not enough (WCAG 3.3.2).
10
+ */
11
+ export type RequiredMark = (typeof requiredMarks)[number];
12
+ /** Fails closed on a value that is not a known required mark. */
13
+ export declare function assertRequiredMark(value: unknown): void;
14
+ /** Whether the mark is drawn: a required control whose mark is visible. */
15
+ export declare function showsRequiredMark(required: boolean, requiredMark: RequiredMark): boolean;
1
16
  /**
2
17
  * A form control's label words and its required mark, as `Field` draws
3
18
  * them. The `<label>` around it inherits the canvas's body type, so its
4
19
  * line box, and the gap to the control below, is the same for every field
5
- * that uses it (`Field`, `DateField` and `Select`). The mark is `aria-hidden`: the
6
- * control's own required state is what assistive technology reads.
20
+ * that uses it (`Field`, `DateField` and `Select`; `Combobox` through
21
+ * `Field`, and `FilterChips` shares `showsRequiredMark` for its legend). The mark is `aria-hidden`: the
22
+ * control's own required state is what assistive technology reads, and
23
+ * `requiredMark="hidden"` leaves it out.
7
24
  */
8
- export declare function FieldLabelText({ label, required, size, visuallyHidden, }: {
25
+ export declare function FieldLabelText({ label, required, requiredMark, size, visuallyHidden, }: {
9
26
  label: string;
10
27
  required?: boolean;
28
+ requiredMark?: RequiredMark;
11
29
  size?: 'xs' | 'md';
12
30
  visuallyHidden?: boolean;
13
31
  }): import("react").JSX.Element;
@@ -1,12 +1,24 @@
1
1
  import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from './Text.js';
3
+ export const requiredMarks = ['visible', 'hidden'];
4
+ /** Fails closed on a value that is not a known required mark. */
5
+ export function assertRequiredMark(value) {
6
+ if (!requiredMarks.includes(value))
7
+ throw new RangeError(`Unknown requiredMark: ${String(value)}`);
8
+ }
9
+ /** Whether the mark is drawn: a required control whose mark is visible. */
10
+ export function showsRequiredMark(required, requiredMark) {
11
+ return required && requiredMark === 'visible';
12
+ }
3
13
  /**
4
14
  * A form control's label words and its required mark, as `Field` draws
5
15
  * them. The `<label>` around it inherits the canvas's body type, so its
6
16
  * line box, and the gap to the control below, is the same for every field
7
- * that uses it (`Field`, `DateField` and `Select`). The mark is `aria-hidden`: the
8
- * control's own required state is what assistive technology reads.
17
+ * that uses it (`Field`, `DateField` and `Select`; `Combobox` through
18
+ * `Field`, and `FilterChips` shares `showsRequiredMark` for its legend). The mark is `aria-hidden`: the
19
+ * control's own required state is what assistive technology reads, and
20
+ * `requiredMark="hidden"` leaves it out.
9
21
  */
10
- export function FieldLabelText({ label, required = false, size = 'md', visuallyHidden = false, }) {
11
- return (_jsxs(Text, { as: "span", className: visuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: [label, required ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }));
22
+ export function FieldLabelText({ label, required = false, requiredMark = 'visible', size = 'md', visuallyHidden = false, }) {
23
+ return (_jsxs(Text, { as: "span", className: visuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }));
12
24
  }
@@ -1,3 +1,4 @@
1
+ import { type RequiredMark } from './FieldLabelText.js';
1
2
  export type FilterChipOption = {
2
3
  value: string;
3
4
  label: string;
@@ -16,6 +17,13 @@ export type FilterChipsProps = {
16
17
  * which reads the radios' own state).
17
18
  */
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;
19
27
  /**
20
28
  * A message under the chips, such as "Pick a reason": a polite live
21
29
  * region linked to the group by `aria-describedby`; the group is marked
@@ -26,4 +34,4 @@ export type FilterChipsProps = {
26
34
  /** More ids that describe the group, such as a hint beside it. */
27
35
  'aria-describedby'?: string;
28
36
  };
29
- export declare function FilterChips({ label, value, onChange, options, required, error, 'aria-describedby': describedByProp, }: FilterChipsProps): import("react").JSX.Element;
37
+ export declare function FilterChips({ label, value, onChange, options, required, requiredMark, error, 'aria-describedby': describedByProp, }: FilterChipsProps): import("react").JSX.Element;
@@ -3,10 +3,11 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
+ import { assertRequiredMark, showsRequiredMark, } from './FieldLabelText.js';
6
7
  function isCount(count) {
7
8
  return count === undefined || (Number.isInteger(count) && count >= 0);
8
9
  }
9
- export function FilterChips({ label, value, onChange, options, required = false, error, 'aria-describedby': describedByProp, }) {
10
+ export function FilterChips({ label, value, onChange, options, required = false, requiredMark = 'visible', error, 'aria-describedby': describedByProp, }) {
10
11
  const name = useId();
11
12
  const errorId = useId();
12
13
  const describedBy = [describedByProp?.trim() || null, error ? errorId : null]
@@ -15,6 +16,7 @@ export function FilterChips({ label, value, onChange, options, required = false,
15
16
  const values = options.map((option) => option.value);
16
17
  if (!label.trim())
17
18
  throw new RangeError('label must not be empty');
19
+ assertRequiredMark(requiredMark);
18
20
  if (options.length === 0 ||
19
21
  options.some((option) => !option.value.trim() || !option.label.trim()))
20
22
  throw new RangeError('options must have nonempty values and labels');
@@ -24,7 +26,7 @@ export function FilterChips({ label, value, onChange, options, required = false,
24
26
  throw new RangeError('value must match an option or be empty');
25
27
  if (options.some((option) => !isCount(option.count)))
26
28
  throw new RangeError('count must be a non-negative integer');
27
- 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, required ? (_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) => {
29
+ 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) => {
28
30
  if (event.currentTarget.checked && !option.disabled)
29
31
  onChange(option.value);
30
32
  }, required: required, 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 })] }));
@@ -1,3 +1,15 @@
1
+ import { type NavAppearance, navAppearances } from '../css/css-nav-words.js';
1
2
  import { type BoxProps } from './Box.js';
2
- export type NavProps = Omit<BoxProps, 'as'>;
3
- export declare function Nav({ className, ...rest }: NavProps): import("react").JSX.Element;
3
+ export { navAppearances, type NavAppearance };
4
+ export type NavProps = Omit<BoxProps, 'as'> & {
5
+ /**
6
+ * `words` draws every destination (a link or a Button) as plain words:
7
+ * the current one (`aria-current`) in the text color at label weight,
8
+ * the others muted, with no fill or underline. The words must be visible:
9
+ * a glyph-only destination has no weight to change, so its current mark
10
+ * would be color alone (keep `default` there). `default` keeps each
11
+ * destination's own look.
12
+ */
13
+ appearance?: NavAppearance;
14
+ };
15
+ export declare function Nav({ appearance, className, ...rest }: NavProps): import("react").JSX.Element;
@@ -1,7 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { cx } from '../class-names.js';
4
+ import { navAppearances, navWordsClass, } from '../css/css-nav-words.js';
4
5
  import { Box } from './Box.js';
5
- export function Nav({ className, ...rest }) {
6
- return _jsx(Box, { as: "nav", className: cx('sw-nav', className), ...rest });
6
+ export { navAppearances };
7
+ export function Nav({ appearance = 'default', className, ...rest }) {
8
+ if (!navAppearances.includes(appearance))
9
+ throw new RangeError(`Unknown Nav appearance: ${String(appearance)}`);
10
+ return (_jsx(Box, { as: "nav", className: cx('sw-nav', appearance === 'words' && navWordsClass, className), ...rest }));
7
11
  }
@@ -22,9 +22,12 @@ type SegmentedLabel = {
22
22
  * `compact` is the quiet chip track for a section switch; `filled` gives every
23
23
  * segment the same width and paints the selected one in the accent, for a
24
24
  * choice that decides what a form does. It sizes to its container: the full
25
- * width in a Stack, the width of its widest label in an Inline.
25
+ * width in a Stack, the width of its widest label in an Inline. `raised` is
26
+ * a grey track whose selected segment is the surface raised by a small
27
+ * shadow, every segment at the md control height with its words in the
28
+ * text color, sized to its segments even in a Stack (unlike `compact`).
26
29
  */
27
- export type SegmentedControlVariant = 'compact' | 'filled';
30
+ export type SegmentedControlVariant = 'compact' | 'filled' | 'raised';
28
31
  /**
29
32
  * A disabled control can never report a change, so it needs no `onChange`;
30
33
  * a live one must have it.
@@ -2,6 +2,7 @@
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 { segmentedRaisedClass } from '../css/css-segmented-raised.js';
5
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
7
  export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
7
8
  const errorId = useId();
@@ -23,7 +24,7 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
23
24
  }
24
25
  // The wrapper is always rendered, so an error that appears later swaps
25
26
  // text into a live region that already exists and the group keeps focus.
26
- 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', withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
27
+ 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) => {
27
28
  const selected = item.id === value;
28
29
  return (_jsxs("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
29
30
  if (!selected) {
@@ -1,5 +1,6 @@
1
1
  import { type SelectOption } from '../select-list.js';
2
2
  import { type FieldSize } from './Field.js';
3
+ import { type RequiredMark } from './FieldLabelText.js';
3
4
  export type { SelectOption };
4
5
  /**
5
6
  * `content` (the default) sizes the field to its longest option, as a native
@@ -26,6 +27,13 @@ export type SelectProps = {
26
27
  placeholder?: string;
27
28
  /** Marks the label as Field does and sets `aria-required`. */
28
29
  required?: boolean;
30
+ /**
31
+ * `hidden` leaves out the label's " *" while the control stays required
32
+ * (and announced so); `visible` (default) draws it.
33
+ * When hidden, say in visible text which fields must be filled; a
34
+ * placeholder alone is not enough.
35
+ */
36
+ requiredMark?: RequiredMark;
29
37
  /** A validation message under the control, wired as Field's error is. */
30
38
  error?: string;
31
39
  /**
@@ -35,4 +43,4 @@ export type SelectProps = {
35
43
  */
36
44
  width?: SelectWidth;
37
45
  };
38
- export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, width, }: SelectProps): import("react").JSX.Element;
46
+ export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;
@@ -4,12 +4,13 @@ import { useId, useRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { selectedSelectIndex } from '../select-list.js';
6
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
7
- import { FieldLabelText } from './FieldLabelText.js';
7
+ import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
8
8
  import { SelectListbox } from './select/SelectListbox.js';
9
9
  import { SelectValue } from './select/SelectValue.js';
10
10
  import { useSelectList } from './select/use-select-list.js';
11
11
  import { Stack } from './Stack.js';
12
- export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, width = 'content', }) {
12
+ export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, requiredMark = 'visible', error, width = 'content', }) {
13
+ assertRequiredMark(requiredMark);
13
14
  if (placeholder !== undefined && !placeholder.trim())
14
15
  throw new RangeError('placeholder must not be empty');
15
16
  const labelId = useId();
@@ -22,7 +23,7 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
22
23
  const showsPlaceholder = placeholder !== undefined &&
23
24
  !options.some((option) => option.value === value);
24
25
  const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
25
- return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
26
+ return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
26
27
  const next = event.relatedTarget;
27
28
  if (next instanceof Node && rootRef.current?.contains(next)) {
28
29
  return;
@@ -1,5 +1,12 @@
1
1
  /** `fixed` sizes the count for a few digits; `full` fills its container, such as a table cell. */
2
2
  export type StepperWidth = 'fixed' | 'full';
3
+ export declare const stepperLimitAppearances: readonly ["faded", "plain"];
4
+ /**
5
+ * How − looks at `min` and + at `max`, where a press cannot move the count:
6
+ * `faded` (the default) fades the button as any disabled control; `plain`
7
+ * keeps its usual look. Either way the button is disabled.
8
+ */
9
+ export type StepperLimitAppearance = (typeof stepperLimitAppearances)[number];
3
10
  export type StepperProps = {
4
11
  /** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
5
12
  label: string;
@@ -35,6 +42,12 @@ export type StepperProps = {
35
42
  placeholder?: string;
36
43
  name?: string;
37
44
  width?: StepperWidth;
45
+ /**
46
+ * `plain` keeps − at `min` and + at `max` in their usual look instead of
47
+ * fading them (`faded`, the default). The button is still disabled, and a
48
+ * disabled or read-only Stepper is unchanged.
49
+ */
50
+ limitAppearance?: StepperLimitAppearance;
38
51
  };
39
52
  /**
40
53
  * A whole-number count between − and + buttons that can also be typed: one