@scalewing/react 1.36.0 → 1.38.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 (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +41 -3
  3. package/dist/accessible-name.d.ts +13 -0
  4. package/dist/accessible-name.js +1 -0
  5. package/dist/components/DataBar.d.ts +48 -0
  6. package/dist/components/DataBar.js +28 -0
  7. package/dist/components/DenominationGrid.d.ts +1 -1
  8. package/dist/components/DenominationGrid.js +2 -0
  9. package/dist/components/Pagination.d.ts +34 -0
  10. package/dist/components/Pagination.js +27 -0
  11. package/dist/components/ScrollRegion.d.ts +9 -5
  12. package/dist/components/ScrollRegion.js +8 -6
  13. package/dist/components/SectionNav.d.ts +2 -8
  14. package/dist/components/SegmentedControl.d.ts +2 -8
  15. package/dist/components/Stepper.d.ts +5 -0
  16. package/dist/components/Stepper.js +4 -2
  17. package/dist/components/Table.d.ts +48 -4
  18. package/dist/components/Table.js +9 -3
  19. package/dist/components/Tabs.d.ts +2 -9
  20. package/dist/components/denomination-grid/DenominationStrip.js +3 -1
  21. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  22. package/dist/components/denomination-grid/EntryCell.js +2 -2
  23. package/dist/components/denomination-grid/StripRow.d.ts +8 -2
  24. package/dist/components/denomination-grid/StripRow.js +14 -5
  25. package/dist/components/denomination-grid/StripTotal.d.ts +11 -0
  26. package/dist/components/denomination-grid/StripTotal.js +12 -0
  27. package/dist/components/denomination-grid/StripValueRow.d.ts +25 -0
  28. package/dist/components/denomination-grid/StripValueRow.js +27 -0
  29. package/dist/components/denomination-grid/types.d.ts +19 -0
  30. package/dist/components/denomination-grid/validate-value-row.d.ts +6 -0
  31. package/dist/components/denomination-grid/validate-value-row.js +30 -0
  32. package/dist/components/table/TableRow.d.ts +8 -2
  33. package/dist/components/table/TableRow.js +1 -1
  34. package/dist/components/table/table-max-height.d.ts +20 -0
  35. package/dist/components/table/table-max-height.js +23 -0
  36. package/dist/components/use-scroll-overflow.d.ts +10 -8
  37. package/dist/components/use-scroll-overflow.js +23 -10
  38. package/dist/css/chevron.d.ts +6 -3
  39. package/dist/css/chevron.js +9 -4
  40. package/dist/css/css-components.js +6 -0
  41. package/dist/css/css-data-bar.d.ts +16 -0
  42. package/dist/css/css-data-bar.js +107 -0
  43. package/dist/css/css-denomination-grid.js +4 -0
  44. package/dist/css/css-denomination-value-row.d.ts +3 -0
  45. package/dist/css/css-denomination-value-row.js +54 -0
  46. package/dist/css/css-pagination.d.ts +2 -0
  47. package/dist/css/css-pagination.js +63 -0
  48. package/dist/css/css-scroll-region.d.ts +1 -0
  49. package/dist/css/css-scroll-region.js +10 -5
  50. package/dist/css/css-table-bounded-edges.d.ts +1 -0
  51. package/dist/css/css-table-bounded-edges.js +132 -0
  52. package/dist/css/css-table-bounded.d.ts +2 -0
  53. package/dist/css/css-table-bounded.js +73 -0
  54. package/dist/css/css-table-ledger.d.ts +5 -2
  55. package/dist/css/css-table-ledger.js +5 -2
  56. package/dist/css/css-table-metrics.d.ts +14 -0
  57. package/dist/css/css-table-metrics.js +13 -0
  58. package/dist/css/css-table-sort.js +2 -1
  59. package/dist/css/css-table-total-row.d.ts +2 -0
  60. package/dist/css/css-table-total-row.js +29 -0
  61. package/dist/css/css-table.js +14 -3
  62. package/dist/css/stylesheet.js +4 -0
  63. package/dist/data-bar-geometry.d.ts +24 -0
  64. package/dist/data-bar-geometry.js +41 -0
  65. package/dist/index.d.ts +4 -2
  66. package/dist/index.js +2 -0
  67. package/dist/pagination-pages.d.ts +13 -0
  68. package/dist/pagination-pages.js +22 -0
  69. package/dist/scroll-overflow.d.ts +12 -0
  70. package/dist/scroll-overflow.js +31 -7
  71. package/dist/styles.css +258 -0
  72. package/package.json +1 -1
@@ -3,15 +3,21 @@ import { forwardRef } from 'react';
3
3
  import { cx } from '../class-names.js';
4
4
  import { ScrollRegion } from './ScrollRegion.js';
5
5
  import { assertTableLayout, assertTableStickyHeader, assertTableVerticalAlign, } from './table/table-layout.js';
6
+ import { assertTableMaxHeight, tableMaxHeightStyle, } from './table/table-max-height.js';
6
7
  export { TableCell, } from './table/TableCell.js';
7
8
  export { TableRow, } from './table/TableRow.js';
8
9
  export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
9
- /** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
10
- export const Table = forwardRef(function Table({ children, className, columnRules = false, density = 'comfortable', layout = 'auto', stickyHeader = true, verticalAlign = 'middle', ...rest }, ref) {
10
+ /**
11
+ * A wide table scrolls inside its own `ScrollRegion`, named after the
12
+ * table; a bounded one (`maxHeight`) scrolls its rows in it too.
13
+ */
14
+ export const Table = forwardRef(function Table({ children, className, columnRules = false, density = 'comfortable', layout = 'auto', maxHeight, stickyHeader = true, verticalAlign = 'middle', ...rest }, ref) {
11
15
  assertTableLayout(layout);
12
16
  assertTableStickyHeader(stickyHeader);
13
17
  assertTableVerticalAlign(verticalAlign);
14
- return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", markFits: true, children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', layout === 'fixed' && 'sw-table-fixed', stickyHeader !== false && 'sw-table-sticky', stickyHeader === 'page' && 'sw-table-sticky-page', verticalAlign === 'top' && 'sw-table-top', columnRules && 'sw-table-column-rules', className), ...rest, children: children }) }));
18
+ assertTableMaxHeight(maxHeight, stickyHeader);
19
+ const bounded = maxHeight !== undefined;
20
+ return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: cx('sw-table-wrap', bounded && 'sw-table-bounded'), markFits: !bounded, style: tableMaxHeightStyle(maxHeight), children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', layout === 'fixed' && 'sw-table-fixed', stickyHeader !== false && 'sw-table-sticky', stickyHeader === 'page' && 'sw-table-sticky-page', verticalAlign === 'top' && 'sw-table-top', columnRules && 'sw-table-column-rules', className), ...rest, children: children }) }));
15
21
  });
16
22
  export const TableHeader = forwardRef(function TableHeader({ className, ...rest }, ref) {
17
23
  return _jsx("thead", { ref: ref, className: className, ...rest });
@@ -1,16 +1,10 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type AccessibleName } from '../accessible-name.js';
2
3
  export type TabItem = {
3
4
  id: string;
4
5
  label: ReactNode;
5
6
  };
6
- type TabsLabel = {
7
- 'aria-label': string;
8
- 'aria-labelledby'?: never;
9
- } | {
10
- 'aria-label'?: never;
11
- 'aria-labelledby': string;
12
- };
13
- export type TabsProps = TabsLabel & {
7
+ export type TabsProps = AccessibleName & {
14
8
  /** Prefix for the tab and panel ids, so a `TabPanel` can point back at its tab. */
15
9
  id: string;
16
10
  items: readonly TabItem[];
@@ -58,4 +52,3 @@ export type TabPanelProps = Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
58
52
  };
59
53
  /** The content of one tab, labelled by its tab and hidden while another tab is current. */
60
54
  export declare function TabPanel({ children, className, id, tabsId, value, ...rest }: TabPanelProps): import("react").JSX.Element;
61
- export {};
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
2
3
  import { cx } from '../../class-names.js';
3
4
  import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
4
5
  import { denominationFramedClass, denominationScrollFramedClass, } from '../../css/css-denomination-appearance.js';
@@ -18,6 +19,7 @@ function TotalHead({ totalLabel }) {
18
19
  return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
19
20
  }
20
21
  export function DenominationStrip({ className, framed, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
22
+ const idPrefix = useId();
21
23
  const hasTotals = rows.some((row) => row.total !== undefined);
22
24
  // An aligned strip sizes its columns from the region's width; it needs
23
25
  // the column count and whether a total column takes a share.
@@ -27,5 +29,5 @@ export function DenominationStrip({ className, framed, label, columns, rows, zer
27
29
  '--sw-denomination-columns': columns.length,
28
30
  '--sw-denomination-total': hasTotals ? 1 : 0,
29
31
  };
30
- return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned', framed && denominationScrollFramedClass), 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), framed && denominationFramedClass, 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))) })] }) }));
32
+ return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned', framed && denominationScrollFramedClass), 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), framed && denominationFramedClass, 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, rowIndex) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, idPrefix: idPrefix, row: row, rowIndex: rowIndex, zeroLabel: zeroLabel }, row.id))) })] }) }));
31
33
  }
@@ -4,6 +4,8 @@ type EntryCellProps = {
4
4
  entry: DenominationGridEntry;
5
5
  column: DenominationGridColumn;
6
6
  index: number;
7
+ /** The value cell under this count, which describes it, if any. */
8
+ describedBy?: string;
7
9
  };
8
10
  /**
9
11
  * One editable count of a strip row: a full-width `Stepper` named by the
@@ -11,5 +13,5 @@ type EntryCellProps = {
11
13
  * message from the entry, and the cell's tone on its border (an accent one
12
14
  * is the Stepper's changed look).
13
15
  */
14
- export declare function EntryCell({ row, entry, column, index }: EntryCellProps): import("react").JSX.Element;
16
+ export declare function EntryCell({ row, entry, column, index, describedBy, }: EntryCellProps): import("react").JSX.Element;
15
17
  export {};
@@ -9,8 +9,8 @@ import { columnName } from './ColumnHeadText.js';
9
9
  * message from the entry, and the cell's tone on its border (an accent one
10
10
  * is the Stepper's changed look).
11
11
  */
12
- export function EntryCell({ row, entry, column, index }) {
12
+ export function EntryCell({ row, entry, column, index, describedBy, }) {
13
13
  const max = entry.max?.[index] ?? undefined;
14
14
  const tone = row.cellTones?.[index];
15
- return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, limitAppearance: entry.limitAppearance, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
15
+ return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { "aria-describedby": describedBy, changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, limitAppearance: entry.limitAppearance, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
16
16
  }
@@ -4,10 +4,16 @@ type StripRowProps = {
4
4
  columns: readonly DenominationGridColumn[];
5
5
  zeroLabel: string;
6
6
  hasTotals: boolean;
7
+ /** A per-grid prefix that keeps the value cells' ids unique on the page. */
8
+ idPrefix: string;
9
+ /** The row's place in the grid, for its value cells' ids. */
10
+ rowIndex: number;
7
11
  };
8
12
  /**
9
13
  * One strip row: its pinned label (with the phone total under it), a count
10
- * per column, read-only or entered, and its total.
14
+ * per column, read-only or entered, and its total. With a `valueRow`, the
15
+ * value line follows it and takes the total; each entry count is described
16
+ * by the value under it.
11
17
  */
12
- export declare function StripRow({ row, columns, zeroLabel, hasTotals, }: StripRowProps): import("react").JSX.Element;
18
+ export declare function StripRow({ row, columns, zeroLabel, hasTotals, idPrefix, rowIndex, }: StripRowProps): import("react").JSX.Element;
13
19
  export {};
@@ -1,9 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
4
4
  import { denominationCellView } from '../../denomination-cells.js';
5
5
  import { EntryCell } from './EntryCell.js';
6
6
  import { RowLabelLine } from './RowLabelLine.js';
7
+ import { StripTotalCell, StripTotalInline } from './StripTotal.js';
8
+ import { hasValue, StripValueRow, valueCellId } from './StripValueRow.js';
7
9
  /**
8
10
  * One count, read-only: the zero label, a signed delta, and the cell's tone.
9
11
  * The value has its own box, which the `pill` tone appearance fills.
@@ -14,9 +16,16 @@ function CountCell({ row, index, zeroLabel, }) {
14
16
  }
15
17
  /**
16
18
  * One strip row: its pinned label (with the phone total under it), a count
17
- * per column, read-only or entered, and its total.
19
+ * per column, read-only or entered, and its total. With a `valueRow`, the
20
+ * value line follows it and takes the total; each entry count is described
21
+ * by the value under it.
18
22
  */
19
- export function StripRow({ row, columns, zeroLabel, hasTotals, }) {
20
- const { entry } = row;
21
- return (_jsxs("tr", { "aria-disabled": row.disabled || undefined, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed', row.emphasis === 'quiet' && 'sw-denomination-row-quiet', row.disabled && 'sw-denomination-row-disabled'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => entry ? (_jsx(EntryCell, { column: column, entry: entry, index: index, row: row }, column.key)) : (_jsx(CountCell, { index: index, row: row, zeroLabel: zeroLabel }, column.key))), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }));
23
+ export function StripRow({ row, columns, zeroLabel, hasTotals, idPrefix, rowIndex, }) {
24
+ const { entry, valueRow } = row;
25
+ // With a value line, the total is on it, not on the counts.
26
+ const total = valueRow ? undefined : row.total;
27
+ const describedBy = (index) => hasValue(valueRow?.cells[index])
28
+ ? valueCellId(idPrefix, rowIndex, index)
29
+ : undefined;
30
+ return (_jsxs(_Fragment, { children: [_jsxs("tr", { "aria-disabled": row.disabled || undefined, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed', row.emphasis === 'quiet' && 'sw-denomination-row-quiet', row.disabled && 'sw-denomination-row-disabled'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), _jsx(StripTotalInline, { total: total })] }) }), columns.map((column, index) => entry ? (_jsx(EntryCell, { column: column, describedBy: describedBy(index), entry: entry, index: index, row: row }, column.key)) : (_jsx(CountCell, { index: index, row: row, zeroLabel: zeroLabel }, column.key))), hasTotals ? _jsx(StripTotalCell, { total: total }) : null] }), valueRow ? (_jsx(StripValueRow, { columns: columns, hasTotals: hasTotals, idPrefix: idPrefix, row: row, rowIndex: rowIndex, valueRow: valueRow })) : null] }));
22
31
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A strip row's total cell: the consumer's total, or empty. Below md it is
3
+ * visually hidden, and `StripTotalInline` shows it under the row's name.
4
+ */
5
+ export declare function StripTotalCell({ total }: {
6
+ total: string | undefined;
7
+ }): import("react").JSX.Element;
8
+ /** The total under a row's name on a phone; the cell reads it to everyone. */
9
+ export declare function StripTotalInline({ total }: {
10
+ total: string | undefined;
11
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * A strip row's total cell: the consumer's total, or empty. Below md it is
4
+ * visually hidden, and `StripTotalInline` shows it under the row's name.
5
+ */
6
+ export function StripTotalCell({ total }) {
7
+ return (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: total ?? '' }) }));
8
+ }
9
+ /** The total under a row's name on a phone; the cell reads it to everyone. */
10
+ export function StripTotalInline({ total }) {
11
+ return total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: total })) : null;
12
+ }
@@ -0,0 +1,25 @@
1
+ import { type DenominationGridColumn, type DenominationGridRow, type DenominationGridValueRow } from './types.js';
2
+ /**
3
+ * The id of a value cell, which the count above it is described by: the
4
+ * grid's prefix and the cell's place, never consumer text, so it is always
5
+ * one valid token of an id list.
6
+ */
7
+ export declare function valueCellId(idPrefix: string, rowIndex: number, columnIndex: number): string;
8
+ /** Whether a value cell shows something: not `null` and not blank. */
9
+ export declare function hasValue(cell: string | null | undefined): cell is string;
10
+ type StripValueRowProps = {
11
+ row: DenominationGridRow;
12
+ valueRow: DenominationGridValueRow;
13
+ columns: readonly DenominationGridColumn[];
14
+ hasTotals: boolean;
15
+ /** A per-grid prefix that keeps the value cells' ids unique on the page. */
16
+ idPrefix: string;
17
+ rowIndex: number;
18
+ };
19
+ /**
20
+ * The value line under a strip row: its name in the label column (with the
21
+ * row's total under it on a phone), a centred value per column, and the
22
+ * row's total. It keeps the row's tone, so a toned total stays toned.
23
+ */
24
+ export declare function StripValueRow({ row, valueRow, columns, hasTotals, idPrefix, rowIndex, }: StripValueRowProps): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { StripTotalCell, StripTotalInline } from './StripTotal.js';
4
+ import { denominationValueRowClass } from '../../css/css-denomination-value-row.js';
5
+ /**
6
+ * The id of a value cell, which the count above it is described by: the
7
+ * grid's prefix and the cell's place, never consumer text, so it is always
8
+ * one valid token of an id list.
9
+ */
10
+ export function valueCellId(idPrefix, rowIndex, columnIndex) {
11
+ return `${idPrefix}value-${rowIndex}-${columnIndex}`;
12
+ }
13
+ /** Whether a value cell shows something: not `null` and not blank. */
14
+ export function hasValue(cell) {
15
+ return typeof cell === 'string' && cell.trim() !== '';
16
+ }
17
+ /**
18
+ * The value line under a strip row: its name in the label column (with the
19
+ * row's total under it on a phone), a centred value per column, and the
20
+ * row's total. It keeps the row's tone, so a toned total stays toned.
21
+ */
22
+ export function StripValueRow({ row, valueRow, columns, hasTotals, idPrefix, rowIndex, }) {
23
+ return (_jsxs("tr", { "aria-disabled": row.disabled || undefined, className: cx('sw-denomination-row', denominationValueRowClass, `sw-denomination-row-${row.tone ?? 'neutral'}`, row.disabled && 'sw-denomination-row-disabled'), children: [_jsx("th", { "aria-label": `${row.label} ${valueRow.label}`, className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx("span", { className: "sw-denomination-label-line", children: _jsx("span", { className: "sw-denomination-label-text", children: valueRow.label }) }), _jsx(StripTotalInline, { total: row.total })] }) }), columns.map((column, index) => {
24
+ const cell = valueRow.cells[index];
25
+ return (_jsx("td", { className: "sw-denomination-value", id: valueCellId(idPrefix, rowIndex, index), children: hasValue(cell) ? cell : '' }, column.key));
26
+ }), hasTotals ? _jsx(StripTotalCell, { total: row.total }) : null] }));
27
+ }
@@ -45,6 +45,20 @@ export type DenominationGridEntry = {
45
45
  */
46
46
  limitAppearance?: StepperLimitAppearance;
47
47
  };
48
+ /**
49
+ * A line under a strip row that gives each column's value, such as the
50
+ * money its count adds up to: its own table row on the subtle fill, named
51
+ * in the label column, one consumer-formatted value per column centred
52
+ * under its count (`null` or `''` leaves a cell blank, such as for a zero
53
+ * count), and the row's `total` in the total column. The grid does no
54
+ * arithmetic and no currency formatting.
55
+ */
56
+ export type DenominationGridValueRow = {
57
+ /** The line's name in the label column, such as "Value". */
58
+ label: string;
59
+ /** One formatted value, or `null` for none, per column. */
60
+ cells: readonly (string | null)[];
61
+ };
48
62
  export type DenominationGridRow = {
49
63
  id: string;
50
64
  label: string;
@@ -80,6 +94,11 @@ export type DenominationGridRow = {
80
94
  * `entry.errors` wins.
81
95
  */
82
96
  entry?: DenominationGridEntry;
97
+ /**
98
+ * A value line under this row (the `strip` layout only); the row's
99
+ * `total` moves to it, and each entry count is described by its value.
100
+ */
101
+ valueRow?: DenominationGridValueRow;
83
102
  };
84
103
  export type DenominationGridProps = {
85
104
  label: string;
@@ -0,0 +1,6 @@
1
+ import { type DenominationGridColumn, type DenominationGridLayout, type DenominationGridRow } from './types.js';
2
+ /**
3
+ * Value rows: the strip layout only, a name that is not blank, and one
4
+ * formatted value or null per column.
5
+ */
6
+ export declare function assertDenominationValueRows(layout: DenominationGridLayout, columns: readonly DenominationGridColumn[], rows: readonly DenominationGridRow[]): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Value rows: the strip layout only, a name that is not blank, and one
3
+ * formatted value or null per column.
4
+ */
5
+ export function assertDenominationValueRows(layout, columns, rows) {
6
+ const valueRows = rows.flatMap((row) => row.valueRow == null ? [] : [row.valueRow]);
7
+ if (valueRows.length === 0)
8
+ return;
9
+ if (layout !== 'strip')
10
+ throw new RangeError('value rows need the strip layout');
11
+ for (const valueRow of valueRows) {
12
+ if (typeof valueRow !== 'object')
13
+ throw new RangeError('a value row is an object of label and cells');
14
+ if (typeof valueRow.label !== 'string' || !valueRow.label.trim())
15
+ throw new RangeError('a value row needs a label');
16
+ if (!fitsColumns(valueRow.cells, columns.length))
17
+ throw new RangeError('a value row needs one string or null per column');
18
+ }
19
+ }
20
+ function fitsColumns(cells, columnCount) {
21
+ if (!Array.isArray(cells) || cells.length !== columnCount)
22
+ return false;
23
+ // An index loop, not every(): every() skips the holes of a sparse array.
24
+ for (let index = 0; index < columnCount; index += 1) {
25
+ const cell = cells[index];
26
+ if (cell !== null && typeof cell !== 'string')
27
+ return false;
28
+ }
29
+ return true;
30
+ }
@@ -7,7 +7,10 @@ export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
7
7
  * `sub` sets the row under the one above it, as an item of a batch: its
8
8
  * row header (`TableCell as="th" scope="row"`) is indented with a guide
9
9
  * bar at the indent; a sub row without a row header is not indented.
10
- * `default` is a row of its own.
10
+ * `total` is a total mid-table, such as a subtotal the rows above add up
11
+ * to: its cells at weight 600 under a 2 px rule, as a
12
+ * `TableFooter` total is; name it with a row header and keep it in the
13
+ * body. `default` is a row of its own.
11
14
  */
12
15
  variant?: TableRowVariant;
13
16
  /**
@@ -40,7 +43,10 @@ export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAtt
40
43
  * `sub` sets the row under the one above it, as an item of a batch: its
41
44
  * row header (`TableCell as="th" scope="row"`) is indented with a guide
42
45
  * bar at the indent; a sub row without a row header is not indented.
43
- * `default` is a row of its own.
46
+ * `total` is a total mid-table, such as a subtotal the rows above add up
47
+ * to: its cells at weight 600 under a 2 px rule, as a
48
+ * `TableFooter` total is; name it with a row header and keep it in the
49
+ * body. `default` is a row of its own.
44
50
  */
45
51
  variant?: TableRowVariant;
46
52
  /**
@@ -25,5 +25,5 @@ export const TableRow = forwardRef(function TableRow({ className, onClick, press
25
25
  forwarding.current = false;
26
26
  }
27
27
  };
28
- return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', struck && 'sw-table-row-struck', pressable && 'sw-table-row-press', className), onClick: pressable || onClick ? press : undefined, ...rest }));
28
+ return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', variant === 'total' && 'sw-table-row-total', struck && 'sw-table-row-struck', pressable && 'sw-table-row-press', className), onClick: pressable || onClick ? press : undefined, ...rest }));
29
29
  });
@@ -0,0 +1,20 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { TableStickyHeader } from './table-layout.js';
3
+ /**
4
+ * A table's bounded height, counted in rows: the table's region is as tall
5
+ * as `rows` single-line body rows plus its header row and, when it has one,
6
+ * its footer row, and the rest of the rows scroll inside the region. Rows
7
+ * taller than one line show fewer at a time.
8
+ */
9
+ export type TableMaxHeight = {
10
+ rows: number;
11
+ };
12
+ /**
13
+ * A bounded table pins its header to its own region (`stickyHeader` true,
14
+ * the default); `'page'` and `false` contradict that and throw, as do a
15
+ * value that is not `{ rows }` and a row count that is not a whole number
16
+ * from 1.
17
+ */
18
+ export declare function assertTableMaxHeight(maxHeight: TableMaxHeight | undefined, stickyHeader: TableStickyHeader): void;
19
+ /** The row count the generated `sw-table-bounded` height reads. */
20
+ export declare function tableMaxHeightStyle(maxHeight: TableMaxHeight | undefined): CSSProperties | undefined;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A bounded table pins its header to its own region (`stickyHeader` true,
3
+ * the default); `'page'` and `false` contradict that and throw, as do a
4
+ * value that is not `{ rows }` and a row count that is not a whole number
5
+ * from 1.
6
+ */
7
+ export function assertTableMaxHeight(maxHeight, stickyHeader) {
8
+ if (maxHeight === undefined)
9
+ return;
10
+ if (typeof maxHeight !== 'object' || maxHeight === null)
11
+ throw new RangeError(`Table maxHeight must be a row count, { rows }: ${String(maxHeight)}`);
12
+ const rows = maxHeight.rows;
13
+ if (typeof rows !== 'number' || !Number.isInteger(rows) || rows < 1)
14
+ throw new RangeError(`Table maxHeight rows must be a whole number from 1: ${String(rows)}`);
15
+ if (stickyHeader !== true)
16
+ throw new RangeError(`Table maxHeight pins the header to the table's own region; stickyHeader ${String(stickyHeader)} cannot be used with it`);
17
+ }
18
+ /** The row count the generated `sw-table-bounded` height reads. */
19
+ export function tableMaxHeightStyle(maxHeight) {
20
+ if (maxHeight === undefined)
21
+ return undefined;
22
+ return { '--sw-table-max-rows': maxHeight.rows };
23
+ }
@@ -1,14 +1,16 @@
1
1
  import { type RefObject } from 'react';
2
- import { type ScrollOverflow } from '../scroll-overflow.js';
3
- /** A box's overflow, and whether it has been measured yet. */
4
- export type MeasuredScrollOverflow = ScrollOverflow & {
2
+ import { type BlockScrollOverflow, type ScrollOverflow } from '../scroll-overflow.js';
3
+ /** A box's overflow on both axes, and whether it has been measured yet. */
4
+ export type MeasuredScrollOverflow = ScrollOverflow & BlockScrollOverflow & {
5
5
  measured: boolean;
6
6
  };
7
7
  /**
8
- * Follows which inline edges of a scroll box have more content past them:
9
- * on mount, on every scroll, and whenever the box or its content changes
10
- * size. Before mount (and on the server) it reports no overflow and
11
- * `measured: false`, so the box renders as it always has until it is
12
- * measured; a measured box with neither edge set fits its content.
8
+ * Follows which edges of a scroll box have more content past them, the
9
+ * inline start and end and the top and bottom: on mount, on every scroll,
10
+ * and whenever the box or its content changes size. Before mount (and on
11
+ * the server) it reports no overflow and `measured: false`, so the box
12
+ * renders as it always has until it is measured; a measured box with
13
+ * neither inline edge set fits its content's width. A box whose height
14
+ * follows its content never reports a block edge.
13
15
  */
14
16
  export declare function useScrollOverflow(ref: RefObject<HTMLElement | null>): MeasuredScrollOverflow;
@@ -1,16 +1,31 @@
1
1
  'use client';
2
2
  import { useEffect, useState } from 'react';
3
- import { noScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
3
+ import { blockScrollOverflow, noBlockScrollOverflow, noScrollOverflow, sameBlockScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
4
4
  const unmeasured = {
5
5
  ...noScrollOverflow,
6
+ ...noBlockScrollOverflow,
6
7
  measured: false,
7
8
  };
9
+ function measureBox(box) {
10
+ return {
11
+ ...scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth),
12
+ ...blockScrollOverflow(box.scrollTop, box.scrollHeight, box.clientHeight),
13
+ measured: true,
14
+ };
15
+ }
16
+ function sameMeasure(current, next) {
17
+ return (current.measured &&
18
+ sameScrollOverflow(current, next) &&
19
+ sameBlockScrollOverflow(current, next));
20
+ }
8
21
  /**
9
- * Follows which inline edges of a scroll box have more content past them:
10
- * on mount, on every scroll, and whenever the box or its content changes
11
- * size. Before mount (and on the server) it reports no overflow and
12
- * `measured: false`, so the box renders as it always has until it is
13
- * measured; a measured box with neither edge set fits its content.
22
+ * Follows which edges of a scroll box have more content past them, the
23
+ * inline start and end and the top and bottom: on mount, on every scroll,
24
+ * and whenever the box or its content changes size. Before mount (and on
25
+ * the server) it reports no overflow and `measured: false`, so the box
26
+ * renders as it always has until it is measured; a measured box with
27
+ * neither inline edge set fits its content's width. A box whose height
28
+ * follows its content never reports a block edge.
14
29
  */
15
30
  export function useScrollOverflow(ref) {
16
31
  const [overflow, setOverflow] = useState(unmeasured);
@@ -25,10 +40,8 @@ export function useScrollOverflow(ref) {
25
40
  function measure() {
26
41
  if (!box)
27
42
  return;
28
- const next = scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth);
29
- setOverflow((current) => current.measured && sameScrollOverflow(current, next)
30
- ? current
31
- : { ...next, measured: true });
43
+ const next = measureBox(box);
44
+ setOverflow((current) => (sameMeasure(current, next) ? current : next));
32
45
  }
33
46
  measure();
34
47
  box.addEventListener('scroll', measure, { passive: true });
@@ -1,5 +1,8 @@
1
1
  /**
2
- * The stroked chevron Accordion and Select share: two muted sides of a
3
- * `space-2` square, turned by each surface to point where it needs.
2
+ * A stroked chevron in `color`: two sides of a `space-2` square (its
3
+ * bottom and right borders, so forced colours keep it), turned by each
4
+ * surface to point where it needs.
4
5
  */
5
- export declare const chevronStroke = "border-bottom: 2px solid var(--sw-color-muted);\n border-right: 2px solid var(--sw-color-muted);\n box-sizing: border-box;\n height: var(--sw-space-2);\n width: var(--sw-space-2);";
6
+ export declare function chevronStrokeIn(color: string): string;
7
+ /** The muted chevron Accordion, Select and the menus share. */
8
+ export declare const chevronStroke: string;
@@ -1,9 +1,14 @@
1
1
  /**
2
- * The stroked chevron Accordion and Select share: two muted sides of a
3
- * `space-2` square, turned by each surface to point where it needs.
2
+ * A stroked chevron in `color`: two sides of a `space-2` square (its
3
+ * bottom and right borders, so forced colours keep it), turned by each
4
+ * surface to point where it needs.
4
5
  */
5
- export const chevronStroke = `border-bottom: 2px solid var(--sw-color-muted);
6
- border-right: 2px solid var(--sw-color-muted);
6
+ export function chevronStrokeIn(color) {
7
+ return `border-bottom: 2px solid ${color};
8
+ border-right: 2px solid ${color};
7
9
  box-sizing: border-box;
8
10
  height: var(--sw-space-2);
9
11
  width: var(--sw-space-2);`;
12
+ }
13
+ /** The muted chevron Accordion, Select and the menus share. */
14
+ export const chevronStroke = chevronStrokeIn('var(--sw-color-muted)');
@@ -4,6 +4,8 @@ import { cssAccordionClasses } from './css-accordion.js';
4
4
  import { cssButtonClasses } from './css-button.js';
5
5
  import { cssCalendarButtonClasses } from './css-calendar-button.js';
6
6
  import { cssChartClasses } from './css-chart.js';
7
+ import { cssDataBarClasses } from './css-data-bar.js';
8
+ import { cssPaginationClasses } from './css-pagination.js';
7
9
  import { cssCheckboxClasses } from './css-checkbox.js';
8
10
  import { cssRadioGroupClasses } from './css-radio-group.js';
9
11
  import { cssChromeClasses } from './css-chrome.js';
@@ -137,6 +139,10 @@ ${cssCalendarButtonClasses()}
137
139
 
138
140
  ${cssChartClasses()}
139
141
 
142
+ ${cssDataBarClasses()}
143
+
144
+ ${cssPaginationClasses()}
145
+
140
146
  ${cssChromeClasses()}
141
147
 
142
148
  ${cssSectionNavClasses()}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a range bar's fill means: the start or a carried figure (`neutral`),
3
+ * money in (`success`), money out (`muted`, never danger: an outflow is not
4
+ * an error), a subtotal (`strong`) and what was counted or recorded
5
+ * (`accent`).
6
+ */
7
+ export declare const dataBarTones: readonly ["neutral", "success", "muted", "strong", "accent"];
8
+ export type DataBarTone = (typeof dataBarTones)[number];
9
+ export declare const dataBarToneMixes: Record<DataBarTone, {
10
+ color: 'muted' | 'success' | 'text' | 'accent';
11
+ share: number;
12
+ }>;
13
+ /** A tone's fill: its colour, mixed into the surface by its share. */
14
+ export declare function dataBarToneFill(tone: DataBarTone): string;
15
+ export declare function cssDataBarClasses(): string;
16
+ export declare function dataBarClassCatalog(): string[];