@scalewing/react 1.26.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 +33 -0
- package/README.md +87 -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/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 +7 -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-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 +4 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/stepper-count.d.ts +37 -0
- package/dist/stepper-count.js +50 -0
- package/dist/styles.css +324 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -31,6 +31,8 @@ import { cssGridClasses } from './css-grid.js';
|
|
|
31
31
|
import { cssGridSpanClasses } from './css-grid-span.js';
|
|
32
32
|
import { cssDenominationGridClasses } from './css-denomination-grid.js';
|
|
33
33
|
import { cssFieldClasses } from './css-field.js';
|
|
34
|
+
import { cssStepperClasses } from './css-stepper.js';
|
|
35
|
+
import { cssStepsClasses } from './css-steps.js';
|
|
34
36
|
import { cssToastClasses } from './css-toast.js';
|
|
35
37
|
import { cssBoxBorderClasses } from './css-box-border.js';
|
|
36
38
|
import { cssPopupHostRules } from './stacking.js';
|
|
@@ -44,6 +46,7 @@ function reducedTransparencyRules() {
|
|
|
44
46
|
.sw-card-glass,
|
|
45
47
|
.sw-app-header,
|
|
46
48
|
.sw-action-bar,
|
|
49
|
+
.sw-steps-item,
|
|
47
50
|
.sw-badge-neutral,
|
|
48
51
|
.sw-segmented,
|
|
49
52
|
.sw-table-sticky thead th,
|
|
@@ -164,6 +167,10 @@ ${cssDenominationGridClasses()}
|
|
|
164
167
|
|
|
165
168
|
${cssFieldClasses()}
|
|
166
169
|
|
|
170
|
+
${cssStepperClasses()}
|
|
171
|
+
|
|
172
|
+
${cssStepsClasses()}
|
|
173
|
+
|
|
167
174
|
${cssSplitClasses()}
|
|
168
175
|
|
|
169
176
|
${cssToastClasses()}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
const caption = typographyVariants.caption;
|
|
3
|
+
/** An enabled entry count, unless its Stepper shows an error (whose danger border wins). */
|
|
4
|
+
const tonedEntryInput = '[data-theme] .sw-denomination-cell-toned .sw-stepper:not(.sw-stepper-invalid) input.sw-stepper-input:not(:disabled)';
|
|
5
|
+
/*
|
|
6
|
+
* Count entry and the labels around it. An entry cell holds a full-width
|
|
7
|
+
* Stepper; its count keeps spacing step 8 (room for three digits), so a narrow strip scrolls
|
|
8
|
+
* in its region instead of crushing the counts. A cell tone draws the
|
|
9
|
+
* count's border in the tone, a hairline thicker through an inset shadow so
|
|
10
|
+
* nothing moves. A caption is a muted caption-size line: under a row label,
|
|
11
|
+
* or after a column label. A quiet row sits on the subtle fill, its pinned
|
|
12
|
+
* label too. A disabled row fades its label, counts and total (its entry is
|
|
13
|
+
* disabled by its Stepper); a pinned label fades its contents, not its
|
|
14
|
+
* fill, so counts never show through it.
|
|
15
|
+
*/
|
|
16
|
+
export function cssDenominationEntryClasses() {
|
|
17
|
+
return `.sw-denomination-strip td.sw-denomination-entry {
|
|
18
|
+
padding: var(--sw-space-1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.sw-denomination-entry .sw-stepper-full .sw-stepper-track {
|
|
22
|
+
grid-template-columns: var(--sw-space-6) minmax(var(--sw-space-8), 1fr) var(--sw-space-6);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* A strip with entry rows centres its heads and read-only counts over the steppers. */
|
|
26
|
+
.sw-denomination-strip-entry .sw-denomination-head:not(.sw-denomination-total-head),
|
|
27
|
+
.sw-denomination-strip-entry .sw-denomination-cell {
|
|
28
|
+
text-align: center;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
${tonedEntryInput} {
|
|
32
|
+
border-color: var(--sw-denomination-cell-tone);
|
|
33
|
+
box-shadow: inset 0 0 0 1px var(--sw-denomination-cell-tone);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sw-denomination-label-words {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.sw-denomination-label-caption,
|
|
43
|
+
.sw-denomination-head-caption {
|
|
44
|
+
color: var(--sw-color-muted);
|
|
45
|
+
font-size: ${caption.fontSize}px;
|
|
46
|
+
font-weight: ${caption.fontWeight};
|
|
47
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
48
|
+
line-height: ${caption.lineHeight}px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sw-denomination-row-quiet,
|
|
52
|
+
.sw-denomination-strip .sw-denomination-row-quiet .sw-denomination-label {
|
|
53
|
+
background: var(--sw-color-subtle);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-label-body,
|
|
57
|
+
.sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-cell,
|
|
58
|
+
.sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-total {
|
|
59
|
+
opacity: var(--sw-disabled-opacity);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media (forced-colors: active) {
|
|
63
|
+
${tonedEntryInput} { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
|
|
64
|
+
.sw-denomination-label-caption,
|
|
65
|
+
.sw-denomination-head-caption { color: CanvasText; }
|
|
66
|
+
}`;
|
|
67
|
+
}
|
|
68
|
+
export function denominationEntryClassCatalog() {
|
|
69
|
+
return [
|
|
70
|
+
'sw-denomination-entry',
|
|
71
|
+
'sw-denomination-strip-entry',
|
|
72
|
+
'sw-denomination-label-words',
|
|
73
|
+
'sw-denomination-label-caption',
|
|
74
|
+
'sw-denomination-head-caption',
|
|
75
|
+
'sw-denomination-row-quiet',
|
|
76
|
+
'sw-denomination-row-disabled',
|
|
77
|
+
];
|
|
78
|
+
}
|
|
@@ -2,6 +2,7 @@ import { typographyVariants } from '@scalewing/tokens';
|
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
3
|
import { badgeTones } from './css-data.js';
|
|
4
4
|
import { cssDenominationAlignedClasses, denominationAlignedClassCatalog, } from './css-denomination-aligned.js';
|
|
5
|
+
import { cssDenominationEntryClasses, denominationEntryClassCatalog, } from './css-denomination-entry.js';
|
|
5
6
|
import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
|
|
6
7
|
import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
|
|
7
8
|
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
@@ -249,6 +250,8 @@ ${pinnedStartShadeRules('.sw-denomination-scroll', [
|
|
|
249
250
|
|
|
250
251
|
${cssDenominationCellToneClasses()}
|
|
251
252
|
|
|
253
|
+
${cssDenominationEntryClasses()}
|
|
254
|
+
|
|
252
255
|
${cssDenominationAlignedClasses()}
|
|
253
256
|
|
|
254
257
|
@media (forced-colors: active) {
|
|
@@ -290,6 +293,7 @@ export function denominationGridClassCatalog() {
|
|
|
290
293
|
'sw-denomination-tile',
|
|
291
294
|
'sw-denomination-subtotal',
|
|
292
295
|
...denominationCellToneClassCatalog(),
|
|
296
|
+
...denominationEntryClassCatalog(),
|
|
293
297
|
...denominationAlignedClassCatalog(),
|
|
294
298
|
];
|
|
295
299
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web Stepper: a label over a track of − count +. The count is a few
|
|
3
|
+
* digits wide by default, the track at its own width even under a wider label
|
|
4
|
+
* or message (`5ch` plus its padding, so its width never follows
|
|
5
|
+
* its value), or fills its container with `sw-stepper-full`. The buttons are
|
|
6
|
+
* spacing step 6 wide on the subtle fill and every part is the `md` control
|
|
7
|
+
* height.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cssStepperClasses(): string;
|
|
10
|
+
export declare function stepperClassCatalog(): string[];
|