@scalewing/react 1.51.0 → 1.53.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 +20 -0
- package/README.md +14 -2
- package/dist/components/Checkbox.d.ts +18 -1
- package/dist/components/Checkbox.js +15 -5
- package/dist/components/Dialog.d.ts +14 -1
- package/dist/components/Dialog.js +5 -2
- package/dist/components/PasteRows.d.ts +55 -0
- package/dist/components/PasteRows.js +55 -0
- package/dist/components/SiteHeader.d.ts +6 -3
- package/dist/components/SiteHeader.js +8 -4
- package/dist/components/Table.d.ts +1 -0
- package/dist/components/Table.js +1 -0
- package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
- package/dist/components/checkbox/CheckboxControl.js +4 -3
- package/dist/components/dialog/validate.d.ts +3 -1
- package/dist/components/dialog/validate.js +3 -1
- package/dist/components/paste-rows/parse-pasted-rows.d.ts +26 -0
- package/dist/components/paste-rows/parse-pasted-rows.js +123 -0
- package/dist/components/site-header/types.d.ts +16 -1
- package/dist/components/site-header/validate.d.ts +4 -3
- package/dist/components/site-header/validate.js +12 -3
- package/dist/components/table/TableSelectCell.d.ts +26 -0
- package/dist/components/table/TableSelectCell.js +19 -0
- package/dist/css/css-checkbox.js +37 -3
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-dialog-end.d.ts +13 -0
- package/dist/css/css-dialog-end.js +75 -0
- package/dist/css/css-select.js +10 -0
- package/dist/css/css-site-footer.js +11 -1
- package/dist/css/css-site-header-anchors.d.ts +1 -0
- package/dist/css/css-site-header-anchors.js +22 -0
- package/dist/css/css-site-header-skip-link.d.ts +1 -0
- package/dist/css/css-site-header-skip-link.js +49 -0
- package/dist/css/css-site-header.js +26 -11
- package/dist/css/css-table-select.d.ts +3 -0
- package/dist/css/css-table-select.js +24 -0
- package/dist/css/css-table.js +4 -0
- package/dist/css/site-header-geometry.d.ts +21 -0
- package/dist/css/site-header-geometry.js +28 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/styles.css +165 -3
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -15,6 +15,16 @@ export type SiteHeaderLabels = {
|
|
|
15
15
|
/** The open menu button's name, such as "Close the menu". */
|
|
16
16
|
closeMenu: string;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* A "skip to content" link, the header's first focusable element: hidden
|
|
20
|
+
* until it has focus, then shown at the bar's start.
|
|
21
|
+
*/
|
|
22
|
+
export type SiteHeaderSkipLink = {
|
|
23
|
+
/** The link's words, such as "Skip to content". */
|
|
24
|
+
label: string;
|
|
25
|
+
/** The main content's fragment, such as `#main`. */
|
|
26
|
+
href: string;
|
|
27
|
+
};
|
|
18
28
|
export type SiteHeaderProps = {
|
|
19
29
|
/** The site's mark at the start, such as a logo linked to the home page. */
|
|
20
30
|
brand: ReactNode;
|
|
@@ -24,7 +34,10 @@ export type SiteHeaderProps = {
|
|
|
24
34
|
items: readonly SiteHeaderItem[];
|
|
25
35
|
/** The bar's end from `lg`, such as a language switch, Sign in and a ButtonLink. */
|
|
26
36
|
actions?: ReactNode;
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Beside the menu button below `lg`, such as the language switch. They
|
|
39
|
+
* step aside while the menu is open, which holds its own `menuActions`.
|
|
40
|
+
*/
|
|
28
41
|
compactActions?: ReactNode;
|
|
29
42
|
/** In the open menu under its links, such as full-width ButtonLinks. */
|
|
30
43
|
menuActions?: ReactNode;
|
|
@@ -35,4 +48,6 @@ export type SiteHeaderProps = {
|
|
|
35
48
|
closeIcon: ReactNode;
|
|
36
49
|
/** Whether the bar sticks to the window's top as the page scrolls (default `true`). */
|
|
37
50
|
sticky?: boolean;
|
|
51
|
+
/** A "skip to content" link before everything else in the header. */
|
|
52
|
+
skipLink?: SiteHeaderSkipLink;
|
|
38
53
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type SiteHeaderProps } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
-
* repeated ids, blank labels or hrefs,
|
|
5
|
-
* button name
|
|
4
|
+
* repeated ids, blank labels or hrefs, a blank navigation name or menu
|
|
5
|
+
* button name, and a skip link with blank words or an address that is not
|
|
6
|
+
* a fragment on the page.
|
|
6
7
|
*/
|
|
7
|
-
export declare function assertSiteHeaderProps({ items, labels, navLabel, }: Pick<SiteHeaderProps, 'items' | 'labels' | 'navLabel'>): void;
|
|
8
|
+
export declare function assertSiteHeaderProps({ items, labels, navLabel, skipLink, }: Pick<SiteHeaderProps, 'items' | 'labels' | 'navLabel' | 'skipLink'>): void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { assertNavItems } from '../nav-items.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
-
* repeated ids, blank labels or hrefs,
|
|
5
|
-
* button name
|
|
4
|
+
* repeated ids, blank labels or hrefs, a blank navigation name or menu
|
|
5
|
+
* button name, and a skip link with blank words or an address that is not
|
|
6
|
+
* a fragment on the page.
|
|
6
7
|
*/
|
|
7
|
-
export function assertSiteHeaderProps({ items, labels, navLabel, }) {
|
|
8
|
+
export function assertSiteHeaderProps({ items, labels, navLabel, skipLink, }) {
|
|
8
9
|
if (items.length === 0)
|
|
9
10
|
throw new RangeError('SiteHeader needs at least one item');
|
|
10
11
|
assertNavItems('SiteHeader', items);
|
|
@@ -14,4 +15,12 @@ export function assertSiteHeaderProps({ items, labels, navLabel, }) {
|
|
|
14
15
|
throw new RangeError('SiteHeader needs a nonempty navLabel');
|
|
15
16
|
if (!labels.openMenu.trim() || !labels.closeMenu.trim())
|
|
16
17
|
throw new RangeError('SiteHeader labels need nonempty openMenu and closeMenu');
|
|
18
|
+
if (skipLink)
|
|
19
|
+
assertSkipLink(skipLink);
|
|
20
|
+
}
|
|
21
|
+
function assertSkipLink({ href, label }) {
|
|
22
|
+
if (!label.trim())
|
|
23
|
+
throw new RangeError('SiteHeader skipLink needs a nonempty label');
|
|
24
|
+
if (!/^#\S+$/.test(href))
|
|
25
|
+
throw new RangeError('SiteHeader skipLink href must be a fragment such as #main');
|
|
17
26
|
}
|
|
@@ -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
|
+
});
|
package/dist/css/css-checkbox.js
CHANGED
|
@@ -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()}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Breakpoint } from './breakpoints.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a Dialog sits from the layout breakpoint up: `center` (the
|
|
4
|
+
* default) in the middle of the window, `end` as a side panel docked to
|
|
5
|
+
* the inline end (the right, the left in right-to-left).
|
|
6
|
+
*/
|
|
7
|
+
export declare const dialogPlacements: readonly ["center", "end"];
|
|
8
|
+
export type DialogPlacement = (typeof dialogPlacements)[number];
|
|
9
|
+
export declare const dialogEndClass = "sw-dialog-end";
|
|
10
|
+
/** The side panel takes its place from this breakpoint up. */
|
|
11
|
+
export declare const dialogEndFrom: Breakpoint;
|
|
12
|
+
export declare function cssDialogEndClasses(): string;
|
|
13
|
+
export declare function dialogEndClassCatalog(): string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a Dialog sits from the layout breakpoint up: `center` (the
|
|
4
|
+
* default) in the middle of the window, `end` as a side panel docked to
|
|
5
|
+
* the inline end (the right, the left in right-to-left).
|
|
6
|
+
*/
|
|
7
|
+
export const dialogPlacements = ['center', 'end'];
|
|
8
|
+
export const dialogEndClass = 'sw-dialog-end';
|
|
9
|
+
/** The side panel takes its place from this breakpoint up. */
|
|
10
|
+
export const dialogEndFrom = 'md';
|
|
11
|
+
const end = `.${dialogEndClass}`;
|
|
12
|
+
/*
|
|
13
|
+
* From md up, `placement="end"` is a side panel: the window's full height
|
|
14
|
+
* at the inline end, square corners, one hairline on its inline-start
|
|
15
|
+
* edge, the size's width (md 32rem, lg 56rem, xl 65rem) with a space-8
|
|
16
|
+
* strip of backdrop always in view. Its column grows to the full height so
|
|
17
|
+
* the pinned footer sits on the bottom edge even when the content is short;
|
|
18
|
+
* longer content scrolls inside the panel under the sticky footer, as in
|
|
19
|
+
* the centred dialog. It slides in from its edge unless reduced motion is
|
|
20
|
+
* asked for. Below md the class draws nothing: the dialog is centred, or a
|
|
21
|
+
* bottom sheet with `sheetBelow`. Emitted after the banded and grabber
|
|
22
|
+
* rules so its edges win at equal specificity.
|
|
23
|
+
*/
|
|
24
|
+
export function cssDialogEndClasses() {
|
|
25
|
+
return `@media ${breakpointQuery('from', dialogEndFrom)} {
|
|
26
|
+
${end} {
|
|
27
|
+
border-color: var(--sw-color-border);
|
|
28
|
+
border-radius: 0;
|
|
29
|
+
border-width: 0;
|
|
30
|
+
border-inline-start-width: 1px;
|
|
31
|
+
height: 100%;
|
|
32
|
+
inset-block: 0;
|
|
33
|
+
margin: 0;
|
|
34
|
+
margin-inline-start: auto;
|
|
35
|
+
max-height: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${end}[open] {
|
|
39
|
+
animation: sw-dialog-end-in var(--sw-motion-default) var(--sw-motion-easing);
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
${end}[open]:dir(rtl) {
|
|
45
|
+
animation-name: sw-dialog-end-in-rtl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
${end} > .sw-stack {
|
|
49
|
+
flex: 1 0 auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
${end} .sw-dialog-footer {
|
|
53
|
+
margin-top: auto;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes sw-dialog-end-in {
|
|
58
|
+
from { translate: 100% 0; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@keyframes sw-dialog-end-in-rtl {
|
|
62
|
+
from { translate: -100% 0; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
${end}[open] { animation: none; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
${end} { border-inline-start-color: CanvasText; }
|
|
71
|
+
}`;
|
|
72
|
+
}
|
|
73
|
+
export function dialogEndClassCatalog() {
|
|
74
|
+
return [dialogEndClass];
|
|
75
|
+
}
|
package/dist/css/css-select.js
CHANGED
|
@@ -2,6 +2,7 @@ import { typographyVariants } from '@scalewing/tokens';
|
|
|
2
2
|
import { chevronStroke } from './chevron.js';
|
|
3
3
|
import { zIndex } from './stacking.js';
|
|
4
4
|
import { quietFill } from './quiet-fill.js';
|
|
5
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
5
6
|
const caption = typographyVariants.caption;
|
|
6
7
|
const label = typographyVariants.label;
|
|
7
8
|
export function cssSelectClasses() {
|
|
@@ -186,6 +187,15 @@ export function cssSelectClasses() {
|
|
|
186
187
|
border-top: 1px solid var(--sw-color-border);
|
|
187
188
|
color: var(--sw-color-accent);
|
|
188
189
|
margin-top: var(--sw-space-1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* The comfortable touch target on a coarse pointer, at every size: the
|
|
193
|
+
trigger and each option are at least 44 px high (the md control height),
|
|
194
|
+
as every Button is. Type and padding keep the size's; the rule follows
|
|
195
|
+
the size rules, which it outranks only by order. */
|
|
196
|
+
@media ${coarsePointerQuery} {
|
|
197
|
+
.sw-select .sw-select-trigger,
|
|
198
|
+
.sw-select .sw-select-option { min-height: ${touchTarget}; }
|
|
189
199
|
}`;
|
|
190
200
|
}
|
|
191
201
|
export function selectClassCatalog() {
|
|
@@ -21,7 +21,10 @@ function type(variant, weight = variant.fontWeight) {
|
|
|
21
21
|
* win by coming after it (`cssSiteFooterClasses` follows `cssBandClasses`;
|
|
22
22
|
* a test holds the order).
|
|
23
23
|
* Below `md` the brand spans two columns of groups; from `md` the brand
|
|
24
|
-
* and the groups share one wrapping row, spread apart.
|
|
24
|
+
* and the groups share one wrapping row, spread apart. On a coarse pointer
|
|
25
|
+
* the group links and a link that is all of a bottom-row slot (`legal` or
|
|
26
|
+
* `end`, such as a language switch) are the 44 px touch target; a link
|
|
27
|
+
* inside running legal words stays inline.
|
|
25
28
|
*/
|
|
26
29
|
export function cssSiteFooterClasses() {
|
|
27
30
|
return `.sw-site-footer-top {
|
|
@@ -120,6 +123,13 @@ export function cssSiteFooterClasses() {
|
|
|
120
123
|
min-height: ${touchTarget};
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
[data-theme] .sw-band-inverse :is(.sw-site-footer-legal, .sw-site-footer-end) > a:not(.sw-button) {
|
|
127
|
+
align-items: center;
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
min-height: ${touchTarget};
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
.sw-site-footer-links {
|
|
124
134
|
gap: 0;
|
|
125
135
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cssSiteHeaderAnchorRules(): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { siteHeaderCompactHeight, siteHeaderWideHeight, siteHeaderWideQuery, } from './site-header-geometry.js';
|
|
2
|
+
/** The room an in-page jump leaves above its target: the bar, its hairline and a gap. */
|
|
3
|
+
function scrollPadding(barHeight) {
|
|
4
|
+
return `calc(${barHeight} + 1px + var(--sw-space-3))`;
|
|
5
|
+
}
|
|
6
|
+
/*
|
|
7
|
+
* While a sticky SiteHeader is on the page, the document's scroll padding
|
|
8
|
+
* clears the bar (its height at each width, its hairline) and a small gap,
|
|
9
|
+
* so a #fragment link, :target and scrollIntoView land below the bar
|
|
10
|
+
* instead of under it. A header in the page flow changes nothing.
|
|
11
|
+
*/
|
|
12
|
+
export function cssSiteHeaderAnchorRules() {
|
|
13
|
+
return `html:has(.sw-site-header-sticky) {
|
|
14
|
+
scroll-padding-top: ${scrollPadding(siteHeaderCompactHeight)};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media ${siteHeaderWideQuery} {
|
|
18
|
+
html:has(.sw-site-header-sticky) {
|
|
19
|
+
scroll-padding-top: ${scrollPadding(siteHeaderWideHeight)};
|
|
20
|
+
}
|
|
21
|
+
}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cssSiteHeaderSkipLinkRules(): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
3
|
+
import { siteHeaderContentStart, siteHeaderGutter, siteHeaderGutterQuery, } from './site-header-geometry.js';
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
/*
|
|
6
|
+
* The header's skip link, its first focusable element: visually hidden
|
|
7
|
+
* until it has focus, then a pill on the surface in the text colour with
|
|
8
|
+
* the accent ring, laid over the bar's start (centred in its height and
|
|
9
|
+
* lined up with the brand), the control height tall only then. Positioned,
|
|
10
|
+
* it paints over the bar's content, which is not, so it needs no layer of
|
|
11
|
+
* its own.
|
|
12
|
+
*/
|
|
13
|
+
export function cssSiteHeaderSkipLinkRules() {
|
|
14
|
+
return `.sw-site-header-skip-link {
|
|
15
|
+
align-items: center;
|
|
16
|
+
background: var(--sw-color-surface);
|
|
17
|
+
border-radius: var(--sw-radius-pill);
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
font-family: var(--sw-font-sans);
|
|
21
|
+
font-size: ${label.fontSize}px;
|
|
22
|
+
font-weight: ${label.fontWeight};
|
|
23
|
+
inset-block-start: 50%;
|
|
24
|
+
inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.compact)};
|
|
25
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
26
|
+
line-height: ${label.lineHeight}px;
|
|
27
|
+
padding-inline: var(--sw-space-4);
|
|
28
|
+
position: absolute;
|
|
29
|
+
transform: translateY(-50%);
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[data-theme] .sw-site-header-skip-link {
|
|
34
|
+
color: var(--sw-color-text);
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme] .sw-site-header-skip-link:focus {
|
|
39
|
+
min-height: var(--sw-control-md-min-height);
|
|
40
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
41
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sw-site-header-skip-link:not(:focus) { ${visuallyHiddenDeclarations} }
|
|
45
|
+
|
|
46
|
+
@media ${siteHeaderGutterQuery} {
|
|
47
|
+
.sw-site-header-skip-link { inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.wide)}; }
|
|
48
|
+
}`;
|
|
49
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
-
import {
|
|
2
|
+
import { cssSiteHeaderAnchorRules } from './css-site-header-anchors.js';
|
|
3
|
+
import { cssSiteHeaderSkipLinkRules } from './css-site-header-skip-link.js';
|
|
3
4
|
import { quietFill } from './quiet-fill.js';
|
|
5
|
+
import { siteHeaderBarMaxWidth, siteHeaderCompactHeight as compactHeight, siteHeaderGutter, siteHeaderGutterQuery, siteHeaderWideHeight as wideHeight, siteHeaderWideQuery as wide, } from './site-header-geometry.js';
|
|
4
6
|
import { zIndex } from './stacking.js';
|
|
5
7
|
const label = typographyVariants.label;
|
|
6
8
|
const title = typographyVariants.title;
|
|
7
|
-
/** The bar's height: 56 px on a phone, 72 px from `lg`. */
|
|
8
|
-
const compactHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-3))';
|
|
9
|
-
const wideHeight = 'calc(var(--sw-control-md-min-height) + var(--sw-space-5) + var(--sw-space-1))';
|
|
10
|
-
const wide = breakpointQuery('from', 'lg');
|
|
11
9
|
/*
|
|
12
10
|
* The bar: the solid surface with a hairline under it, its content in the
|
|
13
11
|
* page container with the page gutter. Below `lg` it holds the brand, the
|
|
14
12
|
* compact actions and the menu button; from `lg` the brand, the
|
|
15
13
|
* destinations (the current one at label weight over a bar in the words'
|
|
16
|
-
* colour, as tall as the bar) and the actions, spread apart.
|
|
14
|
+
* colour, as tall as the bar) and the actions, spread apart. The compact
|
|
15
|
+
* actions' box adds no layout of its own (`display: contents`).
|
|
17
16
|
*/
|
|
18
17
|
function barRules() {
|
|
19
18
|
return `.sw-site-header {
|
|
@@ -38,9 +37,9 @@ function barRules() {
|
|
|
38
37
|
gap: var(--sw-space-5);
|
|
39
38
|
justify-content: space-between;
|
|
40
39
|
margin-inline: auto;
|
|
41
|
-
max-width:
|
|
40
|
+
max-width: ${siteHeaderBarMaxWidth};
|
|
42
41
|
min-height: var(--sw-site-header-height);
|
|
43
|
-
padding-inline:
|
|
42
|
+
padding-inline: ${siteHeaderGutter.compact};
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
.sw-site-header-brand {
|
|
@@ -60,6 +59,10 @@ function barRules() {
|
|
|
60
59
|
gap: var(--sw-space-1);
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
.sw-site-header-compact-actions {
|
|
63
|
+
display: contents;
|
|
64
|
+
}
|
|
65
|
+
|
|
63
66
|
.sw-site-header-list {
|
|
64
67
|
list-style: none;
|
|
65
68
|
margin: 0;
|
|
@@ -101,8 +104,8 @@ function barRules() {
|
|
|
101
104
|
gap: var(--sw-space-4);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
@media ${
|
|
105
|
-
.sw-site-header-bar { padding-inline:
|
|
107
|
+
@media ${siteHeaderGutterQuery} {
|
|
108
|
+
.sw-site-header-bar { padding-inline: ${siteHeaderGutter.wide}; }
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
@media ${wide} {
|
|
@@ -118,13 +121,19 @@ function barRules() {
|
|
|
118
121
|
* open, the other pair hidden so only one is read), and its panel lies
|
|
119
122
|
* under the bar over the rest of the window, the destinations as large
|
|
120
123
|
* rows with hairlines and the menu's actions at the panel's foot. It
|
|
121
|
-
* scrolls on its own when the window is short.
|
|
124
|
+
* scrolls on its own when the window is short. While it is open the bar's
|
|
125
|
+
* compact actions step aside, since the menu holds its own actions, so the
|
|
126
|
+
* bar shows only the brand and the close button.
|
|
122
127
|
*/
|
|
123
128
|
function menuRules() {
|
|
124
129
|
return `.sw-site-header-menu > summary {
|
|
125
130
|
list-style: none;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
.sw-site-header-compact:has(> .sw-site-header-menu[open]) > .sw-site-header-compact-actions {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
.sw-site-header-menu > summary::-webkit-details-marker {
|
|
129
138
|
display: none;
|
|
130
139
|
}
|
|
@@ -214,6 +223,10 @@ export function cssSiteHeaderClasses() {
|
|
|
214
223
|
|
|
215
224
|
${menuRules()}
|
|
216
225
|
|
|
226
|
+
${cssSiteHeaderSkipLinkRules()}
|
|
227
|
+
|
|
228
|
+
${cssSiteHeaderAnchorRules()}
|
|
229
|
+
|
|
217
230
|
/*
|
|
218
231
|
* Forced colours drop box shadows, so the current destination's bar is a
|
|
219
232
|
* thick underline there.
|
|
@@ -248,6 +261,8 @@ export function siteHeaderClassCatalog() {
|
|
|
248
261
|
'sw-site-header-nav',
|
|
249
262
|
'sw-site-header-actions',
|
|
250
263
|
'sw-site-header-compact',
|
|
264
|
+
'sw-site-header-compact-actions',
|
|
265
|
+
'sw-site-header-skip-link',
|
|
251
266
|
'sw-site-header-list',
|
|
252
267
|
'sw-site-header-link',
|
|
253
268
|
'sw-site-header-menu',
|