@scalewing/react 1.25.0 → 1.27.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.
- package/CHANGELOG.md +45 -0
- package/README.md +110 -1
- package/dist/components/ActionBar.d.ts +14 -0
- package/dist/components/ActionBar.js +5 -2
- package/dist/components/DenominationGrid.d.ts +1 -1
- package/dist/components/DenominationGrid.js +3 -0
- package/dist/components/DescriptionList.d.ts +28 -0
- package/dist/components/DescriptionList.js +15 -0
- package/dist/components/Stepper.d.ts +44 -0
- package/dist/components/Stepper.js +77 -0
- package/dist/components/Steps.d.ts +18 -0
- package/dist/components/Steps.js +34 -0
- package/dist/components/action-bar/ActionBarSummary.d.ts +22 -0
- package/dist/components/action-bar/ActionBarSummary.js +23 -0
- package/dist/components/denomination-grid/ColumnHeadText.d.ts +7 -0
- package/dist/components/denomination-grid/ColumnHeadText.js +13 -0
- package/dist/components/denomination-grid/DenominationStrip.js +3 -7
- package/dist/components/denomination-grid/DenominationTiles.js +2 -1
- package/dist/components/denomination-grid/EntryCell.d.ts +15 -0
- package/dist/components/denomination-grid/EntryCell.js +16 -0
- package/dist/components/denomination-grid/RowLabelLine.d.ts +3 -2
- package/dist/components/denomination-grid/RowLabelLine.js +5 -3
- package/dist/components/denomination-grid/StripRow.d.ts +13 -0
- package/dist/components/denomination-grid/StripRow.js +19 -0
- package/dist/components/denomination-grid/types.d.ts +44 -1
- package/dist/components/denomination-grid/validate-entry.d.ts +15 -0
- package/dist/components/denomination-grid/validate-entry.js +78 -0
- package/dist/components/steps/Step.d.ts +24 -0
- package/dist/components/steps/Step.js +62 -0
- package/dist/components/steps/step-status.d.ts +26 -0
- package/dist/components/steps/step-status.js +37 -0
- package/dist/components/steps/steps-context.d.ts +16 -0
- package/dist/components/steps/steps-context.js +3 -0
- package/dist/css/css-action-bar-summary.d.ts +2 -0
- package/dist/css/css-action-bar-summary.js +48 -0
- package/dist/css/css-action-bar.js +4 -0
- package/dist/css/css-components.js +10 -0
- package/dist/css/css-denomination-entry.d.ts +2 -0
- package/dist/css/css-denomination-entry.js +78 -0
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-description-list.d.ts +12 -0
- package/dist/css/css-description-list.js +63 -0
- package/dist/css/css-stepper.d.ts +10 -0
- package/dist/css/css-stepper.js +126 -0
- package/dist/css/css-steps.d.ts +9 -0
- package/dist/css/css-steps.js +203 -0
- package/dist/css/stylesheet.js +6 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/stepper-count.d.ts +37 -0
- package/dist/stepper-count.js +50 -0
- package/dist/styles.css +367 -0
- package/package.json +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
3
|
import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
|
|
4
|
-
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
5
|
-
import { denominationCellView } from '../../denomination-cells.js';
|
|
6
4
|
import { ScrollRegion } from '../ScrollRegion.js';
|
|
7
|
-
import {
|
|
5
|
+
import { ColumnHeadText } from './ColumnHeadText.js';
|
|
6
|
+
import { StripRow } from './StripRow.js';
|
|
8
7
|
/**
|
|
9
8
|
* The total column's header: the consumer's name for the column, shown over
|
|
10
9
|
* the totals from md up in the column heads' style, and visually hidden
|
|
@@ -27,8 +26,5 @@ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel,
|
|
|
27
26
|
'--sw-denomination-columns': columns.length,
|
|
28
27
|
'--sw-denomination-total': hasTotals ? 1 : 0,
|
|
29
28
|
};
|
|
30
|
-
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', 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: column
|
|
31
|
-
const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
|
|
32
|
-
return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }, column.key));
|
|
33
|
-
}), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, 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)), 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))) })] }) }));
|
|
34
30
|
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } 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
|
+
import { ColumnHeadText } from './ColumnHeadText.js';
|
|
5
6
|
import { RowLabelLine } from './RowLabelLine.js';
|
|
6
7
|
/**
|
|
7
8
|
* A lone row is named by the grid's own label, so its label line shows only
|
|
@@ -32,7 +33,7 @@ export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zer
|
|
|
32
33
|
const amount = subtotal && cell !== null && cell !== 0
|
|
33
34
|
? subtotal(cell, column)
|
|
34
35
|
: null;
|
|
35
|
-
return (_jsxs("li", { className: cx('sw-denomination-tile', ...denominationCellToneClassNames(row.cellTones?.[index])), children: [_jsx("span", { className: "sw-denomination-head", children: column
|
|
36
|
+
return (_jsxs("li", { className: cx('sw-denomination-tile', ...denominationCellToneClassNames(row.cellTones?.[index])), children: [_jsx("span", { className: "sw-denomination-head", children: _jsx(ColumnHeadText, { column: column }) }), _jsx("span", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }), subtotal ? (_jsx("span", { className: "sw-denomination-subtotal", children: amount ?? zeroLabel })) : null] }, column.key));
|
|
36
37
|
}) })] }, row.id));
|
|
37
38
|
}) }));
|
|
38
39
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DenominationGridColumn, type DenominationGridEntry, type DenominationGridRow } from './types.js';
|
|
2
|
+
type EntryCellProps = {
|
|
3
|
+
row: DenominationGridRow;
|
|
4
|
+
entry: DenominationGridEntry;
|
|
5
|
+
column: DenominationGridColumn;
|
|
6
|
+
index: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* One editable count of a strip row: a full-width `Stepper` named by the
|
|
10
|
+
* row and the column as its header reads ("Counted $1 × 100"), its cap and
|
|
11
|
+
* message from the entry, and the cell's tone on its border (an accent one
|
|
12
|
+
* is the Stepper's changed look).
|
|
13
|
+
*/
|
|
14
|
+
export declare function EntryCell({ row, entry, column, index }: EntryCellProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
4
|
+
import { Stepper } from '../Stepper.js';
|
|
5
|
+
import { columnName } from './ColumnHeadText.js';
|
|
6
|
+
/**
|
|
7
|
+
* One editable count of a strip row: a full-width `Stepper` named by the
|
|
8
|
+
* row and the column as its header reads ("Counted $1 × 100"), its cap and
|
|
9
|
+
* message from the entry, and the cell's tone on its border (an accent one
|
|
10
|
+
* is the Stepper's changed look).
|
|
11
|
+
*/
|
|
12
|
+
export function EntryCell({ row, entry, column, index }) {
|
|
13
|
+
const max = entry.max?.[index] ?? undefined;
|
|
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, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
|
|
16
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type DenominationGridRow } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* A row label's glyph and words, kept on one line: when the words wrap,
|
|
4
|
-
* they wrap beside the glyph, never under it (Teisoro DRW-28).
|
|
5
|
-
*
|
|
4
|
+
* they wrap beside the glyph, never under it (Teisoro DRW-28). A caption is
|
|
5
|
+
* a muted line under the words, beside the same glyph. Both the strip and
|
|
6
|
+
* the tiles layouts draw a row label with it.
|
|
6
7
|
*/
|
|
7
8
|
export declare function RowLabelLine({ row }: {
|
|
8
9
|
row: DenominationGridRow;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* A row label's glyph and words, kept on one line: when the words wrap,
|
|
4
|
-
* they wrap beside the glyph, never under it (Teisoro DRW-28).
|
|
5
|
-
*
|
|
4
|
+
* they wrap beside the glyph, never under it (Teisoro DRW-28). A caption is
|
|
5
|
+
* a muted line under the words, beside the same glyph. Both the strip and
|
|
6
|
+
* the tiles layouts draw a row label with it.
|
|
6
7
|
*/
|
|
7
8
|
export function RowLabelLine({ row }) {
|
|
8
|
-
|
|
9
|
+
const text = _jsx("span", { className: "sw-denomination-label-text", children: row.label });
|
|
10
|
+
return (_jsxs("span", { className: "sw-denomination-label-line", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, row.caption === undefined ? (text) : (_jsxs("span", { className: "sw-denomination-label-words", children: [text, _jsx("span", { className: "sw-denomination-label-caption", children: row.caption })] }))] }));
|
|
9
11
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DenominationGridColumn, type DenominationGridRow } from './types.js';
|
|
2
|
+
type StripRowProps = {
|
|
3
|
+
row: DenominationGridRow;
|
|
4
|
+
columns: readonly DenominationGridColumn[];
|
|
5
|
+
zeroLabel: string;
|
|
6
|
+
hasTotals: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* One strip row: its pinned label (with the phone total under it), a count
|
|
10
|
+
* per column, read-only or entered, and its total.
|
|
11
|
+
*/
|
|
12
|
+
export declare function StripRow({ row, columns, zeroLabel, hasTotals, }: StripRowProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
4
|
+
import { denominationCellView } from '../../denomination-cells.js';
|
|
5
|
+
import { EntryCell } from './EntryCell.js';
|
|
6
|
+
import { RowLabelLine } from './RowLabelLine.js';
|
|
7
|
+
/** One count, read-only: the zero label, a signed delta, and the cell's tone. */
|
|
8
|
+
function CountCell({ row, index, zeroLabel, }) {
|
|
9
|
+
const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
|
|
10
|
+
return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One strip row: its pinned label (with the phone total under it), a count
|
|
14
|
+
* per column, read-only or entered, and its total.
|
|
15
|
+
*/
|
|
16
|
+
export function StripRow({ row, columns, zeroLabel, hasTotals, }) {
|
|
17
|
+
const { entry } = row;
|
|
18
|
+
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] }));
|
|
19
|
+
}
|
|
@@ -12,6 +12,29 @@ export type DenominationGridRowRole = 'region' | 'group';
|
|
|
12
12
|
export type DenominationGridColumn = {
|
|
13
13
|
key: string;
|
|
14
14
|
label: string;
|
|
15
|
+
/** A muted note after the label, such as "× 100" for a strap of $1 bills. */
|
|
16
|
+
caption?: string;
|
|
17
|
+
};
|
|
18
|
+
/** How a row sits among the others: `quiet` sets it on the subtle fill, for a row to compare against. */
|
|
19
|
+
export type DenominationGridRowEmphasis = 'default' | 'quiet';
|
|
20
|
+
/**
|
|
21
|
+
* Makes a strip row's counts editable: one `Stepper` per column, holding the
|
|
22
|
+
* row's `cells` (`null` is an empty box), named by the row label and the
|
|
23
|
+
* column's label and caption ("Counted $1 × 100"). The minimum is 0. The
|
|
24
|
+
* grid does no arithmetic: recompute the row's `total` in `onChange`.
|
|
25
|
+
*/
|
|
26
|
+
export type DenominationGridEntry = {
|
|
27
|
+
onChange: (columnKey: string, value: number | null) => void;
|
|
28
|
+
/** The − buttons' name, such as "One less". */
|
|
29
|
+
decrementLabel: string;
|
|
30
|
+
/** The + buttons' name, such as "One more". */
|
|
31
|
+
incrementLabel: string;
|
|
32
|
+
/** A cap per column (`null` for none), such as the bills the drawer holds. */
|
|
33
|
+
max?: readonly (number | null)[];
|
|
34
|
+
/** A message per column (`null` for none), shown under that count. */
|
|
35
|
+
errors?: readonly (string | null)[];
|
|
36
|
+
/** What an empty box shows, such as "0". */
|
|
37
|
+
placeholder?: string;
|
|
15
38
|
};
|
|
16
39
|
export type DenominationGridRow = {
|
|
17
40
|
id: string;
|
|
@@ -25,9 +48,29 @@ export type DenominationGridRow = {
|
|
|
25
48
|
* A tone per count, one entry per column (null for none), for a single
|
|
26
49
|
* count that needs attention, such as a bill the vault is short of. The
|
|
27
50
|
* count is set in the tone and, in the tiles layout, its tile's border
|
|
28
|
-
* too, in place
|
|
51
|
+
* too, in place; an entry count's border takes it. The tone never says
|
|
52
|
+
* why alone: put that in words.
|
|
29
53
|
*/
|
|
30
54
|
cellTones?: readonly (DenominationGridTone | null)[];
|
|
55
|
+
/** A muted line under the label, such as "from the customer". */
|
|
56
|
+
caption?: string;
|
|
57
|
+
/** The `strip` layout only. */
|
|
58
|
+
emphasis?: DenominationGridRowEmphasis;
|
|
59
|
+
/**
|
|
60
|
+
* A row that does not apply now (the `strip` layout only): its entry is
|
|
61
|
+
* disabled, its label, counts and total fade, and the row is
|
|
62
|
+
* `aria-disabled`.
|
|
63
|
+
*/
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Count entry for this row (the `strip` layout only). Its counts are zero
|
|
67
|
+
* or more, it is not `signed`, and its label is its own among the entry
|
|
68
|
+
* rows, as each column's label and caption are, so every count has its
|
|
69
|
+
* own name. A cell tone draws the count's border in that tone (an
|
|
70
|
+
* `accent` one is the `Stepper`'s changed look); a message in
|
|
71
|
+
* `entry.errors` wins.
|
|
72
|
+
*/
|
|
73
|
+
entry?: DenominationGridEntry;
|
|
31
74
|
};
|
|
32
75
|
export type DenominationGridProps = {
|
|
33
76
|
label: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DenominationGridColumn, type DenominationGridLayout, type DenominationGridRow } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The parts of a row and column that only label or set a row apart: a
|
|
4
|
+
* caption is never blank, an emphasis is a known one, `disabled` is a
|
|
5
|
+
* boolean, and a quiet or disabled row is a strip row (tiles have no row
|
|
6
|
+
* fill to set apart).
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertDenominationExtras(layout: DenominationGridLayout, columns: readonly DenominationGridColumn[], rows: readonly DenominationGridRow[]): void;
|
|
9
|
+
/**
|
|
10
|
+
* Entry rows: the strip layout only, named buttons, counts of zero or more,
|
|
11
|
+
* no signed deltas, one cap and one message (or null) per column with caps
|
|
12
|
+
* that are whole counts of zero or more, and one name per count: every
|
|
13
|
+
* entry row label and every column name (label and caption) is its own.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertDenominationEntries(layout: DenominationGridLayout, columns: readonly DenominationGridColumn[], rows: readonly DenominationGridRow[]): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { columnName } from './ColumnHeadText.js';
|
|
2
|
+
const emphases = ['default', 'quiet'];
|
|
3
|
+
/**
|
|
4
|
+
* The parts of a row and column that only label or set a row apart: a
|
|
5
|
+
* caption is never blank, an emphasis is a known one, `disabled` is a
|
|
6
|
+
* boolean, and a quiet or disabled row is a strip row (tiles have no row
|
|
7
|
+
* fill to set apart).
|
|
8
|
+
*/
|
|
9
|
+
export function assertDenominationExtras(layout, columns, rows) {
|
|
10
|
+
if (columns.some((column) => isBlank(column.caption)))
|
|
11
|
+
throw new RangeError('a column caption must not be empty');
|
|
12
|
+
if (rows.some((row) => isBlank(row.caption)))
|
|
13
|
+
throw new RangeError('a row caption must not be empty');
|
|
14
|
+
if (rows.some((row) => row.emphasis !== undefined && !emphases.includes(row.emphasis)))
|
|
15
|
+
throw new RangeError("emphasis must be 'default' or 'quiet'");
|
|
16
|
+
if (rows.some((row) => row.disabled !== undefined && typeof row.disabled !== 'boolean'))
|
|
17
|
+
throw new RangeError('disabled must be a boolean');
|
|
18
|
+
if (layout !== 'strip' &&
|
|
19
|
+
rows.some((row) => row.emphasis === 'quiet' || row.disabled))
|
|
20
|
+
throw new RangeError('quiet and disabled rows need the strip layout');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Entry rows: the strip layout only, named buttons, counts of zero or more,
|
|
24
|
+
* no signed deltas, one cap and one message (or null) per column with caps
|
|
25
|
+
* that are whole counts of zero or more, and one name per count: every
|
|
26
|
+
* entry row label and every column name (label and caption) is its own.
|
|
27
|
+
*/
|
|
28
|
+
export function assertDenominationEntries(layout, columns, rows) {
|
|
29
|
+
const entryRows = rows.filter((row) => row.entry);
|
|
30
|
+
if (entryRows.length === 0)
|
|
31
|
+
return;
|
|
32
|
+
if (layout !== 'strip')
|
|
33
|
+
throw new RangeError('entry rows need the strip layout');
|
|
34
|
+
if (!allDistinct(entryRows.map((row) => row.label)))
|
|
35
|
+
throw new RangeError('entry rows need distinct labels');
|
|
36
|
+
if (!allDistinct(columns.map(columnName)))
|
|
37
|
+
throw new RangeError('entry rows need columns with distinct labels and captions');
|
|
38
|
+
for (const row of entryRows) {
|
|
39
|
+
if (row.signed)
|
|
40
|
+
throw new RangeError('an entry row cannot be signed');
|
|
41
|
+
if (row.cells.some((cell) => cell !== null && cell < 0))
|
|
42
|
+
throw new RangeError('an entry row counts zero or more');
|
|
43
|
+
assertEntry(row.entry, columns.length);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertEntry(entry, columnCount) {
|
|
47
|
+
if (typeof entry.onChange !== 'function')
|
|
48
|
+
throw new RangeError('entry needs an onChange');
|
|
49
|
+
if (!entry.decrementLabel.trim() || !entry.incrementLabel.trim())
|
|
50
|
+
throw new RangeError('entry needs decrementLabel and incrementLabel');
|
|
51
|
+
if (!fitsColumns(entry.max, columnCount, isCap))
|
|
52
|
+
throw new RangeError('entry.max needs one whole count of zero or more, or null, per column');
|
|
53
|
+
if (!fitsColumns(entry.errors, columnCount, isMessage))
|
|
54
|
+
throw new RangeError('entry.errors needs one message or null per column');
|
|
55
|
+
}
|
|
56
|
+
function allDistinct(names) {
|
|
57
|
+
return new Set(names).size === names.length;
|
|
58
|
+
}
|
|
59
|
+
function isBlank(text) {
|
|
60
|
+
return text !== undefined && !text.trim();
|
|
61
|
+
}
|
|
62
|
+
function isCap(cap) {
|
|
63
|
+
return cap === null || (Number.isSafeInteger(cap) && cap >= 0);
|
|
64
|
+
}
|
|
65
|
+
function isMessage(message) {
|
|
66
|
+
return message === null || typeof message === 'string';
|
|
67
|
+
}
|
|
68
|
+
function fitsColumns(values, columnCount, fits) {
|
|
69
|
+
if (values === undefined)
|
|
70
|
+
return true;
|
|
71
|
+
if (!Array.isArray(values) || values.length !== columnCount)
|
|
72
|
+
return false;
|
|
73
|
+
// An index loop, not every(): every() skips the holes of a sparse array.
|
|
74
|
+
for (let index = 0; index < columnCount; index += 1)
|
|
75
|
+
if (!fits(values[index]))
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StepHeadingLevel, type StepStatus } from './step-status.js';
|
|
3
|
+
export type StepProps = {
|
|
4
|
+
status: StepStatus;
|
|
5
|
+
title: string;
|
|
6
|
+
/** One line beside a done step's title, such as "Cash · $480.00"; truncated. */
|
|
7
|
+
summary?: ReactNode;
|
|
8
|
+
/** Reopens a done step; needs `editLabel`. */
|
|
9
|
+
onEdit?: () => void;
|
|
10
|
+
/** The Edit button's words; its name adds the step's title ("Edit The sale"). */
|
|
11
|
+
editLabel?: string;
|
|
12
|
+
/** Muted words after a skipped step's title, such as "not needed". */
|
|
13
|
+
skippedReason?: string;
|
|
14
|
+
/** Overrides the list's heading level for this step's title. */
|
|
15
|
+
headingLevel?: StepHeadingLevel;
|
|
16
|
+
/** The step's fields, shown while it is current or in error. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One step of a `Steps` list: a numbered marker, the title as a heading,
|
|
21
|
+
* and, by status, its fields (current, error) or one folded line (a done
|
|
22
|
+
* step's summary and Edit; an upcoming or skipped step's title alone).
|
|
23
|
+
*/
|
|
24
|
+
export declare function Step(props: StepProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, useContext, useEffect, useId, useRef, } from 'react';
|
|
4
|
+
import { cx } from '../../class-names.js';
|
|
5
|
+
import { Button } from '../Button.js';
|
|
6
|
+
import { isOpenStep, stepHeadingLevels, stepStatuses, stepStatusWord, stepTakesFocus, } from './step-status.js';
|
|
7
|
+
import { StepPlaceContext } from './steps-context.js';
|
|
8
|
+
function assertStep(props, level) {
|
|
9
|
+
if (!props.title.trim())
|
|
10
|
+
throw new RangeError('Step title must not be empty');
|
|
11
|
+
if (!stepStatuses.includes(props.status))
|
|
12
|
+
throw new RangeError(`Unknown Step status: ${String(props.status)}`);
|
|
13
|
+
if (!stepHeadingLevels.includes(level))
|
|
14
|
+
throw new RangeError('Step headingLevel must be 2, 3 or 4');
|
|
15
|
+
if (props.onEdit && !props.editLabel?.trim())
|
|
16
|
+
throw new RangeError('Step onEdit needs an editLabel');
|
|
17
|
+
if (props.skippedReason !== undefined && !props.skippedReason.trim())
|
|
18
|
+
throw new RangeError('Step skippedReason must not be empty');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Moves focus to the heading when the step becomes current from a folded
|
|
22
|
+
* state, unless an earlier step of the list already took it in this commit
|
|
23
|
+
* (effects run in list order, so the first one wins; the claim is released
|
|
24
|
+
* after the flush).
|
|
25
|
+
*/
|
|
26
|
+
function useFocusWhenOpened(status, heading, focusClaimRef) {
|
|
27
|
+
const previous = useRef(undefined);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (stepTakesFocus(previous.current, status) && !focusClaimRef.current) {
|
|
30
|
+
focusClaimRef.current = true;
|
|
31
|
+
heading.current?.focus();
|
|
32
|
+
// One commit's effects run in one flush: release the claim after it,
|
|
33
|
+
// whichever component re-rendered.
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
focusClaimRef.current = false;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
previous.current = status;
|
|
39
|
+
}, [status, heading, focusClaimRef]);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One step of a `Steps` list: a numbered marker, the title as a heading,
|
|
43
|
+
* and, by status, its fields (current, error) or one folded line (a done
|
|
44
|
+
* step's summary and Edit; an upcoming or skipped step's title alone).
|
|
45
|
+
*/
|
|
46
|
+
export function Step(props) {
|
|
47
|
+
const place = useContext(StepPlaceContext);
|
|
48
|
+
if (!place)
|
|
49
|
+
throw new RangeError('Step must be a child of Steps');
|
|
50
|
+
const level = props.headingLevel ?? place.headingLevel;
|
|
51
|
+
assertStep(props, level);
|
|
52
|
+
const { status, title, summary, onEdit, editLabel, skippedReason, children } = props;
|
|
53
|
+
const heading = useRef(null);
|
|
54
|
+
const titleId = useId();
|
|
55
|
+
const editId = useId();
|
|
56
|
+
useFocusWhenOpened(status, heading, place.focusClaimRef);
|
|
57
|
+
const Heading = `h${level}`;
|
|
58
|
+
const reason = status === 'skipped' ? skippedReason : undefined;
|
|
59
|
+
const word = stepStatusWord(status, place.labels, reason !== undefined);
|
|
60
|
+
const done = status === 'done';
|
|
61
|
+
return (_jsxs("li", { "aria-current": status === 'current' ? 'step' : undefined, className: cx('sw-steps-item', `sw-steps-item-${status}`), children: [_jsxs("div", { className: "sw-steps-header", children: [_jsx("span", { "aria-hidden": "true", className: "sw-steps-marker", children: status === 'error' ? '!' : done ? null : place.number }), _jsxs(Heading, { ref: heading, className: "sw-steps-title", tabIndex: -1, children: [_jsx("span", { id: titleId, children: title }), reason ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-steps-reason", children: reason })] })) : null, word ? _jsxs("span", { className: "sw-sr-only", children: [", ", word] }) : null] }), done && summary ? (_jsx("span", { className: "sw-steps-summary", children: summary })) : null, done && onEdit ? (_jsx(Button, { "aria-labelledby": `${editId} ${titleId}`, className: "sw-steps-edit", id: editId, onPress: onEdit, size: "sm", variant: "ghost", children: editLabel })) : null] }), isOpenStep(status) && Children.toArray(children).length > 0 ? (_jsx("div", { className: "sw-steps-body", children: children })) : null] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Where a step stands in its form. */
|
|
2
|
+
export declare const stepStatuses: readonly ["upcoming", "current", "done", "error", "skipped"];
|
|
3
|
+
export type StepStatus = (typeof stepStatuses)[number];
|
|
4
|
+
/** A step title's heading level. */
|
|
5
|
+
export type StepHeadingLevel = 2 | 3 | 4;
|
|
6
|
+
export declare const stepHeadingLevels: readonly StepHeadingLevel[];
|
|
7
|
+
/** The words assistive technology hears after a folded step's title. */
|
|
8
|
+
export type StepsLabels = {
|
|
9
|
+
done: string;
|
|
10
|
+
error: string;
|
|
11
|
+
skipped: string;
|
|
12
|
+
upcoming: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const defaultStepsLabels: StepsLabels;
|
|
15
|
+
/** A current or error step shows its fields; the others fold to one line. */
|
|
16
|
+
export declare function isOpenStep(status: StepStatus): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a step that was `previous` and is now `next` should take focus: it
|
|
19
|
+
* became current from a folded state (upcoming, done, skipped). A first
|
|
20
|
+
* render (`previous` undefined), a move between current and error, and a
|
|
21
|
+
* step that turns to error keep focus where it is: after a refused submit
|
|
22
|
+
* the consumer focuses the first invalid field, as `Field` asks.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stepTakesFocus(previous: StepStatus | undefined, next: StepStatus): boolean;
|
|
25
|
+
/** The status word read after a step's title, or none (current, or a skipped step that shows its reason). */
|
|
26
|
+
export declare function stepStatusWord(status: StepStatus, labels: StepsLabels, hasReason: boolean): string | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Where a step stands in its form. */
|
|
2
|
+
export const stepStatuses = [
|
|
3
|
+
'upcoming',
|
|
4
|
+
'current',
|
|
5
|
+
'done',
|
|
6
|
+
'error',
|
|
7
|
+
'skipped',
|
|
8
|
+
];
|
|
9
|
+
export const stepHeadingLevels = [2, 3, 4];
|
|
10
|
+
export const defaultStepsLabels = {
|
|
11
|
+
done: 'Done',
|
|
12
|
+
error: 'Needs attention',
|
|
13
|
+
skipped: 'Not needed',
|
|
14
|
+
upcoming: 'Not started',
|
|
15
|
+
};
|
|
16
|
+
/** A current or error step shows its fields; the others fold to one line. */
|
|
17
|
+
export function isOpenStep(status) {
|
|
18
|
+
return status === 'current' || status === 'error';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a step that was `previous` and is now `next` should take focus: it
|
|
22
|
+
* became current from a folded state (upcoming, done, skipped). A first
|
|
23
|
+
* render (`previous` undefined), a move between current and error, and a
|
|
24
|
+
* step that turns to error keep focus where it is: after a refused submit
|
|
25
|
+
* the consumer focuses the first invalid field, as `Field` asks.
|
|
26
|
+
*/
|
|
27
|
+
export function stepTakesFocus(previous, next) {
|
|
28
|
+
return previous !== undefined && !isOpenStep(previous) && next === 'current';
|
|
29
|
+
}
|
|
30
|
+
/** The status word read after a step's title, or none (current, or a skipped step that shows its reason). */
|
|
31
|
+
export function stepStatusWord(status, labels, hasReason) {
|
|
32
|
+
if (status === 'current')
|
|
33
|
+
return null;
|
|
34
|
+
if (status === 'skipped' && hasReason)
|
|
35
|
+
return null;
|
|
36
|
+
return labels[status];
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type StepHeadingLevel, type StepsLabels } from './step-status.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a `Step` learns from its `Steps`: its number, its heading level, the
|
|
5
|
+
* status words, and the list's focus claim for this commit (the first step
|
|
6
|
+
* to open in a commit takes focus and releases the claim after that
|
|
7
|
+
* commit's effects).
|
|
8
|
+
*/
|
|
9
|
+
export type StepPlace = {
|
|
10
|
+
number: number;
|
|
11
|
+
headingLevel: StepHeadingLevel;
|
|
12
|
+
labels: StepsLabels;
|
|
13
|
+
focusClaimRef: RefObject<boolean>;
|
|
14
|
+
};
|
|
15
|
+
/** Null outside `Steps`: a `Step` must be one of a list's steps. */
|
|
16
|
+
export declare const StepPlaceContext: import("react").Context<StepPlace | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
const label = typographyVariants.label;
|
|
3
|
+
/*
|
|
4
|
+
* The bar's figure: label, value and detail on one baseline, a small gap
|
|
5
|
+
* apart, wrapping as one phrase. The value takes the heading variant and
|
|
6
|
+
* tabular figures from the text classes it carries (so it steps down below
|
|
7
|
+
* md with every heading); the label and detail are muted label-size words
|
|
8
|
+
* at regular weight. A tone colors the value only. Forced colors keep the
|
|
9
|
+
* system text color.
|
|
10
|
+
*/
|
|
11
|
+
export function cssActionBarSummaryClasses() {
|
|
12
|
+
return `.sw-action-bar-summary {
|
|
13
|
+
align-items: baseline;
|
|
14
|
+
column-gap: var(--sw-space-2);
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
flex-wrap: wrap;
|
|
17
|
+
min-width: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sw-action-bar-summary-label,
|
|
21
|
+
.sw-action-bar-summary-detail {
|
|
22
|
+
color: var(--sw-color-muted);
|
|
23
|
+
font-size: ${label.fontSize}px;
|
|
24
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
25
|
+
line-height: ${label.lineHeight}px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sw-action-bar-summary-value {
|
|
29
|
+
color: var(--sw-color-text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sw-action-bar-summary-danger .sw-action-bar-summary-value { color: var(--sw-color-danger); }
|
|
33
|
+
.sw-action-bar-summary-success .sw-action-bar-summary-value { color: var(--sw-color-success); }
|
|
34
|
+
|
|
35
|
+
@media (forced-colors: active) {
|
|
36
|
+
.sw-action-bar-summary-value { color: CanvasText; }
|
|
37
|
+
}`;
|
|
38
|
+
}
|
|
39
|
+
export function actionBarSummaryClassCatalog() {
|
|
40
|
+
return [
|
|
41
|
+
'sw-action-bar-summary',
|
|
42
|
+
'sw-action-bar-summary-label',
|
|
43
|
+
'sw-action-bar-summary-value',
|
|
44
|
+
'sw-action-bar-summary-detail',
|
|
45
|
+
'sw-action-bar-summary-danger',
|
|
46
|
+
'sw-action-bar-summary-success',
|
|
47
|
+
];
|
|
48
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery, breakpoints, } from './breakpoints.js';
|
|
3
|
+
import { actionBarSummaryClassCatalog, cssActionBarSummaryClasses, } from './css-action-bar-summary.js';
|
|
3
4
|
import { zIndex } from './stacking.js';
|
|
4
5
|
const caption = typographyVariants.caption;
|
|
5
6
|
/* The bar floats a step above the bottom edge and clears the home indicator. */
|
|
@@ -93,6 +94,8 @@ export function cssActionBarClasses() {
|
|
|
93
94
|
.sw-action-bar-actions > * { flex: 1 1 auto; }
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
${cssActionBarSummaryClasses()}
|
|
98
|
+
|
|
96
99
|
${stickyRules()}`;
|
|
97
100
|
}
|
|
98
101
|
export function actionBarClassCatalog() {
|
|
@@ -101,6 +104,7 @@ export function actionBarClassCatalog() {
|
|
|
101
104
|
'sw-action-bar-status',
|
|
102
105
|
'sw-action-bar-actions',
|
|
103
106
|
'sw-action-bar-sticky',
|
|
107
|
+
...actionBarSummaryClassCatalog(),
|
|
104
108
|
...breakpoints.map((breakpoint) => actionBarStickyClass(breakpoint)),
|
|
105
109
|
];
|
|
106
110
|
}
|
|
@@ -23,6 +23,7 @@ import { cssTooltipClasses } from './css-tooltip.js';
|
|
|
23
23
|
import { cssInfoTipClasses } from './css-info-tip.js';
|
|
24
24
|
import { cssSeparatorClasses } from './css-separator.js';
|
|
25
25
|
import { cssStatTileClasses } from './css-stat-tile.js';
|
|
26
|
+
import { cssDescriptionListClasses } from './css-description-list.js';
|
|
26
27
|
import { cssTabsClasses } from './css-tabs.js';
|
|
27
28
|
import { cssResponsiveClasses } from './css-responsive.js';
|
|
28
29
|
import { cssFilterChipsClasses } from './css-filter-chips.js';
|
|
@@ -30,6 +31,8 @@ import { cssGridClasses } from './css-grid.js';
|
|
|
30
31
|
import { cssGridSpanClasses } from './css-grid-span.js';
|
|
31
32
|
import { cssDenominationGridClasses } from './css-denomination-grid.js';
|
|
32
33
|
import { cssFieldClasses } from './css-field.js';
|
|
34
|
+
import { cssStepperClasses } from './css-stepper.js';
|
|
35
|
+
import { cssStepsClasses } from './css-steps.js';
|
|
33
36
|
import { cssToastClasses } from './css-toast.js';
|
|
34
37
|
import { cssBoxBorderClasses } from './css-box-border.js';
|
|
35
38
|
import { cssPopupHostRules } from './stacking.js';
|
|
@@ -43,6 +46,7 @@ function reducedTransparencyRules() {
|
|
|
43
46
|
.sw-card-glass,
|
|
44
47
|
.sw-app-header,
|
|
45
48
|
.sw-action-bar,
|
|
49
|
+
.sw-steps-item,
|
|
46
50
|
.sw-badge-neutral,
|
|
47
51
|
.sw-segmented,
|
|
48
52
|
.sw-table-sticky thead th,
|
|
@@ -151,6 +155,8 @@ ${cssSeparatorClasses()}
|
|
|
151
155
|
|
|
152
156
|
${cssStatTileClasses()}
|
|
153
157
|
|
|
158
|
+
${cssDescriptionListClasses()}
|
|
159
|
+
|
|
154
160
|
${cssTabsClasses()}
|
|
155
161
|
|
|
156
162
|
${cssFilterChipsClasses()}
|
|
@@ -161,6 +167,10 @@ ${cssDenominationGridClasses()}
|
|
|
161
167
|
|
|
162
168
|
${cssFieldClasses()}
|
|
163
169
|
|
|
170
|
+
${cssStepperClasses()}
|
|
171
|
+
|
|
172
|
+
${cssStepsClasses()}
|
|
173
|
+
|
|
164
174
|
${cssSplitClasses()}
|
|
165
175
|
|
|
166
176
|
${cssToastClasses()}
|