@scalewing/react 1.52.0 → 1.54.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 (43) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +14 -1
  3. package/dist/components/Box.d.ts +26 -0
  4. package/dist/components/Box.js +6 -2
  5. package/dist/components/Checkbox.d.ts +18 -1
  6. package/dist/components/Checkbox.js +15 -5
  7. package/dist/components/Dialog.d.ts +14 -1
  8. package/dist/components/Dialog.js +5 -2
  9. package/dist/components/PasteRows.d.ts +55 -0
  10. package/dist/components/PasteRows.js +55 -0
  11. package/dist/components/Stepper.d.ts +2 -66
  12. package/dist/components/Stepper.js +18 -5
  13. package/dist/components/Table.d.ts +1 -0
  14. package/dist/components/Table.js +1 -0
  15. package/dist/components/box/external-link.d.ts +20 -0
  16. package/dist/components/box/external-link.js +37 -0
  17. package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
  18. package/dist/components/checkbox/CheckboxControl.js +4 -3
  19. package/dist/components/dialog/validate.d.ts +3 -1
  20. package/dist/components/dialog/validate.js +3 -1
  21. package/dist/components/paste-rows/parse-pasted-rows.d.ts +26 -0
  22. package/dist/components/paste-rows/parse-pasted-rows.js +123 -0
  23. package/dist/components/stepper/stepper-props.d.ts +78 -0
  24. package/dist/components/stepper/stepper-props.js +1 -0
  25. package/dist/components/stepper/use-stepper-commit.d.ts +22 -0
  26. package/dist/components/stepper/use-stepper-commit.js +66 -0
  27. package/dist/components/table/TableSelectCell.d.ts +26 -0
  28. package/dist/components/table/TableSelectCell.js +19 -0
  29. package/dist/css/css-box-link.d.ts +3 -0
  30. package/dist/css/css-box-link.js +41 -2
  31. package/dist/css/css-checkbox.js +37 -3
  32. package/dist/css/css-components.js +3 -0
  33. package/dist/css/css-dialog-end.d.ts +13 -0
  34. package/dist/css/css-dialog-end.js +75 -0
  35. package/dist/css/css-select.js +10 -0
  36. package/dist/css/css-table-select.d.ts +3 -0
  37. package/dist/css/css-table-select.js +24 -0
  38. package/dist/css/css-table.js +4 -0
  39. package/dist/css/stylesheet.js +2 -0
  40. package/dist/index.d.ts +4 -2
  41. package/dist/index.js +4 -2
  42. package/dist/styles.css +128 -3
  43. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { assertTableLayout, assertTableStickyHeader, assertTableVerticalAlign, }
6
6
  import { assertTableMaxHeight, tableMaxHeightStyle, } from './table/table-max-height.js';
7
7
  export { TableCell, } from './table/TableCell.js';
8
8
  export { TableRow, } from './table/TableRow.js';
9
+ export { TableSelectCell, } from './table/TableSelectCell.js';
9
10
  export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
10
11
  /**
11
12
  * A wide table scrolls inside its own `ScrollRegion`, named after the
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ /** What a `Box` passes to check and draw its external link. */
3
+ export type BoxExternalOptions = {
4
+ as: string;
5
+ external: boolean | undefined;
6
+ newTabLabel: string | undefined;
7
+ };
8
+ /**
9
+ * Refuses an external box that is not a link or has no words for its new
10
+ * tab, and new-tab words on a box that is not external.
11
+ */
12
+ export declare function assertBoxExternal({ as, external, newTabLabel, }: BoxExternalOptions): void;
13
+ /** The consumer's `rel` words with `noopener noreferrer`, each once. */
14
+ export declare function externalLinkRel(rel: string | undefined): string;
15
+ /**
16
+ * An external link's content: its words, the decorative new-tab mark
17
+ * right after them (no space, so it never wraps alone) and the hidden
18
+ * words read after the name, such as "(opens in a new tab)".
19
+ */
20
+ export declare function externalLinkContent(children: ReactNode, newTabLabel: string): ReactNode;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { externalLinkMarkClass } from '../../css/css-box-link.js';
3
+ /** The `rel` words a link that opens a new tab always carries. */
4
+ const newTabRelWords = ['noopener', 'noreferrer'];
5
+ /**
6
+ * Refuses an external box that is not a link or has no words for its new
7
+ * tab, and new-tab words on a box that is not external.
8
+ */
9
+ export function assertBoxExternal({ as, external, newTabLabel, }) {
10
+ if (external === undefined || external === false) {
11
+ if (newTabLabel !== undefined)
12
+ throw new TypeError('Box newTabLabel is for an external link');
13
+ return;
14
+ }
15
+ if (external !== true)
16
+ throw new RangeError('Box external must be a boolean');
17
+ if (as !== 'a')
18
+ throw new TypeError('Box external is for a link (as="a")');
19
+ if (typeof newTabLabel !== 'string' || newTabLabel.trim() === '')
20
+ throw new RangeError('Box external needs a non-empty newTabLabel');
21
+ }
22
+ /** The consumer's `rel` words with `noopener noreferrer`, each once. */
23
+ export function externalLinkRel(rel) {
24
+ const words = (rel ?? '').split(/\s+/).filter((word) => word !== '');
25
+ for (const word of newTabRelWords)
26
+ if (!words.includes(word))
27
+ words.push(word);
28
+ return words.join(' ');
29
+ }
30
+ /**
31
+ * An external link's content: its words, the decorative new-tab mark
32
+ * right after them (no space, so it never wraps alone) and the hidden
33
+ * words read after the name, such as "(opens in a new tab)".
34
+ */
35
+ export function externalLinkContent(children, newTabLabel) {
36
+ return (_jsxs(_Fragment, { children: [children, _jsx("span", { "aria-hidden": "true", className: externalLinkMarkClass }), _jsxs("span", { className: "sw-sr-only", children: [" ", newTabLabel] })] }));
37
+ }
@@ -5,5 +5,5 @@ type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'classNa
5
5
  * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
6
6
  * and MultiSelect's rows so the two cannot drift apart.
7
7
  */
8
- export declare function CheckboxControl({ tabIndex, ...props }: CheckboxControlProps): import("react").JSX.Element;
8
+ export declare const CheckboxControl: import("react").ForwardRefExoticComponent<CheckboxControlProps & import("react").RefAttributes<HTMLInputElement>>;
9
9
  export {};
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
2
3
  import { tabStop } from '../../tab-stop.js';
3
4
  /**
4
5
  * The native checkbox and its drawn mark, the one markup Checkbox's
5
6
  * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
6
7
  * and MultiSelect's rows so the two cannot drift apart.
7
8
  */
8
- export function CheckboxControl({ tabIndex, ...props }) {
9
- return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
10
- }
9
+ export const CheckboxControl = forwardRef(function CheckboxControl({ tabIndex, ...props }, ref) {
10
+ return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ref: ref, tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
11
+ });
@@ -1,5 +1,6 @@
1
1
  import { type Breakpoint } from '../../css/breakpoints.js';
2
2
  import { type DialogAppearance } from '../../css/css-dialog-banded.js';
3
+ import { type DialogPlacement } from '../../css/css-dialog-end.js';
3
4
  import { type ThemeCanvas } from '../../css/css-canvas-grouped.js';
4
5
  import { type ActionBarSummary } from '../action-bar/ActionBarSummary.js';
5
6
  export declare const dialogSizes: readonly ["md", "lg", "xl"];
@@ -20,5 +21,6 @@ export type DialogChecks = {
20
21
  canvas: ThemeCanvas;
21
22
  sheetBelow: Breakpoint | undefined;
22
23
  sheetGrabber: boolean;
24
+ placement: DialogPlacement;
23
25
  };
24
- export declare function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
26
+ export declare function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
@@ -1,4 +1,5 @@
1
1
  import { dialogAppearances, } from '../../css/css-dialog-banded.js';
2
+ import { dialogPlacements, } from '../../css/css-dialog-end.js';
2
3
  import { themeCanvases, } from '../../css/css-canvas-grouped.js';
3
4
  import { assertActionBarSummary, } from '../action-bar/ActionBarSummary.js';
4
5
  export const dialogSizes = ['md', 'lg', 'xl'];
@@ -7,10 +8,11 @@ function oneOf(values, value, prop) {
7
8
  if (!values.includes(value))
8
9
  throw new RangeError(`Unknown Dialog ${prop}: ${String(value)}`);
9
10
  }
10
- export function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }) {
11
+ export function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }) {
11
12
  oneOf(dialogSizes, size, 'size');
12
13
  oneOf(dialogAppearances, appearance, 'appearance');
13
14
  oneOf(themeCanvases, canvas, 'canvas');
15
+ oneOf(dialogPlacements, placement, 'placement');
14
16
  if (role !== undefined)
15
17
  oneOf(dialogRoles, role, 'role');
16
18
  if (description !== undefined &&
@@ -0,0 +1,26 @@
1
+ /**
2
+ * How pasted cells are split: `tab` (what a spreadsheet copies), `comma`
3
+ * (a CSV line), or `auto`, tab when the text holds a tab and comma
4
+ * otherwise. A list whose one column may hold commas ("Smith, Jones") is
5
+ * parsed with `tab`, so a comma never splits it.
6
+ */
7
+ export type PastedRowsDelimiter = 'auto' | 'tab' | 'comma';
8
+ export declare const pastedRowsDelimiters: readonly PastedRowsDelimiter[];
9
+ export type PastedRows = {
10
+ /** The rows in order, each padded with `''` to `columnCount` cells. */
11
+ rows: string[][];
12
+ /** The most cells any row has; 0 when nothing but blank lines was pasted. */
13
+ columnCount: number;
14
+ /** The delimiter used, `auto` resolved. */
15
+ delimiter: 'tab' | 'comma';
16
+ };
17
+ /**
18
+ * Splits pasted text into rows of trimmed cells: rows on line breaks
19
+ * (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
20
+ * double quote and has a closing one later is quoted, as spreadsheets and
21
+ * CSV write a cell holding the delimiter, a line break or a quote; any
22
+ * other quote is a plain character. Rows whose cells are all blank are
23
+ * left out. It knows nothing of what the columns mean: the consumer maps
24
+ * and checks them. Linear in the text's length.
25
+ */
26
+ export declare function parsePastedRows(text: string, delimiter?: PastedRowsDelimiter): PastedRows;
@@ -0,0 +1,123 @@
1
+ export const pastedRowsDelimiters = [
2
+ 'auto',
3
+ 'tab',
4
+ 'comma',
5
+ ];
6
+ function resolveDelimiter(text, delimiter) {
7
+ if (!pastedRowsDelimiters.includes(delimiter))
8
+ throw new RangeError(`Unknown PasteRows delimiter: ${String(delimiter)}`);
9
+ if (delimiter !== 'auto')
10
+ return delimiter;
11
+ return text.includes('\t') ? 'tab' : 'comma';
12
+ }
13
+ /**
14
+ * Reads one quoted cell starting at the opening quote `start`: `""` is
15
+ * one quote, and delimiters and line breaks inside it are its words.
16
+ * Returns the words and the index after the closing quote, or `null` when
17
+ * no lone closing quote follows (the opening quote is then a character).
18
+ */
19
+ function readQuoted(text, start) {
20
+ let words = '';
21
+ let index = start + 1;
22
+ while (index < text.length) {
23
+ const char = text[index];
24
+ if (char === '"') {
25
+ if (text[index + 1] === '"') {
26
+ words += '"';
27
+ index += 2;
28
+ continue;
29
+ }
30
+ return [words, index + 1];
31
+ }
32
+ words += char;
33
+ index += 1;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * For each index, where a quoted cell's scan starting there would find its
39
+ * lone closing quote (`""` being one escaped quote), or -1 when none
40
+ * follows; one extra entry for the end. Built backwards in one pass, so
41
+ * each opening quote is checked in constant time and the parse stays
42
+ * linear: a run of quotes from i of odd length closes on its last quote,
43
+ * an even run is all escapes and the scan carries on after it.
44
+ */
45
+ function closingQuoteTable(text) {
46
+ const closesFrom = new Int32Array(text.length + 1).fill(-1);
47
+ const runFrom = new Int32Array(text.length + 1);
48
+ for (let index = text.length - 1; index >= 0; index -= 1) {
49
+ if (text[index] !== '"') {
50
+ closesFrom[index] = closesFrom[index + 1];
51
+ continue;
52
+ }
53
+ const run = 1 + runFrom[index + 1];
54
+ runFrom[index] = run;
55
+ closesFrom[index] =
56
+ run % 2 === 1 ? index + run - 1 : closesFrom[index + run];
57
+ }
58
+ return closesFrom;
59
+ }
60
+ /**
61
+ * Splits pasted text into rows of trimmed cells: rows on line breaks
62
+ * (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
63
+ * double quote and has a closing one later is quoted, as spreadsheets and
64
+ * CSV write a cell holding the delimiter, a line break or a quote; any
65
+ * other quote is a plain character. Rows whose cells are all blank are
66
+ * left out. It knows nothing of what the columns mean: the consumer maps
67
+ * and checks them. Linear in the text's length.
68
+ */
69
+ export function parsePastedRows(text, delimiter = 'auto') {
70
+ const used = resolveDelimiter(text, delimiter);
71
+ const separator = used === 'tab' ? '\t' : ',';
72
+ const closesFrom = closingQuoteTable(text);
73
+ const rows = [];
74
+ let row = [];
75
+ let cell = '';
76
+ let cellStart = true;
77
+ const endCell = () => {
78
+ row.push(cell.trim());
79
+ cell = '';
80
+ cellStart = true;
81
+ };
82
+ const endRow = () => {
83
+ endCell();
84
+ if (row.some((value) => value !== ''))
85
+ rows.push(row);
86
+ row = [];
87
+ };
88
+ let index = 0;
89
+ while (index < text.length) {
90
+ const char = text[index];
91
+ const quoted = cellStart && char === '"' && closesFrom[index + 1] >= 0
92
+ ? readQuoted(text, index)
93
+ : null;
94
+ if (quoted) {
95
+ cell += quoted[0];
96
+ index = quoted[1];
97
+ cellStart = false;
98
+ }
99
+ else if (char === separator) {
100
+ endCell();
101
+ index += 1;
102
+ }
103
+ else if (char === '\r' || char === '\n') {
104
+ endRow();
105
+ index += char === '\r' && text[index + 1] === '\n' ? 2 : 1;
106
+ }
107
+ else {
108
+ // Spaces before a quoted cell's opening quote do not unquote it.
109
+ if (!(cellStart && char === ' ')) {
110
+ cell += char;
111
+ cellStart = false;
112
+ }
113
+ index += 1;
114
+ }
115
+ }
116
+ if (cell !== '' || row.length > 0)
117
+ endRow();
118
+ const columnCount = rows.reduce((most, cells) => Math.max(most, cells.length), 0);
119
+ for (const cells of rows)
120
+ while (cells.length < columnCount)
121
+ cells.push('');
122
+ return { rows, columnCount, delimiter: used };
123
+ }
@@ -0,0 +1,78 @@
1
+ /** `fixed` sizes the count for a few digits; `full` fills its container, such as a table cell. */
2
+ export type StepperWidth = 'fixed' | 'full';
3
+ export declare const stepperLimitAppearances: readonly ["faded", "plain"];
4
+ /**
5
+ * How − looks at `min` and + at `max`, where a press cannot move the count:
6
+ * `faded` (the default) fades the button as any disabled control; `plain`
7
+ * keeps its usual look. Either way the button is disabled.
8
+ */
9
+ export type StepperLimitAppearance = (typeof stepperLimitAppearances)[number];
10
+ export type StepperProps = {
11
+ /** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
12
+ label: string;
13
+ labelVisuallyHidden?: boolean;
14
+ /** The count, or `null` for an empty box (none). */
15
+ value: number | null;
16
+ /** A whole count, or `null` when the box is emptied. Typed counts are reported as typed, even outside the bounds. */
17
+ onChange: (value: number | null) => void;
18
+ /**
19
+ * The finished count, once per entry, for a page that saves as you go:
20
+ * on blur, on Enter, and `commitDelay` ms after the last − / + press or
21
+ * step key (ArrowUp, ArrowDown, Home, End). Never per typed digit, and
22
+ * never when the count is the one last committed (or held before the
23
+ * first edit). It gives the count you hold then, `null` or outside the
24
+ * bounds included, as `onChange` reports typed counts. `onChange` is
25
+ * unchanged.
26
+ */
27
+ onCommit?: (value: number | null) => void;
28
+ /** How long − / + presses and step keys settle before `onCommit`, in ms; defaults to 600. */
29
+ commitDelay?: number;
30
+ /** The lowest count − reaches; defaults to 0. */
31
+ min?: number;
32
+ /** The highest count + reaches; no upper bound when left out. */
33
+ max?: number;
34
+ /** How much one press, or one arrow key, moves the count; defaults to 1. */
35
+ step?: number;
36
+ /** The − button's name, such as "One less". */
37
+ decrementLabel: string;
38
+ /** The + button's name, such as "One more". */
39
+ incrementLabel: string;
40
+ /**
41
+ * A message under the control, as `Field`'s: a polite live region linked by
42
+ * `aria-describedby`, with `aria-invalid` and the danger border. An empty
43
+ * string is no error.
44
+ */
45
+ error?: string;
46
+ /** `Field`'s changed look (the accent border and tint) for a count someone changed, or a suggestion. `error` wins. */
47
+ changed?: boolean;
48
+ disabled?: boolean;
49
+ /** Readable and focusable, never changed: the buttons are hidden and keep their room. */
50
+ readOnly?: boolean;
51
+ /**
52
+ * Says the count in words for assistive technology (`aria-valuetext`),
53
+ * such as "12 straps". It wins over the count and `unit` read together.
54
+ */
55
+ valueText?: string;
56
+ /**
57
+ * A short muted word after the count, inside its frame, such as "min".
58
+ * Without `valueText` the count is read with it ("90 min"), word for
59
+ * word: a unit that changes with the count ("players") reads "1 players",
60
+ * so pass `valueText` with the right form ("1 player"). Not blank.
61
+ */
62
+ unit?: string;
63
+ /**
64
+ * Ids of elements that describe the count, such as the value it adds up
65
+ * to; read before an `error`'s message.
66
+ */
67
+ 'aria-describedby'?: string;
68
+ /** Shown in an empty box, such as "0". */
69
+ placeholder?: string;
70
+ name?: string;
71
+ width?: StepperWidth;
72
+ /**
73
+ * `plain` keeps − at `min` and + at `max` in their usual look instead of
74
+ * fading them (`faded`, the default). The button is still disabled, and a
75
+ * disabled or read-only Stepper is unchanged.
76
+ */
77
+ limitAppearance?: StepperLimitAppearance;
78
+ };
@@ -0,0 +1 @@
1
+ export const stepperLimitAppearances = ['faded', 'plain'];
@@ -0,0 +1,22 @@
1
+ /** How long after the last − / + press or arrow key a count commits, in ms. */
2
+ export declare const defaultStepperCommitDelay = 600;
3
+ /** Refuses a commit delay that is not a finite number of zero or more ms. */
4
+ export declare function assertStepperCommitDelay(delay: number): void;
5
+ /** What a Stepper calls as its count is edited and finished. */
6
+ export type StepperCommit = {
7
+ /** A typed change: it waits for blur or Enter. */
8
+ edited: () => void;
9
+ /** A − / + press or a step key: it commits once the presses settle. */
10
+ stepped: () => void;
11
+ /** Blur or Enter: commits now. */
12
+ finish: () => void;
13
+ };
14
+ /**
15
+ * Turns a Stepper's edits into one `onCommit` per finished entry: on blur,
16
+ * on Enter, and `delay` ms after the last − / + press or step key. A
17
+ * commit gives the count the consumer holds then, and only when it differs
18
+ * from the last committed count (or the count before the first edit). A
19
+ * count the consumer changes while nothing is waiting becomes the new
20
+ * baseline. A commit still waiting when the Stepper unmounts is made then.
21
+ */
22
+ export declare function useStepperCommit(value: number | null, onCommit: ((value: number | null) => void) | undefined, delay: number): StepperCommit;
@@ -0,0 +1,66 @@
1
+ import { useEffect, useRef } from 'react';
2
+ /** How long after the last − / + press or arrow key a count commits, in ms. */
3
+ export const defaultStepperCommitDelay = 600;
4
+ /** Refuses a commit delay that is not a finite number of zero or more ms. */
5
+ export function assertStepperCommitDelay(delay) {
6
+ if (!Number.isFinite(delay) || delay < 0)
7
+ throw new RangeError('Stepper commitDelay must be a finite number of zero or more');
8
+ }
9
+ /**
10
+ * Turns a Stepper's edits into one `onCommit` per finished entry: on blur,
11
+ * on Enter, and `delay` ms after the last − / + press or step key. A
12
+ * commit gives the count the consumer holds then, and only when it differs
13
+ * from the last committed count (or the count before the first edit). A
14
+ * count the consumer changes while nothing is waiting becomes the new
15
+ * baseline. A commit still waiting when the Stepper unmounts is made then.
16
+ */
17
+ export function useStepperCommit(value, onCommit, delay) {
18
+ const latest = useRef({ value, onCommit });
19
+ const baseline = useRef(value);
20
+ const pending = useRef(false);
21
+ const timer = useRef(null);
22
+ useEffect(() => {
23
+ latest.current = { value, onCommit };
24
+ if (!pending.current)
25
+ baseline.current = value;
26
+ }, [value, onCommit]);
27
+ function clearTimer() {
28
+ if (timer.current === null)
29
+ return;
30
+ clearTimeout(timer.current);
31
+ timer.current = null;
32
+ }
33
+ function finish() {
34
+ clearTimer();
35
+ if (!pending.current)
36
+ return;
37
+ pending.current = false;
38
+ const { value: current, onCommit: commit } = latest.current;
39
+ if (current === baseline.current)
40
+ return;
41
+ baseline.current = current;
42
+ commit?.(current);
43
+ }
44
+ // The latest finish, for the timer and the unmount to call.
45
+ const finishRef = useRef(finish);
46
+ useEffect(() => {
47
+ finishRef.current = finish;
48
+ });
49
+ useEffect(() => () => finishRef.current(), []);
50
+ function edited() {
51
+ if (latest.current.onCommit === undefined)
52
+ return;
53
+ pending.current = true;
54
+ }
55
+ function stepped() {
56
+ if (latest.current.onCommit === undefined)
57
+ return;
58
+ pending.current = true;
59
+ clearTimer();
60
+ timer.current = setTimeout(() => {
61
+ timer.current = null;
62
+ finishRef.current();
63
+ }, delay);
64
+ }
65
+ return { edited, stepped, finish };
66
+ }
@@ -0,0 +1,26 @@
1
+ export type TableSelectCellProps = {
2
+ /**
3
+ * The checkbox's name, not drawn: "Select Mallard" in a row, "Select
4
+ * all" in the header. Name the row by the words the row shows.
5
+ */
6
+ label: string;
7
+ checked: boolean;
8
+ /** The mixed state, for a header's "Select all" while some rows are chosen. */
9
+ indeterminate?: boolean;
10
+ onCheckedChange: (checked: boolean) => void;
11
+ disabled?: boolean;
12
+ /**
13
+ * `th` in a `TableHeader` row (the column's header cell, `scope="col"`),
14
+ * `td` (the default) in a body row. A fixed table sizes its columns from
15
+ * the header row, so put one in the header too.
16
+ */
17
+ as?: 'td' | 'th';
18
+ };
19
+ /**
20
+ * A table's select column: a cell holding one `Checkbox` whose label is
21
+ * visually hidden. The column is as narrow as the box, which is its own
22
+ * target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
23
+ * `TableRow selected`; a press on the box is left alone by a
24
+ * `TableRow pressable`.
25
+ */
26
+ export declare const TableSelectCell: import("react").ForwardRefExoticComponent<TableSelectCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { tableSelectCellClass } from '../../css/css-table-select.js';
4
+ import { Checkbox } from '../Checkbox.js';
5
+ /**
6
+ * A table's select column: a cell holding one `Checkbox` whose label is
7
+ * visually hidden. The column is as narrow as the box, which is its own
8
+ * target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
9
+ * `TableRow selected`; a press on the box is left alone by a
10
+ * `TableRow pressable`.
11
+ */
12
+ export const TableSelectCell = forwardRef(function TableSelectCell({ as = 'td', checked, disabled = false, indeterminate = false, label, onCheckedChange, }, ref) {
13
+ if (typeof label !== 'string' || !label.trim())
14
+ throw new RangeError('TableSelectCell label must not be empty');
15
+ if (as !== 'td' && as !== 'th')
16
+ throw new RangeError(`Unknown TableSelectCell as: ${String(as)}`);
17
+ const Component = as;
18
+ return (_jsx(Component, { ref: ref, className: tableSelectCellClass, scope: as === 'th' ? 'col' : undefined, children: _jsx(Checkbox, { checked: checked, disabled: disabled, indeterminate: indeterminate, label: label, labelVisuallyHidden: true, onCheckedChange: onCheckedChange }) }));
19
+ });
@@ -6,8 +6,11 @@
6
6
  */
7
7
  export declare const boxLinkAppearances: readonly ["accent", "quiet"];
8
8
  export type BoxLinkAppearance = (typeof boxLinkAppearances)[number];
9
+ /** The decorative new-tab mark after an external link's words. */
10
+ export declare const externalLinkMarkClass = "sw-link-external-mark";
9
11
  export declare function assertBoxLink(link: unknown, as: string): void;
10
12
  /** The generated class for a Box `link` prop, or none for the canvas link. */
11
13
  export declare function boxLinkClass(link: BoxLinkAppearance | undefined): string | undefined;
12
14
  export declare function cssBoxLinkClasses(): string;
15
+ export declare function cssExternalLinkMarkClasses(): string;
13
16
  export declare function boxLinkClassCatalog(): string[];
@@ -7,6 +7,8 @@ import { typographyVariants } from '@scalewing/tokens';
7
7
  */
8
8
  export const boxLinkAppearances = ['accent', 'quiet'];
9
9
  const quietLinkClass = 'sw-link-quiet';
10
+ /** The decorative new-tab mark after an external link's words. */
11
+ export const externalLinkMarkClass = 'sw-link-external-mark';
10
12
  export function assertBoxLink(link, as) {
11
13
  if (link === undefined)
12
14
  return;
@@ -43,8 +45,45 @@ ${quiet}:hover,
43
45
  ${quiet}:focus-visible {
44
46
  color: inherit;
45
47
  text-decoration-line: underline;
46
- }`;
48
+ }
49
+
50
+ ${cssExternalLinkMarkClasses()}`;
51
+ }
52
+ /*
53
+ * An external link's new-tab mark: an arrow to the inline end and up (↗),
54
+ * drawn with borders in the link's colour so forced colours keep it as the
55
+ * system link colour. Its head is the top and inline-end borders of a
56
+ * space-2 square; its shaft is one border on the square's diagonal. It
57
+ * follows the link's colour (accent, the hover text colour, a quiet link's
58
+ * inherited one) and is mirrored in right-to-left text. It sits a space-1
59
+ * after the last word with no space between them, so it never wraps alone.
60
+ */
61
+ export function cssExternalLinkMarkClasses() {
62
+ const mark = `.${externalLinkMarkClass}`;
63
+ return `${mark} {
64
+ border-right: 2px solid currentColor;
65
+ border-top: 2px solid currentColor;
66
+ box-sizing: border-box;
67
+ display: inline-block;
68
+ height: var(--sw-space-2);
69
+ margin-inline-start: var(--sw-space-1);
70
+ position: relative;
71
+ vertical-align: middle;
72
+ width: var(--sw-space-2);
73
+ }
74
+
75
+ ${mark}::after {
76
+ border-left: 2px solid currentColor;
77
+ content: '';
78
+ height: calc(var(--sw-space-2) * 1.25);
79
+ left: 50%;
80
+ position: absolute;
81
+ top: 50%;
82
+ transform: translate(-50%, -50%) rotate(45deg);
83
+ }
84
+
85
+ ${mark}:dir(rtl) { transform: scaleX(-1); }`;
47
86
  }
48
87
  export function boxLinkClassCatalog() {
49
- return [quietLinkClass];
88
+ return [quietLinkClass, externalLinkMarkClass];
50
89
  }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { checkStrokeIn } from './chevron.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
4
  const label = typographyVariants.label;
4
5
  const caption = typographyVariants.caption;
5
6
  export function cssCheckboxClasses() {
@@ -68,6 +69,38 @@ export function cssCheckboxClasses() {
68
69
 
69
70
  .sw-checkbox-input:checked + .sw-checkbox-mark::after { display: block; }
70
71
 
72
+ /* The mixed state: the accent fill with a dash in place of the check. */
73
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark {
74
+ background: var(--sw-color-accent);
75
+ border-color: var(--sw-color-accent);
76
+ }
77
+
78
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark::after {
79
+ border-right: 0;
80
+ display: block;
81
+ height: 0;
82
+ inset-block-start: calc(50% - 1px);
83
+ inset-inline-start: calc(50% - var(--sw-space-3) / 2);
84
+ transform: none;
85
+ width: var(--sw-space-3);
86
+ }
87
+
88
+ /* A visually hidden label: the box alone is the target, its own size on a
89
+ fine pointer (so a compact table row keeps its height), the 44 px touch
90
+ target both ways on a coarse one. */
91
+ .sw-checkbox-label-hidden {
92
+ justify-content: center;
93
+ min-height: 0;
94
+ position: relative;
95
+ }
96
+
97
+ @media ${coarsePointerQuery} {
98
+ .sw-checkbox-label-hidden {
99
+ min-height: ${touchTarget};
100
+ min-width: ${touchTarget};
101
+ }
102
+ }
103
+
71
104
  .sw-checkbox-input:focus-visible + .sw-checkbox-mark {
72
105
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
73
106
  outline-offset: var(--sw-focus-ring-offset);
@@ -104,13 +137,13 @@ export function cssCheckboxClasses() {
104
137
 
105
138
  @media (prefers-reduced-transparency: reduce) {
106
139
  .sw-checkbox-mark { background: var(--sw-color-surface); }
107
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: var(--sw-color-accent); }
140
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: var(--sw-color-accent); }
108
141
  }
109
142
 
110
143
  @media (forced-colors: active) {
111
144
  .sw-checkbox-mark { background: Canvas; border-color: CanvasText; }
112
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
113
- .sw-checkbox-input:checked + .sw-checkbox-mark::after { border-color: HighlightText; }
145
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
146
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark::after { border-color: HighlightText; }
114
147
  .sw-checkbox-input[aria-invalid='true'] + .sw-checkbox-mark { border-color: CanvasText; }
115
148
  }`;
116
149
  }
@@ -118,6 +151,7 @@ export function checkboxClassCatalog() {
118
151
  return [
119
152
  'sw-checkbox',
120
153
  'sw-checkbox-label',
154
+ 'sw-checkbox-label-hidden',
121
155
  'sw-checkbox-control',
122
156
  'sw-checkbox-input',
123
157
  'sw-checkbox-mark',
@@ -35,6 +35,7 @@ import { cssDialogBandedClasses } from './css-dialog-banded.js';
35
35
  import { cssDialogFrameClasses } from './css-dialog-frame.js';
36
36
  import { cssDialogCanvasClasses } from './css-dialog-canvas.js';
37
37
  import { cssDialogGrabberClasses } from './css-dialog-grabber.js';
38
+ import { cssDialogEndClasses } from './css-dialog-end.js';
38
39
  import { cssComboboxClasses } from './css-combobox.js';
39
40
  import { cssSelectClasses } from './css-select.js';
40
41
  import { cssMultiSelectClasses } from './css-multi-select.js';
@@ -224,6 +225,8 @@ ${cssDialogCanvasClasses()}
224
225
 
225
226
  ${cssDialogGrabberClasses()}
226
227
 
228
+ ${cssDialogEndClasses()}
229
+
227
230
  ${cssAccordionClasses()}
228
231
 
229
232
  ${cssSelectClasses()}