@scalewing/react 1.7.0 → 1.9.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 (84) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +70 -3
  3. package/dist/anchored-position.d.ts +28 -0
  4. package/dist/anchored-position.js +17 -0
  5. package/dist/calendar-keys.d.ts +19 -0
  6. package/dist/calendar-keys.js +39 -0
  7. package/dist/calendar-labels.d.ts +12 -0
  8. package/dist/calendar-labels.js +32 -0
  9. package/dist/calendar-month.d.ts +36 -0
  10. package/dist/calendar-month.js +111 -0
  11. package/dist/calendar-years.d.ts +14 -0
  12. package/dist/calendar-years.js +20 -0
  13. package/dist/components/Accordion.d.ts +6 -1
  14. package/dist/components/Accordion.js +12 -3
  15. package/dist/components/ActionBar.d.ts +30 -0
  16. package/dist/components/ActionBar.js +13 -0
  17. package/dist/components/Box.d.ts +7 -3
  18. package/dist/components/Box.js +2 -2
  19. package/dist/components/DateField.d.ts +15 -1
  20. package/dist/components/DateField.js +58 -18
  21. package/dist/components/Field.d.ts +5 -1
  22. package/dist/components/Field.js +16 -4
  23. package/dist/components/FieldAdornment.d.ts +20 -0
  24. package/dist/components/FieldAdornment.js +34 -0
  25. package/dist/components/Grid.d.ts +4 -1
  26. package/dist/components/Grid.js +2 -2
  27. package/dist/components/Progress.d.ts +3 -1
  28. package/dist/components/Progress.js +2 -2
  29. package/dist/components/ScrollRegion.d.ts +15 -0
  30. package/dist/components/ScrollRegion.js +10 -0
  31. package/dist/components/Table.d.ts +1 -5
  32. package/dist/components/Table.js +3 -6
  33. package/dist/components/date-field/CalendarDialog.d.ts +26 -0
  34. package/dist/components/date-field/CalendarDialog.js +93 -0
  35. package/dist/components/date-field/CalendarGrid.d.ts +23 -0
  36. package/dist/components/date-field/CalendarGrid.js +36 -0
  37. package/dist/components/date-field/CalendarHeader.d.ts +18 -0
  38. package/dist/components/date-field/CalendarHeader.js +33 -0
  39. package/dist/components/date-field/DateFieldGlyph.d.ts +9 -0
  40. package/dist/components/date-field/DateFieldGlyph.js +15 -0
  41. package/dist/components/date-field/use-anchored-popover.d.ts +8 -0
  42. package/dist/components/date-field/use-anchored-popover.js +61 -0
  43. package/dist/components/date-field/use-date-entry.d.ts +20 -0
  44. package/dist/components/date-field/use-date-entry.js +61 -0
  45. package/dist/components/date-field/use-lang-locale.d.ts +7 -0
  46. package/dist/components/date-field/use-lang-locale.js +17 -0
  47. package/dist/components/denomination-grid/DenominationStrip.js +5 -4
  48. package/dist/css/css-accordion.js +55 -8
  49. package/dist/css/css-action-bar.d.ts +4 -0
  50. package/dist/css/css-action-bar.js +99 -0
  51. package/dist/css/css-action-menu.js +2 -1
  52. package/dist/css/css-box-border.d.ts +5 -0
  53. package/dist/css/css-box-border.js +24 -0
  54. package/dist/css/css-chrome.js +2 -1
  55. package/dist/css/css-components.js +39 -26
  56. package/dist/css/css-data.js +4 -10
  57. package/dist/css/css-date-field.js +241 -11
  58. package/dist/css/css-denomination-grid.js +49 -5
  59. package/dist/css/css-document.d.ts +2 -0
  60. package/dist/css/css-document.js +2 -1
  61. package/dist/css/css-field.js +61 -1
  62. package/dist/css/css-scroll-region.d.ts +8 -0
  63. package/dist/css/css-scroll-region.js +18 -0
  64. package/dist/css/css-select.js +2 -1
  65. package/dist/css/css-tooltip.js +2 -1
  66. package/dist/css/stacking.d.ts +10 -0
  67. package/dist/css/stacking.js +47 -0
  68. package/dist/css/stylesheet.js +4 -0
  69. package/dist/date-entry-validity.d.ts +10 -0
  70. package/dist/date-entry-validity.js +16 -0
  71. package/dist/date-entry.d.ts +32 -0
  72. package/dist/date-entry.js +106 -0
  73. package/dist/date-field-labels.d.ts +38 -0
  74. package/dist/date-field-labels.js +22 -0
  75. package/dist/date-field-locale.d.ts +8 -0
  76. package/dist/date-field-locale.js +27 -0
  77. package/dist/date-formatters.d.ts +13 -0
  78. package/dist/date-formatters.js +28 -0
  79. package/dist/date-only.d.ts +23 -0
  80. package/dist/date-only.js +63 -9
  81. package/dist/index.d.ts +5 -3
  82. package/dist/index.js +3 -2
  83. package/dist/styles.css +461 -45
  84. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { calendarKeyTarget } from '../../calendar-keys.js';
4
+ import { formatDateLabel, weekdayLabels } from '../../calendar-labels.js';
5
+ import { monthGrid, } from '../../calendar-month.js';
6
+ import { isOutsideDateRange } from '../../date-only.js';
7
+ /**
8
+ * The month as a WAI-ARIA grid with one roving tab stop. Days outside
9
+ * `[min, max]` stay focusable for orientation but are `aria-disabled`;
10
+ * padding past the four-digit years is blank and inert.
11
+ */
12
+ export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, selected, titleId, today, weekStartsOn, }) {
13
+ function choose(value) {
14
+ if (!isOutsideDateRange(value, min, max))
15
+ onSelect(value);
16
+ }
17
+ function onKeyDown(event) {
18
+ if (event.key === 'Enter' || event.key === ' ') {
19
+ event.preventDefault();
20
+ choose(focusDate);
21
+ return;
22
+ }
23
+ const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
24
+ const target = calendarKeyTarget({ key: event.key, shiftKey: event.shiftKey }, focusDate, { weekStartsOn, min, max, rtl });
25
+ if (target === null)
26
+ return;
27
+ event.preventDefault();
28
+ onMoveFocus(target);
29
+ }
30
+ return (_jsxs("table", { "aria-labelledby": titleId, className: "sw-date-field-grid", onKeyDown: onKeyDown, role: "grid", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels(locale, weekStartsOn).map((weekday) => (_jsx("th", { abbr: weekday.long, scope: "col", children: weekday.short }, weekday.long))) }) }), _jsx("tbody", { children: monthGrid(month, weekStartsOn).map((row, rowIndex) => (_jsx("tr", { children: row.map((cell, column) => cell === null ? (_jsx("td", { "aria-disabled": "true", className: "sw-date-field-day sw-date-field-day-outside", role: "gridcell" }, `blank-${column}`)) : (_jsx("td", { "aria-current": cell.value === today ? 'date' : undefined, "aria-disabled": isOutsideDateRange(cell.value, min, max) || undefined, "aria-label": formatDateLabel(cell.value, locale), "aria-selected": cell.value === selected, className: cx('sw-date-field-day', !cell.inMonth && 'sw-date-field-day-outside'), "data-date": cell.value, onClick: () => choose(cell.value), onFocus: () => {
31
+ // A press on a neighboring month's day selects it; moving
32
+ // the grid first would pull the day out from under it.
33
+ if (cell.inMonth && cell.value !== focusDate)
34
+ onFocusDate(cell.value);
35
+ }, role: "gridcell", tabIndex: cell.value === focusDate ? 0 : -1, children: cell.day }, cell.value))) }, rowIndex))) })] }));
36
+ }
@@ -0,0 +1,18 @@
1
+ import { type CalendarMonth } from '../../calendar-month.js';
2
+ import { type DateFieldLabels } from '../../date-field-labels.js';
3
+ export type CalendarHeaderProps = {
4
+ labels: DateFieldLabels;
5
+ locale: string;
6
+ max?: string;
7
+ min?: string;
8
+ month: CalendarMonth;
9
+ onMonthChange: (month: CalendarMonth) => void;
10
+ titleId: string;
11
+ todayYear: number;
12
+ };
13
+ /**
14
+ * Month stepping plus month and year selectors for jumping far, both cut to
15
+ * `min` and `max`, over a visually hidden title that announces the shown
16
+ * month when it changes.
17
+ */
18
+ export declare function CalendarHeader({ labels, locale, max, min, month, onMonthChange, titleId, todayYear, }: CalendarHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { monthNames, monthTitle } from '../../calendar-labels.js';
3
+ import { monthInRange, monthsInRange, shiftMonth, } from '../../calendar-month.js';
4
+ import { calendarYearRange, yearsIn } from '../../calendar-years.js';
5
+ import { Select } from '../Select.js';
6
+ import { DateFieldGlyph } from './DateFieldGlyph.js';
7
+ function MonthStep({ delta, label, max, min, month, onMonthChange, }) {
8
+ const target = shiftMonth(month, delta);
9
+ const unavailable = target.year < 1 || target.year > 9999 || !monthInRange(target, min, max);
10
+ return (_jsx("button", { "aria-disabled": unavailable || undefined, "aria-label": label, className: "sw-date-field-step", onClick: () => {
11
+ if (!unavailable)
12
+ onMonthChange(target);
13
+ }, type: "button", children: _jsx(DateFieldGlyph, { name: delta < 0 ? 'previous' : 'next' }) }));
14
+ }
15
+ /**
16
+ * Month stepping plus month and year selectors for jumping far, both cut to
17
+ * `min` and `max`, over a visually hidden title that announces the shown
18
+ * month when it changes.
19
+ */
20
+ export function CalendarHeader({ labels, locale, max, min, month, onMonthChange, titleId, todayYear, }) {
21
+ const range = calendarYearRange(month.year, todayYear, min, max);
22
+ const names = monthNames(locale);
23
+ const stepProps = { max, min, month, onMonthChange };
24
+ return (_jsxs("div", { className: "sw-date-field-header", children: [_jsx("h2", { "aria-live": "polite", className: "sw-sr-only", id: titleId, children: monthTitle(month, locale) }), _jsx(MonthStep, { ...stepProps, delta: -1, label: labels.previousMonth }), _jsxs("div", { className: "sw-date-field-jump", children: [_jsx(Select, { label: labels.month, labelVisuallyHidden: true, onChange: (next) => onMonthChange({ ...month, month: Number(next) }),
25
+ // Only months with a day inside the bounds; the shown one always is.
26
+ options: monthsInRange(month.year, min, max).map((number) => ({
27
+ value: String(number),
28
+ label: names[number - 1] ?? String(number),
29
+ })), size: "xs", value: String(month.month) }), _jsx(Select, { label: labels.year, labelVisuallyHidden: true, onChange: (next) => onMonthChange({ ...month, year: Number(next) }), options: yearsIn(range).map((year) => ({
30
+ value: String(year),
31
+ label: String(year),
32
+ })), size: "xs", value: String(month.year) })] }), _jsx(MonthStep, { ...stepProps, delta: 1, label: labels.nextMonth })] }));
33
+ }
@@ -0,0 +1,9 @@
1
+ export type DateFieldGlyphName = 'calendar' | 'previous' | 'next';
2
+ /**
3
+ * Private control chrome for DateField. Stroke, size, and color come from
4
+ * the generated `sw-date-field-glyph` rule; the chevrons mirror in
5
+ * right-to-left text.
6
+ */
7
+ export declare function DateFieldGlyph({ name }: {
8
+ name: DateFieldGlyphName;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ const PATHS = {
4
+ calendar: 'M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5zM4 10h16M8.5 2.5v3M15.5 2.5v3',
5
+ previous: 'M14.5 6l-6 6 6 6',
6
+ next: 'M9.5 6l6 6-6 6',
7
+ };
8
+ /**
9
+ * Private control chrome for DateField. Stroke, size, and color come from
10
+ * the generated `sw-date-field-glyph` rule; the chevrons mirror in
11
+ * right-to-left text.
12
+ */
13
+ export function DateFieldGlyph({ name }) {
14
+ return (_jsx("svg", { "aria-hidden": "true", className: cx('sw-date-field-glyph', name !== 'calendar' && 'sw-date-field-glyph-directional'), focusable: "false", viewBox: "0 0 24 24", children: _jsx("path", { d: PATHS[name] }) }));
15
+ }
@@ -0,0 +1,8 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * Shows a mounted element on the top layer as a manual popover, keeps it
4
+ * beside `anchorRef` through scroll and resize, and calls `onDismiss` for a
5
+ * press outside it and outside `ignoreRef` (the button that toggles it).
6
+ * Browsers without the popover API get the same fixed placement.
7
+ */
8
+ export declare function useAnchoredPopover(popoverRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>, ignoreRef: RefObject<HTMLElement | null>, onDismiss: () => void): void;
@@ -0,0 +1,61 @@
1
+ import { spacingScale } from '@scalewing/tokens';
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ import { anchoredPosition } from '../../anchored-position.js';
4
+ function placePopover(node, anchor) {
5
+ const position = anchoredPosition(anchor.getBoundingClientRect(), { width: node.offsetWidth, height: node.offsetHeight }, { width: window.innerWidth, height: window.innerHeight }, {
6
+ gap: spacingScale[1],
7
+ inset: spacingScale[2],
8
+ rtl: getComputedStyle(anchor).direction === 'rtl',
9
+ });
10
+ node.style.left = `${position.left}px`;
11
+ node.style.top = `${position.top}px`;
12
+ }
13
+ /**
14
+ * Shows a mounted element on the top layer as a manual popover, keeps it
15
+ * beside `anchorRef` through scroll and resize, and calls `onDismiss` for a
16
+ * press outside it and outside `ignoreRef` (the button that toggles it).
17
+ * Browsers without the popover API get the same fixed placement.
18
+ */
19
+ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss) {
20
+ const dismissRef = useRef(onDismiss);
21
+ useLayoutEffect(() => {
22
+ dismissRef.current = onDismiss;
23
+ });
24
+ useLayoutEffect(() => {
25
+ const node = popoverRef.current;
26
+ const anchor = anchorRef.current;
27
+ if (!node || !anchor)
28
+ return;
29
+ const supportsPopover = typeof node.showPopover === 'function';
30
+ // Set only where supported: an unsupported runtime would hide a
31
+ // `[popover]` element that it can never show.
32
+ if (supportsPopover) {
33
+ node.setAttribute('popover', 'manual');
34
+ if (!node.matches(':popover-open'))
35
+ node.showPopover();
36
+ }
37
+ placePopover(node, anchor);
38
+ function onPointerDown(event) {
39
+ const target = event.target;
40
+ if (!(target instanceof Node))
41
+ return;
42
+ if (node?.contains(target) || ignoreRef.current?.contains(target))
43
+ return;
44
+ dismissRef.current();
45
+ }
46
+ function onMove() {
47
+ if (node && anchor)
48
+ placePopover(node, anchor);
49
+ }
50
+ document.addEventListener('pointerdown', onPointerDown);
51
+ window.addEventListener('resize', onMove);
52
+ window.addEventListener('scroll', onMove, true);
53
+ return () => {
54
+ document.removeEventListener('pointerdown', onPointerDown);
55
+ window.removeEventListener('resize', onMove);
56
+ window.removeEventListener('scroll', onMove, true);
57
+ if (supportsPopover && node.matches(':popover-open'))
58
+ node.hidePopover();
59
+ };
60
+ }, [anchorRef, ignoreRef, popoverRef]);
61
+ }
@@ -0,0 +1,20 @@
1
+ import { type DateEntryPattern } from '../../date-entry.js';
2
+ export type DateEntry = {
3
+ text: string;
4
+ /** The date the text forms: YYYY-MM-DD, '' when blank, null when not one. */
5
+ parsed: string | null;
6
+ /** Typed text that is not a date, after the person left the field. */
7
+ invalid: boolean;
8
+ change: (text: string) => void;
9
+ check: () => void;
10
+ reset: () => void;
11
+ };
12
+ /**
13
+ * Typed entry over a controlled date-only value. A keystroke that finishes
14
+ * a date (see `isCompleteDateEntry`) or blanks the text calls `onCommit`;
15
+ * a date whose last field may still grow commits on `check` (blur or
16
+ * Enter). Text that is not (yet) a date stays on screen and leaves the value
17
+ * alone. The text also stays when the parent keeps the old value; when
18
+ * `value` changes from outside to any other date, the text follows it.
19
+ */
20
+ export declare function useDateEntry(value: string, pattern: DateEntryPattern, onCommit: (value: string) => void): DateEntry;
@@ -0,0 +1,61 @@
1
+ import { useState } from 'react';
2
+ import { formatDateEntry, isCompleteDateEntry, parseDateEntry, } from '../../date-entry.js';
3
+ /**
4
+ * Typed entry over a controlled date-only value. A keystroke that finishes
5
+ * a date (see `isCompleteDateEntry`) or blanks the text calls `onCommit`;
6
+ * a date whose last field may still grow commits on `check` (blur or
7
+ * Enter). Text that is not (yet) a date stays on screen and leaves the value
8
+ * alone. The text also stays when the parent keeps the old value; when
9
+ * `value` changes from outside to any other date, the text follows it.
10
+ */
11
+ export function useDateEntry(value, pattern, onCommit) {
12
+ const [draft, setDraft] = useState(null);
13
+ const current = draft !== null && (draft.value === value || draft.committed === value)
14
+ ? draft
15
+ : null;
16
+ // The parent adopted the commit: tie the text to it, so a later outside
17
+ // change back to the old value replaces the text.
18
+ if (current !== null && current.value !== value)
19
+ setDraft({ ...current, value, committed: null });
20
+ const text = current ? current.text : formatDateEntry(value, pattern);
21
+ const parsed = parseDateEntry(text, pattern);
22
+ function change(next) {
23
+ const nextValue = parseDateEntry(next, pattern);
24
+ const ready = nextValue === '' ||
25
+ (nextValue !== null && isCompleteDateEntry(next, pattern));
26
+ const committed = ready && nextValue !== value ? nextValue : null;
27
+ setDraft({ text: next, value, committed, checked: false });
28
+ if (committed !== null)
29
+ onCommit(committed);
30
+ }
31
+ function check() {
32
+ if (current === null)
33
+ return;
34
+ if (parsed === null) {
35
+ setDraft({ ...current, checked: true });
36
+ return;
37
+ }
38
+ if (parsed === value) {
39
+ // Show the value in the locale's padded form.
40
+ setDraft(null);
41
+ return;
42
+ }
43
+ // Commit once; a parent that keeps the old value keeps the padded text.
44
+ if (parsed !== current.committed)
45
+ onCommit(parsed);
46
+ setDraft({
47
+ text: formatDateEntry(parsed, pattern),
48
+ value,
49
+ committed: parsed,
50
+ checked: false,
51
+ });
52
+ }
53
+ return {
54
+ text,
55
+ parsed,
56
+ invalid: current !== null && current.checked && parsed === null,
57
+ change,
58
+ check,
59
+ reset: () => setDraft(null),
60
+ };
61
+ }
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * The `locale` prop when given; otherwise the `lang` of the nearest ancestor
4
+ * of `ref`, read when the field mounts, then en-US. A product that switches
5
+ * language without remounting passes `locale`.
6
+ */
7
+ export declare function useLangLocale(locale: string | undefined, ref: RefObject<HTMLElement | null>): string;
@@ -0,0 +1,17 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import { resolveDateLocale } from '../../date-field-locale.js';
3
+ /**
4
+ * The `locale` prop when given; otherwise the `lang` of the nearest ancestor
5
+ * of `ref`, read when the field mounts, then en-US. A product that switches
6
+ * language without remounting passes `locale`.
7
+ */
8
+ export function useLangLocale(locale, ref) {
9
+ const [lang, setLang] = useState(null);
10
+ useLayoutEffect(() => {
11
+ if (locale !== undefined)
12
+ return;
13
+ const nearest = ref.current?.closest('[lang]')?.getAttribute('lang');
14
+ setLang(nearest ?? null);
15
+ }, [locale, ref]);
16
+ return resolveDateLocale(locale, lang);
17
+ }
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellView } from '../../denomination-cells.js';
4
+ import { ScrollRegion } from '../ScrollRegion.js';
4
5
  export function DenominationStrip({ label, columns, rows, zeroLabel, }) {
5
6
  const hasTotals = rows.some((row) => row.total !== undefined);
6
- return (_jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", 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: column.label }, column.key))), hasTotals ? _jsx("td", { className: "sw-denomination-corner" }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsxs("th", { className: "sw-denomination-label", scope: "row", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }), columns.map((column, index) => {
7
- const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
8
- return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
9
- }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }));
7
+ return (_jsx(ScrollRegion, { "aria-label": label, className: "sw-denomination-scroll", children: _jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", 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: column.label }, column.key))), hasTotals ? _jsx("td", { className: "sw-denomination-corner" }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
8
+ const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
9
+ return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
10
+ }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }) }));
10
11
  }
@@ -1,3 +1,33 @@
1
+ function markerRules() {
2
+ return `/* A token chevron replaces the user-agent triangle: it points to the inline end when closed and down when open. */
3
+ .sw-accordion-summary::-webkit-details-marker { display: none; }
4
+
5
+ .sw-accordion-marker {
6
+ border-bottom: 2px solid var(--sw-color-muted);
7
+ border-right: 2px solid var(--sw-color-muted);
8
+ box-sizing: border-box;
9
+ flex: none;
10
+ height: var(--sw-space-2);
11
+ margin-inline-end: var(--sw-space-1);
12
+ transform: rotate(-45deg);
13
+ transition: transform var(--sw-motion-default) var(--sw-motion-easing);
14
+ width: var(--sw-space-2);
15
+ }
16
+
17
+ .sw-accordion-marker:dir(rtl) { transform: rotate(135deg); }
18
+
19
+ .sw-accordion[open] > .sw-accordion-summary .sw-accordion-marker {
20
+ transform: rotate(45deg);
21
+ }
22
+
23
+ @media (prefers-reduced-motion: reduce) {
24
+ .sw-accordion-marker { transition: none; }
25
+ }
26
+
27
+ @media (forced-colors: active) {
28
+ .sw-accordion-marker { border-color: CanvasText; }
29
+ }`;
30
+ }
1
31
  export function cssAccordionClasses() {
2
32
  return `.sw-accordion {
3
33
  background: var(--sw-glass-fill);
@@ -9,25 +39,42 @@ export function cssAccordionClasses() {
9
39
  color: var(--sw-color-text);
10
40
  }
11
41
 
42
+ /* A nested disclosure sits inside a larger surface, so its corners are smaller. */
43
+ .sw-accordion-sm { border-radius: var(--sw-radius-md); }
44
+
12
45
  .sw-accordion-summary {
46
+ align-items: center;
47
+ box-sizing: border-box;
13
48
  cursor: pointer;
14
- display: list-item;
49
+ display: flex;
50
+ gap: var(--sw-space-3);
51
+ list-style: none;
15
52
  min-height: var(--sw-control-md-min-height);
16
53
  }
17
54
 
18
- .sw-accordion-summary::-webkit-details-marker {
19
- color: var(--sw-color-muted);
20
- }
55
+ /* A nested header is an sm control, still a comfortable pointer and touch target. */
56
+ .sw-accordion-sm > .sw-accordion-summary { min-height: var(--sw-control-sm-min-height); }
21
57
 
22
- .sw-accordion-summary::marker {
23
- color: var(--sw-color-muted);
58
+ .sw-accordion-heading {
59
+ display: flex;
60
+ flex: 1 1 auto;
61
+ flex-direction: column;
62
+ min-width: 0;
24
63
  }
25
64
 
26
65
  .sw-accordion-summary:focus-visible {
27
66
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
28
67
  outline-offset: var(--sw-focus-ring-offset);
29
- }`;
68
+ }
69
+
70
+ ${markerRules()}`;
30
71
  }
31
72
  export function accordionClassCatalog() {
32
- return ['sw-accordion', 'sw-accordion-summary'];
73
+ return [
74
+ 'sw-accordion',
75
+ 'sw-accordion-sm',
76
+ 'sw-accordion-summary',
77
+ 'sw-accordion-heading',
78
+ 'sw-accordion-marker',
79
+ ];
33
80
  }
@@ -0,0 +1,4 @@
1
+ import { type Breakpoint } from './breakpoints.js';
2
+ export declare function actionBarStickyClass(stickyBelow: Breakpoint | undefined): string;
3
+ export declare function cssActionBarClasses(): string;
4
+ export declare function actionBarClassCatalog(): string[];
@@ -0,0 +1,99 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery, breakpoints, } from './breakpoints.js';
3
+ import { zIndex } from './stacking.js';
4
+ const caption = typographyVariants.caption;
5
+ /* The bar floats a step above the bottom edge and clears the home indicator. */
6
+ function stuckToBottom(indent) {
7
+ return [
8
+ 'bottom: calc(var(--sw-space-2) + env(safe-area-inset-bottom, 0px));',
9
+ 'position: sticky;',
10
+ zIndex('bottomChrome'),
11
+ ]
12
+ .map((declaration) => `${indent}${declaration}`)
13
+ .join('\n');
14
+ }
15
+ export function actionBarStickyClass(stickyBelow) {
16
+ return stickyBelow
17
+ ? `sw-action-bar-sticky-below-${stickyBelow}`
18
+ : 'sw-action-bar-sticky';
19
+ }
20
+ function stickyRules() {
21
+ const below = breakpoints
22
+ .map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
23
+ .${actionBarStickyClass(breakpoint)} {
24
+ ${stuckToBottom(' ')}
25
+ }
26
+ }`)
27
+ .join('\n\n');
28
+ return `.sw-action-bar-sticky {
29
+ ${stuckToBottom(' ')}
30
+ }
31
+
32
+ ${below}`;
33
+ }
34
+ export function cssActionBarClasses() {
35
+ return `.sw-action-bar {
36
+ align-items: center;
37
+ background: var(--sw-glass-fill);
38
+ border: 1px solid var(--sw-glass-border);
39
+ border-radius: var(--sw-radius-lg);
40
+ box-shadow: inset 0 1px 0 var(--sw-glass-specular);
41
+ backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
42
+ -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
43
+ box-sizing: border-box;
44
+ color: var(--sw-color-text);
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ gap: var(--sw-space-2) var(--sw-space-4);
48
+ padding-block: var(--sw-space-3);
49
+ padding-left: max(var(--sw-space-4), env(safe-area-inset-left, 0px));
50
+ padding-right: max(var(--sw-space-4), env(safe-area-inset-right, 0px));
51
+ }
52
+
53
+ .sw-action-bar-status {
54
+ color: var(--sw-color-muted);
55
+ flex: 1 1 auto;
56
+ font-family: var(--sw-font-sans);
57
+ font-size: ${caption.fontSize}px;
58
+ line-height: ${caption.lineHeight}px;
59
+ margin: 0;
60
+ min-width: 0;
61
+ }
62
+
63
+ /* With no status the live region stays in the page, so a first status is
64
+ still announced, but it takes no room in the bar. */
65
+ .sw-action-bar-status:empty {
66
+ clip-path: inset(50%);
67
+ height: 1px;
68
+ overflow: hidden;
69
+ position: absolute;
70
+ width: 1px;
71
+ }
72
+
73
+ .sw-action-bar-actions {
74
+ align-items: center;
75
+ display: flex;
76
+ flex: 0 1 auto;
77
+ flex-wrap: wrap;
78
+ gap: var(--sw-space-2);
79
+ justify-content: flex-end;
80
+ margin-inline-start: auto;
81
+ }
82
+
83
+ @media ${breakpointQuery('below', 'md')} {
84
+ .sw-action-bar-status,
85
+ .sw-action-bar-actions { flex-basis: 100%; }
86
+ .sw-action-bar-actions > * { flex: 1 1 auto; }
87
+ }
88
+
89
+ ${stickyRules()}`;
90
+ }
91
+ export function actionBarClassCatalog() {
92
+ return [
93
+ 'sw-action-bar',
94
+ 'sw-action-bar-status',
95
+ 'sw-action-bar-actions',
96
+ 'sw-action-bar-sticky',
97
+ ...breakpoints.map((breakpoint) => actionBarStickyClass(breakpoint)),
98
+ ];
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { zIndex } from './stacking.js';
2
3
  const label = typographyVariants.label;
3
4
  export function cssActionMenuClasses() {
4
5
  return `.sw-action-menu {
@@ -45,7 +46,7 @@ export function cssActionMenuClasses() {
45
46
  overflow: auto;
46
47
  padding: var(--sw-space-1);
47
48
  position: fixed;
48
- z-index: 10;
49
+ ${zIndex('popup')}
49
50
  }
50
51
 
51
52
  .sw-action-menu-list[hidden] {
@@ -0,0 +1,5 @@
1
+ export type BoxBorderValue = boolean | 'dashed';
2
+ /** The generated class for a Box `border` prop, or none for `false`. */
3
+ export declare function boxBorderClass(border: BoxBorderValue | undefined): string | undefined;
4
+ export declare function cssBoxBorderClasses(): string;
5
+ export declare function boxBorderClassCatalog(): string[];
@@ -0,0 +1,24 @@
1
+ const borderStyles = ['solid', 'dashed'];
2
+ function borderClass(style) {
3
+ return style === 'solid' ? 'sw-border' : `sw-border-${style}`;
4
+ }
5
+ /** The generated class for a Box `border` prop, or none for `false`. */
6
+ export function boxBorderClass(border) {
7
+ if (!border)
8
+ return undefined;
9
+ return borderClass(border === 'dashed' ? 'dashed' : 'solid');
10
+ }
11
+ /*
12
+ * A Box `border` is a token hairline. These rules are emitted after every
13
+ * component rule so that, at equal specificity, the prop still wins over a
14
+ * Box-based component's own frame. A consumer `style` wins over both. In
15
+ * forced colors the system border color replaces the token and the style stays.
16
+ */
17
+ export function cssBoxBorderClasses() {
18
+ return borderStyles
19
+ .map((style) => `.${borderClass(style)} { border: 1px ${style} var(--sw-color-border); }`)
20
+ .join('\n');
21
+ }
22
+ export function boxBorderClassCatalog() {
23
+ return borderStyles.map(borderClass);
24
+ }
@@ -1,4 +1,5 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { zIndex } from './stacking.js';
2
3
  const caption = typographyVariants.caption;
3
4
  const label = typographyVariants.label;
4
5
  export function cssChromeClasses() {
@@ -14,7 +15,7 @@ export function cssChromeClasses() {
14
15
  .sw-app-header-sticky {
15
16
  position: sticky;
16
17
  top: 0;
17
- z-index: 2;
18
+ ${zIndex('topChrome')}
18
19
  }
19
20
 
20
21
  .sw-nav {