@scalewing/react 1.27.0 → 1.29.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 (67) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +99 -3
  3. package/dist/components/Checkbox.js +5 -4
  4. package/dist/components/DenominationGrid.d.ts +4 -1
  5. package/dist/components/DenominationGrid.js +8 -3
  6. package/dist/components/Dialog.d.ts +28 -4
  7. package/dist/components/Dialog.js +20 -2
  8. package/dist/components/GlyphTile.d.ts +21 -0
  9. package/dist/components/GlyphTile.js +17 -0
  10. package/dist/components/MultiSelect.d.ts +10 -0
  11. package/dist/components/MultiSelect.js +62 -0
  12. package/dist/components/Notice.d.ts +69 -0
  13. package/dist/components/Notice.js +48 -0
  14. package/dist/components/Progress.d.ts +3 -1
  15. package/dist/components/Progress.js +4 -0
  16. package/dist/components/SegmentedControl.d.ts +8 -0
  17. package/dist/components/SegmentedControl.js +4 -3
  18. package/dist/components/Toast.d.ts +20 -4
  19. package/dist/components/Toast.js +44 -21
  20. package/dist/components/checkbox/CheckboxControl.d.ts +9 -0
  21. package/dist/components/checkbox/CheckboxControl.js +9 -0
  22. package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
  23. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  24. package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
  25. package/dist/components/denomination-grid/DenominationTiles.js +2 -2
  26. package/dist/components/denomination-grid/types.d.ts +9 -0
  27. package/dist/components/dialog/DialogFooter.d.ts +14 -0
  28. package/dist/components/dialog/DialogFooter.js +11 -0
  29. package/dist/components/dialog/DialogTitle.d.ts +11 -4
  30. package/dist/components/dialog/DialogTitle.js +8 -6
  31. package/dist/components/multi-select/MultiSelectPanel.d.ts +25 -0
  32. package/dist/components/multi-select/MultiSelectPanel.js +41 -0
  33. package/dist/components/multi-select/types.d.ts +35 -0
  34. package/dist/components/multi-select/types.js +1 -0
  35. package/dist/components/multi-select/validate.d.ts +3 -0
  36. package/dist/components/multi-select/validate.js +25 -0
  37. package/dist/components/toast/ToastContent.d.ts +22 -0
  38. package/dist/components/toast/ToastContent.js +21 -0
  39. package/dist/components/toast/use-toast-hold.d.ts +28 -0
  40. package/dist/components/toast/use-toast-hold.js +53 -0
  41. package/dist/components/toast/use-toast-timer.d.ts +15 -0
  42. package/dist/components/toast/use-toast-timer.js +34 -0
  43. package/dist/css/css-components.js +16 -0
  44. package/dist/css/css-denomination-grid.js +4 -0
  45. package/dist/css/css-denomination-label.d.ts +10 -0
  46. package/dist/css/css-denomination-label.js +36 -0
  47. package/dist/css/css-dialog-frame.d.ts +2 -0
  48. package/dist/css/css-dialog-frame.js +83 -0
  49. package/dist/css/css-dialog.js +20 -4
  50. package/dist/css/css-glyph-tile.d.ts +5 -0
  51. package/dist/css/css-glyph-tile.js +57 -0
  52. package/dist/css/css-multi-select.d.ts +2 -0
  53. package/dist/css/css-multi-select.js +156 -0
  54. package/dist/css/css-notice.d.ts +22 -0
  55. package/dist/css/css-notice.js +168 -0
  56. package/dist/css/css-progress.d.ts +3 -0
  57. package/dist/css/css-progress.js +13 -6
  58. package/dist/css/css-segmented.js +58 -0
  59. package/dist/css/css-toast-card.d.ts +5 -0
  60. package/dist/css/css-toast-card.js +122 -0
  61. package/dist/css/css-variables.js +12 -1
  62. package/dist/css/stacking.js +5 -2
  63. package/dist/css/stylesheet.js +10 -0
  64. package/dist/index.d.ts +8 -5
  65. package/dist/index.js +7 -4
  66. package/dist/styles.css +522 -3
  67. package/package.json +2 -2
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, forwardRef, } from 'react';
3
+ import { cx } from '../class-names.js';
4
+ import { noticeTones, noticeVariants, } from '../css/css-notice.js';
5
+ export { noticeTones, noticeVariants };
6
+ const noticeRoles = ['status', 'alert', 'none'];
7
+ function defaultRole(tone, variant) {
8
+ if (variant === 'band')
9
+ return 'none';
10
+ return tone === 'danger' ? 'alert' : 'status';
11
+ }
12
+ /**
13
+ * The message's parts, without the empty ones (`false`, `null`,
14
+ * `undefined`, `''`) a condition leaves; fragments flatten.
15
+ */
16
+ function messageParts(children) {
17
+ return Children.toArray(children).filter((part) => part !== '');
18
+ }
19
+ /** A sentence, even one with values interpolated, may share the title's row. */
20
+ function isText(parts) {
21
+ return parts.every((part) => typeof part === 'string' || typeof part === 'number');
22
+ }
23
+ function assertNoticeProps(tone, title, role, variant, hasMessage) {
24
+ if (!noticeVariants.includes(variant))
25
+ throw new RangeError(`Unknown Notice variant: ${String(variant)}`);
26
+ if (!noticeTones.includes(tone))
27
+ throw new RangeError(`Unknown Notice tone: ${String(tone)}`);
28
+ if (title !== undefined && (typeof title !== 'string' || !title.trim()))
29
+ throw new RangeError('Notice title must not be empty');
30
+ if (role !== undefined && !noticeRoles.includes(role))
31
+ throw new RangeError(`Unknown Notice role: ${String(role)}`);
32
+ if (title === undefined && !hasMessage)
33
+ throw new RangeError('Notice needs a title or a message');
34
+ }
35
+ /**
36
+ * A tinted note in the page's flow: information, a good outcome, a caution
37
+ * or a refusal, with an optional bold lead, glyph and one action. It stays
38
+ * until the consumer removes it. `variant="band"` is the same note as a
39
+ * full-width strip of page chrome. Pass `tabIndex={-1}` and a ref to move
40
+ * focus to it.
41
+ */
42
+ export const Notice = forwardRef(function Notice({ action, children, className, icon, role, title, tone, variant = 'default', ...rest }, ref) {
43
+ const parts = messageParts(children);
44
+ const hasMessage = parts.length > 0;
45
+ assertNoticeProps(tone, title, role, variant, hasMessage);
46
+ const announced = role ?? defaultRole(tone, variant);
47
+ return (_jsxs("div", { ...rest, ref: ref, className: cx('sw-notice', `sw-notice-${tone}`, variant === 'band' && 'sw-notice-band', className), role: announced === 'none' ? undefined : announced, children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-notice-icon", children: icon })) : null, _jsxs("div", { className: cx('sw-notice-content', hasMessage && !isText(parts) && 'sw-notice-stacked'), children: [title ? _jsx("strong", { className: "sw-notice-title", children: title }) : null, hasMessage ? _jsx("div", { className: "sw-notice-body", children: children }) : null] }), action ? _jsx("div", { className: "sw-notice-action", children: action }) : null] }));
48
+ });
@@ -1,4 +1,6 @@
1
- export type ProgressTone = 'accent' | 'success' | 'danger';
1
+ import { type ProgressTone, progressTones } from '../css/css-progress.js';
2
+ export { progressTones };
3
+ export type { ProgressTone };
2
4
  export type ProgressProps = {
3
5
  label: string;
4
6
  value: number;
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
+ import { progressTones } from '../css/css-progress.js';
5
+ export { progressTones };
4
6
  export function Progress({ label, value, max, tone = 'accent', showCount = true, }) {
5
7
  if (!label.trim())
6
8
  throw new RangeError('label must not be empty');
@@ -8,6 +10,8 @@ export function Progress({ label, value, max, tone = 'accent', showCount = true,
8
10
  throw new RangeError('max must be a positive finite number');
9
11
  if (!Number.isFinite(value) || value < 0 || value > max)
10
12
  throw new RangeError('value must be finite and within 0..max');
13
+ if (!progressTones.includes(tone))
14
+ throw new RangeError(`Unknown Progress tone: ${String(tone)}`);
11
15
  const labelId = useId();
12
16
  return (_jsxs("div", { className: `sw-progress sw-progress-${tone}`, children: [_jsxs("div", { className: "sw-progress-heading", children: [_jsx("span", { className: "sw-progress-label", id: labelId, children: label }), showCount ? (_jsxs("span", { "aria-hidden": "true", className: "sw-progress-count", children: [value, " / ", max] })) : null] }), _jsx("progress", { "aria-labelledby": labelId, className: "sw-progress-bar", max: max, value: value })] }));
13
17
  }
@@ -2,6 +2,14 @@ import { type ReactNode } from 'react';
2
2
  export type SegmentedItem = {
3
3
  id: string;
4
4
  label: ReactNode;
5
+ /**
6
+ * A consumer mark before the label, such as a `GlyphTile` or a Lucide
7
+ * glyph, hidden from assistive technology (the label names the item).
8
+ * When any item has one, every item of a compact track takes the `md`
9
+ * control height and label type on a subtle track; a filled track keeps
10
+ * its look and gains the marks. A falsy value (`0`, `''`) is no icon.
11
+ */
12
+ icon?: ReactNode;
5
13
  };
6
14
  type SegmentedLabel = {
7
15
  'aria-label': string;
@@ -5,6 +5,7 @@ import { cx } from '../class-names.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
6
  export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
7
7
  const errorId = useId();
8
+ const withIcons = items.some((item) => item.icon);
8
9
  function onKeyDown(event) {
9
10
  if (disabled || (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft')) {
10
11
  return;
@@ -22,12 +23,12 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
22
23
  }
23
24
  // The wrapper is always rendered, so an error that appears later swaps
24
25
  // text into a live region that already exists and the group keeps focus.
25
- 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', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
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) => {
26
27
  const selected = item.id === value;
27
- return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
28
+ return (_jsxs("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
28
29
  if (!selected) {
29
30
  onChange?.(item.id);
30
31
  }
31
- }, role: "radio", type: "button", children: item.label }, item.id));
32
+ }, role: "radio", type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, item.label] }, item.id));
32
33
  }) }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
33
34
  });
@@ -1,8 +1,10 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
2
  import { toastTones, type ToastTone } from '../css/css-toast.js';
3
- export { toastTones };
4
- export type { ToastTone };
5
- export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popover'> & {
3
+ import { toastPlacements, type ToastPlacement } from '../css/css-toast-card.js';
4
+ import { type ToastAction } from './toast/ToastContent.js';
5
+ export { toastPlacements, toastTones };
6
+ export type { ToastAction, ToastPlacement, ToastTone };
7
+ export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popover' | 'title'> & {
6
8
  open: boolean;
7
9
  onOpenChange: (open: boolean) => void;
8
10
  /**
@@ -19,6 +21,20 @@ export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popo
19
21
  tone?: ToastTone;
20
22
  /** A consumer glyph before the message, hidden from assistive technology. */
21
23
  icon?: ReactNode;
24
+ /** A bold first line; the children become the muted detail under it. */
25
+ title?: string;
26
+ /**
27
+ * One press at the end of the toast, such as "Open". It runs `onPress`,
28
+ * then closes the toast. The toast takes the pointer while it has one.
29
+ */
30
+ action?: ToastAction;
31
+ /** A thin line under the message that runs down with the time left. */
32
+ showRemaining?: boolean;
33
+ /**
34
+ * `bottom` (default) centers it near the bottom edge; `top-end` puts it
35
+ * in the top corner at the end of the line, as a card.
36
+ */
37
+ placement?: ToastPlacement;
22
38
  children: ReactNode;
23
39
  };
24
- export declare function Toast({ anchor, children, className, onOpenChange, open, target, timeoutMs, tone, icon, ...rest }: ToastProps): import("react").JSX.Element;
40
+ export declare function Toast({ action, anchor, children, className, icon, onBlur, onFocus, onOpenChange, onPointerEnter, onPointerLeave, open, placement, showRemaining, style, target, timeoutMs, title, tone, ...rest }: ToastProps): import("react").JSX.Element;
@@ -1,11 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useLayoutEffect, useEffect, useRef, } from 'react';
3
+ import { useCallback, useLayoutEffect, useRef, } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { toastTones } from '../css/css-toast.js';
6
+ import { toastPlacements } from '../css/css-toast-card.js';
6
7
  import { spacingClassNames } from '../spacing-classes.js';
7
8
  import { applyTravelVars, clearTravelVars, travelTranslate, } from '../toast-travel.js';
8
- export { toastTones };
9
+ import { assertToastContent, ToastContent, } from './toast/ToastContent.js';
10
+ import { useToastHold } from './toast/use-toast-hold.js';
11
+ import { useToastTimer } from './toast/use-toast-timer.js';
12
+ export { toastPlacements, toastTones };
9
13
  const DEFAULT_TIMEOUT_MS = 800;
10
14
  /** A warning or an error must be read, not glimpsed. */
11
15
  const URGENT_TIMEOUT_MS = 6000;
@@ -17,10 +21,28 @@ function defaultTimeout(tone) {
17
21
  function travels(anchor, target) {
18
22
  return Boolean(anchor && target);
19
23
  }
20
- export function Toast({ anchor = null, children, className, onOpenChange, open, target = null, timeoutMs, tone = 'neutral', icon, ...rest }) {
24
+ function reducedMotion() {
25
+ return (typeof window !== 'undefined' &&
26
+ typeof window.matchMedia === 'function' &&
27
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches);
28
+ }
29
+ export function Toast({ action, anchor = null, children, className, icon, onBlur, onFocus, onOpenChange, onPointerEnter, onPointerLeave, open, placement = 'bottom', showRemaining = false, style, target = null, timeoutMs, title, tone = 'neutral', ...rest }) {
30
+ assertToastContent(title, action);
31
+ if (!toastPlacements.includes(placement))
32
+ throw new RangeError(`Unknown Toast placement: ${String(placement)}`);
21
33
  const nodeRef = useRef(null);
22
34
  const dismissAfter = timeoutMs ?? defaultTimeout(tone);
23
35
  const moving = travels(anchor, target);
36
+ const interactive = !moving && (Boolean(action) || showRemaining);
37
+ const { handlers, paused, returnFocus } = useToastHold({
38
+ enabled: interactive,
39
+ onBlur,
40
+ onFocus,
41
+ onPointerEnter,
42
+ onPointerLeave,
43
+ open,
44
+ });
45
+ const close = useCallback(() => onOpenChange(false), [onOpenChange]);
24
46
  useLayoutEffect(() => {
25
47
  const node = nodeRef.current;
26
48
  if (!node || typeof node.showPopover !== 'function') {
@@ -28,6 +50,7 @@ export function Toast({ anchor = null, children, className, onOpenChange, open,
28
50
  }
29
51
  if (!open) {
30
52
  if (node.matches(':popover-open')) {
53
+ returnFocus(node);
31
54
  node.hidePopover();
32
55
  }
33
56
  clearTravelVars(node);
@@ -44,28 +67,28 @@ export function Toast({ anchor = null, children, className, onOpenChange, open,
44
67
  if (!node.matches(':popover-open')) {
45
68
  node.showPopover();
46
69
  }
47
- }, [anchor, moving, open, target]);
48
- useEffect(() => {
49
- if (!open) {
50
- return;
51
- }
52
- const reducedMotion = typeof window.matchMedia === 'function' &&
53
- window.matchMedia('(prefers-reduced-motion: reduce)').matches;
54
- if (moving && !reducedMotion) {
55
- return;
56
- }
57
- const timer = window.setTimeout(() => {
58
- onOpenChange(false);
59
- }, dismissAfter);
60
- return () => {
61
- window.clearTimeout(timer);
62
- };
63
- }, [dismissAfter, moving, open, onOpenChange]);
70
+ }, [anchor, moving, open, returnFocus, target]);
71
+ // A travelling toast closes when its animation ends, unless motion is
72
+ // reduced; every other toast closes on the timer, held while paused.
73
+ useToastTimer({
74
+ duration: dismissAfter,
75
+ onExpire: close,
76
+ paused,
77
+ running: open && (!moving || reducedMotion()),
78
+ });
64
79
  function onAnimationEnd(event) {
65
80
  if (event.target !== event.currentTarget || !moving) {
66
81
  return;
67
82
  }
68
83
  onOpenChange(false);
69
84
  }
70
- return (_jsx("div", { ...rest, ref: nodeRef, className: cx('sw-toast', tone !== 'neutral' && `sw-toast-${tone}`, moving && 'sw-toast-travel', ...spacingClassNames({ padding: 3 }), className), onAnimationEnd: onAnimationEnd, popover: "manual", role: tone === 'danger' ? 'alert' : 'status', children: icon ? (_jsxs("div", { className: "sw-toast-row", children: [_jsx("span", { "aria-hidden": "true", className: "sw-toast-icon", children: icon }), _jsx("div", { className: "sw-toast-body", children: children })] })) : (children) }));
85
+ return (_jsxs("div", { ...rest, ref: nodeRef, className: cx('sw-toast', tone !== 'neutral' && `sw-toast-${tone}`, !moving && placement !== 'bottom' && `sw-toast-${placement}`, interactive && 'sw-toast-interactive', paused && 'sw-toast-paused', moving && 'sw-toast-travel', ...spacingClassNames({ padding: 3 }), className), onAnimationEnd: onAnimationEnd, ...handlers, popover: "manual", role: tone === 'danger' ? 'alert' : 'status', style: showRemaining
86
+ ? {
87
+ ...style,
88
+ '--sw-toast-duration': `${dismissAfter}ms`,
89
+ }
90
+ : style, children: [_jsx(ToastContent, { action: action, icon: icon, onAction: () => {
91
+ action?.onPress();
92
+ close();
93
+ }, title: title, children: children }), showRemaining && open && !moving ? (_jsx("span", { "aria-hidden": "true", className: "sw-toast-remaining" })) : null] }));
71
94
  }
@@ -0,0 +1,9 @@
1
+ import { type InputHTMLAttributes } from 'react';
2
+ type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'>;
3
+ /**
4
+ * The native checkbox and its drawn mark, the one markup Checkbox's
5
+ * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
6
+ * and MultiSelect's rows so the two cannot drift apart.
7
+ */
8
+ export declare function CheckboxControl(props: CheckboxControlProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The native checkbox and its drawn mark, the one markup Checkbox's
4
+ * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
5
+ * and MultiSelect's rows so the two cannot drift apart.
6
+ */
7
+ export function CheckboxControl(props) {
8
+ return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
9
+ }
@@ -1,4 +1,7 @@
1
1
  import { type DenominationGridProps } from './types.js';
2
- type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'>;
3
- export declare function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
2
+ type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'> & {
3
+ /** The row labels' appearance class, if any. */
4
+ className?: string;
5
+ };
6
+ export declare function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
4
7
  export {};
@@ -16,7 +16,7 @@ function TotalHead({ totalLabel }) {
16
16
  return _jsx("td", { className: "sw-denomination-corner sw-denomination-total-head" });
17
17
  return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
18
18
  }
19
- export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
19
+ export function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
20
20
  const hasTotals = rows.some((row) => row.total !== undefined);
21
21
  // An aligned strip sizes its columns from the region's width; it needs
22
22
  // the column count and whether a total column takes a share.
@@ -26,5 +26,5 @@ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel,
26
26
  '--sw-denomination-columns': columns.length,
27
27
  '--sw-denomination-total': hasTotals ? 1 : 0,
28
28
  };
29
- return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth)), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
29
+ return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
30
30
  }
@@ -1,4 +1,7 @@
1
1
  import { type DenominationGridProps } from './types.js';
2
- type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'>;
3
- export declare function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
2
+ type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'> & {
3
+ /** The row labels' appearance class, if any. */
4
+ className?: string;
5
+ };
6
+ export declare function DenominationTiles({ className, label, columns, rows, rowRole, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
4
7
  export {};
@@ -21,8 +21,8 @@ function showsRowLabel(rowCount, row) {
21
21
  function namesRow(rowCount, row, gridLabel) {
22
22
  return showsRowLabel(rowCount, row) || row.label !== gridLabel;
23
23
  }
24
- export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }) {
25
- return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => {
24
+ export function DenominationTiles({ className, label, columns, rows, rowRole, subtotal, zeroLabel, }) {
25
+ return (_jsx("div", { "aria-label": label, className: cx('sw-denomination-grid sw-denomination-tiles', className), role: "group", children: rows.map((row) => {
26
26
  const labelled = showsRowLabel(rows.length, row);
27
27
  const named = namesRow(rows.length, row, label);
28
28
  // A named section is a region; role="group" keeps the name without
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type BadgeTone } from '../../css/css-data.js';
3
+ import { type DenominationLabelAppearance } from '../../css/css-denomination-label.js';
3
4
  import { type DenominationLabelWidth } from '../../css/css-denomination-aligned.js';
4
5
  export type { DenominationLabelWidth };
5
6
  export type DenominationGridTone = BadgeTone;
@@ -102,4 +103,12 @@ export type DenominationGridProps = {
102
103
  * strip has a label column.
103
104
  */
104
105
  labelWidth?: DenominationLabelWidth;
106
+ /**
107
+ * `tone` (the default) marks each row label with its row's tone: a
108
+ * stripe at its start and the words in the tone. `plain` sets every row
109
+ * label in the text color at label weight with no stripe, for a grid
110
+ * whose rows need no color to tell apart; a row's tone still colors its
111
+ * total.
112
+ */
113
+ labelAppearance?: DenominationLabelAppearance;
105
114
  };
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ActionBarSummary } from '../action-bar/ActionBarSummary.js';
3
+ type DialogFooterProps = {
4
+ summary?: ActionBarSummary;
5
+ children?: ReactNode;
6
+ };
7
+ /**
8
+ * The dialog's pinned footer: the summary figure (in a polite live region,
9
+ * as in ActionBar) at the start, the actions at the end. Rendered only when
10
+ * the dialog has a footer or a summary, and then always with its status
11
+ * region, so a new figure is announced.
12
+ */
13
+ export declare function DialogFooter({ children, summary }: DialogFooterProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ActionBarSummaryLine, } from '../action-bar/ActionBarSummary.js';
3
+ /**
4
+ * The dialog's pinned footer: the summary figure (in a polite live region,
5
+ * as in ActionBar) at the start, the actions at the end. Rendered only when
6
+ * the dialog has a footer or a summary, and then always with its status
7
+ * region, so a new figure is announced.
8
+ */
9
+ export function DialogFooter({ children, summary }) {
10
+ return (_jsxs("div", { className: "sw-dialog-footer", children: [_jsx("div", { className: "sw-dialog-footer-status", role: "status", children: summary ? _jsx(ActionBarSummaryLine, { summary: summary }) : null }), children ? (_jsx("div", { className: "sw-dialog-footer-actions", children: children })) : null] }));
11
+ }
@@ -1,15 +1,22 @@
1
+ import { type ReactNode } from 'react';
1
2
  type DialogTitleProps = {
2
3
  id: string;
3
4
  level: 2 | 3;
4
5
  title: string;
6
+ /** A muted line under the title, which describes the dialog. */
7
+ description?: string;
8
+ descriptionId?: string;
9
+ /** A consumer glyph or tile before the title, hidden from assistive technology. */
10
+ icon?: ReactNode;
5
11
  /** The close button's accessible name; without it there is no button. */
6
12
  closeLabel?: string;
7
13
  onClose: () => void;
8
14
  };
9
15
  /**
10
- * The dialog's title, which names it through `aria-labelledby`. With a
11
- * `closeLabel` it shares a row with an icon-only ghost close button at its
12
- * end; without one it is the title alone, as it always was.
16
+ * The dialog's title, which names it through `aria-labelledby`. Alone it is
17
+ * the heading, as it always was. With an icon, a description or a
18
+ * `closeLabel` it is a row: the icon, the title over its description, and
19
+ * an icon-only ghost close button at the end.
13
20
  */
14
- export declare function DialogTitle({ closeLabel, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
21
+ export declare function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
15
22
  export {};
@@ -1,14 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
2
3
  import { Button } from '../Button.js';
3
4
  import { Text } from '../Text.js';
4
5
  /**
5
- * The dialog's title, which names it through `aria-labelledby`. With a
6
- * `closeLabel` it shares a row with an icon-only ghost close button at its
7
- * end; without one it is the title alone, as it always was.
6
+ * The dialog's title, which names it through `aria-labelledby`. Alone it is
7
+ * the heading, as it always was. With an icon, a description or a
8
+ * `closeLabel` it is a row: the icon, the title over its description, and
9
+ * an icon-only ghost close button at the end.
8
10
  */
9
- export function DialogTitle({ closeLabel, id, level, onClose, title, }) {
11
+ export function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
10
12
  const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
11
- if (!closeLabel)
13
+ if (!closeLabel && !icon && !description)
12
14
  return heading;
13
- return (_jsxs("div", { className: "sw-dialog-header", children: [heading, _jsxs(Button, { className: "sw-dialog-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] })] }));
15
+ return (_jsxs("div", { className: cx('sw-dialog-header', Boolean(icon || description) && 'sw-dialog-header-framed'), children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-dialog-icon", children: icon })) : null, description ? (_jsxs("div", { className: "sw-dialog-titles", children: [heading, _jsx(Text, { as: "p", className: "sw-dialog-description", color: "muted", id: descriptionId, variant: "caption", children: description })] })) : (heading), closeLabel ? (_jsxs(Button, { className: "sw-dialog-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] })) : null] }));
14
16
  }
@@ -0,0 +1,25 @@
1
+ import { type RefObject } from 'react';
2
+ import { type MultiSelectAlign, type MultiSelectOption } from './types.js';
3
+ type MultiSelectPanelProps = {
4
+ id: string;
5
+ label: string;
6
+ align: MultiSelectAlign;
7
+ options: readonly MultiSelectOption[];
8
+ checked: ReadonlySet<string>;
9
+ allLabel: string;
10
+ clearLabel: string;
11
+ triggerRef: RefObject<HTMLButtonElement | null>;
12
+ onToggle: (value: string, checked: boolean) => void;
13
+ onAll: () => void;
14
+ onClear: () => void;
15
+ /** Closes the panel, returning focus to the trigger when asked. */
16
+ onClose: (restoreFocus: boolean) => void;
17
+ };
18
+ /**
19
+ * The open panel: a group named by the label on the popover layer beside
20
+ * the trigger, with All and Clear over one checkbox per option. The first
21
+ * checkbox takes focus as it opens; Escape closes it and returns focus to
22
+ * the trigger; focus or a press moving outside it closes it.
23
+ */
24
+ export declare function MultiSelectPanel({ align, allLabel, checked, clearLabel, id, label, onAll, onClear, onClose, onToggle, options, triggerRef, }: MultiSelectPanelProps): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLayoutEffect, useRef, } from 'react';
4
+ import { Button } from '../Button.js';
5
+ import { CheckboxControl } from '../checkbox/CheckboxControl.js';
6
+ import { useAnchoredPopover } from '../use-anchored-popover.js';
7
+ /**
8
+ * The open panel: a group named by the label on the popover layer beside
9
+ * the trigger, with All and Clear over one checkbox per option. The first
10
+ * checkbox takes focus as it opens; Escape closes it and returns focus to
11
+ * the trigger; focus or a press moving outside it closes it.
12
+ */
13
+ export function MultiSelectPanel({ align, allLabel, checked, clearLabel, id, label, onAll, onClear, onClose, onToggle, options, triggerRef, }) {
14
+ const panelRef = useRef(null);
15
+ useAnchoredPopover(panelRef, triggerRef, triggerRef, () => onClose(false), {
16
+ align,
17
+ flipInline: true,
18
+ });
19
+ useLayoutEffect(() => {
20
+ panelRef.current
21
+ ?.querySelector('.sw-multi-select-option input')
22
+ ?.focus();
23
+ }, []);
24
+ function onKeyDown(event) {
25
+ if (event.key !== 'Escape')
26
+ return;
27
+ event.preventDefault();
28
+ onClose(true);
29
+ }
30
+ function onBlur(event) {
31
+ const next = event.relatedTarget;
32
+ if (!(next instanceof Node))
33
+ return;
34
+ if (panelRef.current?.contains(next) || triggerRef.current?.contains(next))
35
+ return;
36
+ onClose(false);
37
+ }
38
+ return (_jsxs("div", { "aria-label": label, className: "sw-multi-select-panel", id: id, onBlur: onBlur, onKeyDown: onKeyDown, ref: panelRef, role: "group", children: [_jsxs("div", { className: "sw-multi-select-commands", children: [_jsx(Button, { onPress: onAll, size: "sm", variant: "ghost", children: allLabel }), _jsx(Button, { onPress: onClear, size: "sm", variant: "ghost", children: clearLabel })] }), options.map((option) => (_jsx("div", { className: option.separatorBefore
39
+ ? 'sw-multi-select-row sw-multi-select-row-separated'
40
+ : 'sw-multi-select-row', children: _jsxs("label", { className: "sw-checkbox-label sw-multi-select-option", children: [_jsx(CheckboxControl, { checked: checked.has(option.value), onChange: (event) => onToggle(option.value, event.currentTarget.checked) }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-multi-select-icon", children: option.icon })) : null, _jsx("span", { className: "sw-multi-select-text", children: option.label })] }) }, option.value)))] }));
41
+ }
@@ -0,0 +1,35 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type InlineAlign } from '../../anchored-position.js';
3
+ /** One choice in a MultiSelect. */
4
+ export type MultiSelectOption = {
5
+ value: string;
6
+ label: string;
7
+ /** A consumer mark before the label, such as a `GlyphTile`; hidden from assistive technology. */
8
+ icon?: ReactNode;
9
+ /** Draws a hairline before this option, to set a group of options apart. */
10
+ separatorBefore?: boolean;
11
+ };
12
+ export type MultiSelectAlign = InlineAlign;
13
+ export type MultiSelectProps = {
14
+ /**
15
+ * What is chosen, such as "Show": the muted lead of the trigger and the
16
+ * name of the panel of checkboxes.
17
+ */
18
+ label: string;
19
+ options: readonly MultiSelectOption[];
20
+ /** The checked values; `onChange` reports them in the options' order. */
21
+ value: readonly string[];
22
+ onChange: (value: string[]) => void;
23
+ /** The command that checks every option, such as "All". */
24
+ allLabel: string;
25
+ /** The command that clears every option, such as "Clear". */
26
+ clearLabel: string;
27
+ /**
28
+ * The trigger's words after the label, naming what is chosen, such as
29
+ * "All entries" or "2 services". The consumer writes it.
30
+ */
31
+ summary: string;
32
+ disabled?: boolean;
33
+ /** The trigger edge the panel lines up with (default start). */
34
+ align?: MultiSelectAlign;
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type MultiSelectProps } from './types.js';
2
+ /** Fails closed on words or options a MultiSelect cannot show. */
3
+ export declare function assertMultiSelect({ allLabel, align, clearLabel, label, options, summary, value, }: Pick<MultiSelectProps, 'allLabel' | 'align' | 'clearLabel' | 'label' | 'options' | 'summary' | 'value'>): void;
@@ -0,0 +1,25 @@
1
+ function isBlank(text) {
2
+ return typeof text !== 'string' || !text.trim();
3
+ }
4
+ /** Fails closed on words or options a MultiSelect cannot show. */
5
+ export function assertMultiSelect({ allLabel, align, clearLabel, label, options, summary, value, }) {
6
+ if (isBlank(label) || isBlank(summary))
7
+ throw new RangeError('MultiSelect needs a label and a summary');
8
+ if (isBlank(allLabel) || isBlank(clearLabel))
9
+ throw new RangeError('MultiSelect needs an allLabel and a clearLabel');
10
+ if (options.length === 0)
11
+ throw new RangeError('MultiSelect needs at least one option');
12
+ const values = new Set();
13
+ for (const option of options) {
14
+ if (isBlank(option.value) || isBlank(option.label))
15
+ throw new RangeError('MultiSelect options need a value and a label');
16
+ if (values.has(option.value))
17
+ throw new RangeError(`Duplicate MultiSelect value: ${option.value}`);
18
+ values.add(option.value);
19
+ }
20
+ for (const checked of value)
21
+ if (!values.has(checked))
22
+ throw new RangeError(`Unknown MultiSelect value: ${checked}`);
23
+ if (align !== undefined && align !== 'start' && align !== 'end')
24
+ throw new RangeError(`Unknown MultiSelect align: ${String(align)}`);
25
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from 'react';
2
+ /** One press in the toast, such as "Open" for what was just saved. */
3
+ export type ToastAction = {
4
+ label: string;
5
+ onPress: () => void;
6
+ };
7
+ type ToastContentProps = {
8
+ title?: string;
9
+ icon?: ReactNode;
10
+ action?: ToastAction;
11
+ /** Runs the action, then closes the toast. */
12
+ onAction: () => void;
13
+ children: ReactNode;
14
+ };
15
+ /**
16
+ * What the toast says. Children alone render as they always have; with a
17
+ * title, an icon or an action they go in a row: the icon, the bold title
18
+ * over the children as a muted detail, and the action at the end.
19
+ */
20
+ export declare function ToastContent({ action, children, icon, onAction, title, }: ToastContentProps): import("react").JSX.Element;
21
+ export declare function assertToastContent(title: string | undefined, action: ToastAction | undefined): void;
22
+ export {};