@scalewing/react 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +39 -1
- package/dist/accessible-name.d.ts +13 -0
- package/dist/accessible-name.js +1 -0
- package/dist/components/DataBar.d.ts +48 -0
- package/dist/components/DataBar.js +28 -0
- package/dist/components/Pagination.d.ts +34 -0
- package/dist/components/Pagination.js +27 -0
- package/dist/components/ScrollRegion.d.ts +9 -5
- package/dist/components/ScrollRegion.js +8 -6
- package/dist/components/SectionNav.d.ts +2 -8
- package/dist/components/SegmentedControl.d.ts +2 -8
- package/dist/components/Table.d.ts +48 -4
- package/dist/components/Table.js +9 -3
- package/dist/components/Tabs.d.ts +2 -9
- package/dist/components/table/TableRow.d.ts +8 -2
- package/dist/components/table/TableRow.js +1 -1
- package/dist/components/table/table-max-height.d.ts +20 -0
- package/dist/components/table/table-max-height.js +23 -0
- package/dist/components/use-scroll-overflow.d.ts +10 -8
- package/dist/components/use-scroll-overflow.js +23 -10
- package/dist/css/chevron.d.ts +6 -3
- package/dist/css/chevron.js +9 -4
- package/dist/css/css-components.js +6 -0
- package/dist/css/css-data-bar.d.ts +16 -0
- package/dist/css/css-data-bar.js +107 -0
- package/dist/css/css-pagination.d.ts +2 -0
- package/dist/css/css-pagination.js +63 -0
- package/dist/css/css-scroll-region.d.ts +1 -0
- package/dist/css/css-scroll-region.js +10 -5
- package/dist/css/css-table-bounded-edges.d.ts +1 -0
- package/dist/css/css-table-bounded-edges.js +132 -0
- package/dist/css/css-table-bounded.d.ts +2 -0
- package/dist/css/css-table-bounded.js +73 -0
- package/dist/css/css-table-ledger.d.ts +5 -2
- package/dist/css/css-table-ledger.js +5 -2
- package/dist/css/css-table-metrics.d.ts +14 -0
- package/dist/css/css-table-metrics.js +13 -0
- package/dist/css/css-table-sort.js +2 -1
- package/dist/css/css-table-total-row.d.ts +2 -0
- package/dist/css/css-table-total-row.js +29 -0
- package/dist/css/css-table.js +14 -3
- package/dist/css/stylesheet.js +4 -0
- package/dist/data-bar-geometry.d.ts +24 -0
- package/dist/data-bar-geometry.js +41 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/pagination-pages.d.ts +13 -0
- package/dist/pagination-pages.js +22 -0
- package/dist/scroll-overflow.d.ts +12 -0
- package/dist/scroll-overflow.js +31 -7
- package/dist/styles.css +223 -0
- package/package.json +1 -1
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { noScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
|
|
3
|
+
import { blockScrollOverflow, noBlockScrollOverflow, noScrollOverflow, sameBlockScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
|
|
4
4
|
const unmeasured = {
|
|
5
5
|
...noScrollOverflow,
|
|
6
|
+
...noBlockScrollOverflow,
|
|
6
7
|
measured: false,
|
|
7
8
|
};
|
|
9
|
+
function measureBox(box) {
|
|
10
|
+
return {
|
|
11
|
+
...scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth),
|
|
12
|
+
...blockScrollOverflow(box.scrollTop, box.scrollHeight, box.clientHeight),
|
|
13
|
+
measured: true,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function sameMeasure(current, next) {
|
|
17
|
+
return (current.measured &&
|
|
18
|
+
sameScrollOverflow(current, next) &&
|
|
19
|
+
sameBlockScrollOverflow(current, next));
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
|
-
* Follows which
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `measured: false`, so the box
|
|
13
|
-
* measured; a measured box with
|
|
22
|
+
* Follows which edges of a scroll box have more content past them, the
|
|
23
|
+
* inline start and end and the top and bottom: on mount, on every scroll,
|
|
24
|
+
* and whenever the box or its content changes size. Before mount (and on
|
|
25
|
+
* the server) it reports no overflow and `measured: false`, so the box
|
|
26
|
+
* renders as it always has until it is measured; a measured box with
|
|
27
|
+
* neither inline edge set fits its content's width. A box whose height
|
|
28
|
+
* follows its content never reports a block edge.
|
|
14
29
|
*/
|
|
15
30
|
export function useScrollOverflow(ref) {
|
|
16
31
|
const [overflow, setOverflow] = useState(unmeasured);
|
|
@@ -25,10 +40,8 @@ export function useScrollOverflow(ref) {
|
|
|
25
40
|
function measure() {
|
|
26
41
|
if (!box)
|
|
27
42
|
return;
|
|
28
|
-
const next =
|
|
29
|
-
setOverflow((current) => current
|
|
30
|
-
? current
|
|
31
|
-
: { ...next, measured: true });
|
|
43
|
+
const next = measureBox(box);
|
|
44
|
+
setOverflow((current) => (sameMeasure(current, next) ? current : next));
|
|
32
45
|
}
|
|
33
46
|
measure();
|
|
34
47
|
box.addEventListener('scroll', measure, { passive: true });
|
package/dist/css/chevron.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* A stroked chevron in `color`: two sides of a `space-2` square (its
|
|
3
|
+
* bottom and right borders, so forced colours keep it), turned by each
|
|
4
|
+
* surface to point where it needs.
|
|
4
5
|
*/
|
|
5
|
-
export declare
|
|
6
|
+
export declare function chevronStrokeIn(color: string): string;
|
|
7
|
+
/** The muted chevron Accordion, Select and the menus share. */
|
|
8
|
+
export declare const chevronStroke: string;
|
package/dist/css/chevron.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* A stroked chevron in `color`: two sides of a `space-2` square (its
|
|
3
|
+
* bottom and right borders, so forced colours keep it), turned by each
|
|
4
|
+
* surface to point where it needs.
|
|
4
5
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export function chevronStrokeIn(color) {
|
|
7
|
+
return `border-bottom: 2px solid ${color};
|
|
8
|
+
border-right: 2px solid ${color};
|
|
7
9
|
box-sizing: border-box;
|
|
8
10
|
height: var(--sw-space-2);
|
|
9
11
|
width: var(--sw-space-2);`;
|
|
12
|
+
}
|
|
13
|
+
/** The muted chevron Accordion, Select and the menus share. */
|
|
14
|
+
export const chevronStroke = chevronStrokeIn('var(--sw-color-muted)');
|
|
@@ -4,6 +4,8 @@ import { cssAccordionClasses } from './css-accordion.js';
|
|
|
4
4
|
import { cssButtonClasses } from './css-button.js';
|
|
5
5
|
import { cssCalendarButtonClasses } from './css-calendar-button.js';
|
|
6
6
|
import { cssChartClasses } from './css-chart.js';
|
|
7
|
+
import { cssDataBarClasses } from './css-data-bar.js';
|
|
8
|
+
import { cssPaginationClasses } from './css-pagination.js';
|
|
7
9
|
import { cssCheckboxClasses } from './css-checkbox.js';
|
|
8
10
|
import { cssRadioGroupClasses } from './css-radio-group.js';
|
|
9
11
|
import { cssChromeClasses } from './css-chrome.js';
|
|
@@ -137,6 +139,10 @@ ${cssCalendarButtonClasses()}
|
|
|
137
139
|
|
|
138
140
|
${cssChartClasses()}
|
|
139
141
|
|
|
142
|
+
${cssDataBarClasses()}
|
|
143
|
+
|
|
144
|
+
${cssPaginationClasses()}
|
|
145
|
+
|
|
140
146
|
${cssChromeClasses()}
|
|
141
147
|
|
|
142
148
|
${cssSectionNavClasses()}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a range bar's fill means: the start or a carried figure (`neutral`),
|
|
3
|
+
* money in (`success`), money out (`muted`, never danger: an outflow is not
|
|
4
|
+
* an error), a subtotal (`strong`) and what was counted or recorded
|
|
5
|
+
* (`accent`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const dataBarTones: readonly ["neutral", "success", "muted", "strong", "accent"];
|
|
8
|
+
export type DataBarTone = (typeof dataBarTones)[number];
|
|
9
|
+
export declare const dataBarToneMixes: Record<DataBarTone, {
|
|
10
|
+
color: 'muted' | 'success' | 'text' | 'accent';
|
|
11
|
+
share: number;
|
|
12
|
+
}>;
|
|
13
|
+
/** A tone's fill: its colour, mixed into the surface by its share. */
|
|
14
|
+
export declare function dataBarToneFill(tone: DataBarTone): string;
|
|
15
|
+
export declare function cssDataBarClasses(): string;
|
|
16
|
+
export declare function dataBarClassCatalog(): string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a range bar's fill means: the start or a carried figure (`neutral`),
|
|
3
|
+
* money in (`success`), money out (`muted`, never danger: an outflow is not
|
|
4
|
+
* an error), a subtotal (`strong`) and what was counted or recorded
|
|
5
|
+
* (`accent`).
|
|
6
|
+
*/
|
|
7
|
+
export const dataBarTones = [
|
|
8
|
+
'neutral',
|
|
9
|
+
'success',
|
|
10
|
+
'muted',
|
|
11
|
+
'strong',
|
|
12
|
+
'accent',
|
|
13
|
+
];
|
|
14
|
+
/*
|
|
15
|
+
* Each tone is a semantic colour, mixed into the surface where the board
|
|
16
|
+
* draws a lighter fill: the bar is decorative (the figure is the cell's
|
|
17
|
+
* text), so the mixes keep the tones apart from one another rather than
|
|
18
|
+
* meeting a text contrast: every tone at 1.5:1 or more on the surface, and
|
|
19
|
+
* the three greys (out, start, subtotal) 1.5:1 or more from the next one,
|
|
20
|
+
* in every palette and scheme (`data-bar.test.tsx`). `accent` is the
|
|
21
|
+
* accent itself.
|
|
22
|
+
*/
|
|
23
|
+
export const dataBarToneMixes = {
|
|
24
|
+
neutral: { color: 'muted', share: 0.65 },
|
|
25
|
+
success: { color: 'success', share: 0.55 },
|
|
26
|
+
muted: { color: 'muted', share: 0.35 },
|
|
27
|
+
strong: { color: 'text', share: 0.85 },
|
|
28
|
+
accent: { color: 'accent', share: 1 },
|
|
29
|
+
};
|
|
30
|
+
/** A tone's fill: its colour, mixed into the surface by its share. */
|
|
31
|
+
export function dataBarToneFill(tone) {
|
|
32
|
+
const { color, share } = dataBarToneMixes[tone];
|
|
33
|
+
if (share === 1)
|
|
34
|
+
return `var(--sw-color-${color})`;
|
|
35
|
+
return `color-mix(in srgb, var(--sw-color-${color}) ${Math.round(share * 100)}%, var(--sw-color-surface))`;
|
|
36
|
+
}
|
|
37
|
+
function toneRules() {
|
|
38
|
+
return dataBarTones
|
|
39
|
+
.map((tone) => `.sw-data-bar-${tone} > .sw-data-bar-fill { background: ${dataBarToneFill(tone)}; }`)
|
|
40
|
+
.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/*
|
|
43
|
+
* A range bar fills its container's width: a track with no fill of its own,
|
|
44
|
+
* a spacing step 4 tall, and the bar placed on it by two shares the
|
|
45
|
+
* component sets (`--sw-data-bar-start`, `--sw-data-bar-size`), with
|
|
46
|
+
* logical insets so a right-to-left table mirrors it. A diverging domain
|
|
47
|
+
* draws a hairline at zero (`--sw-data-bar-zero`) across the track, under
|
|
48
|
+
* the bar, and the bar's end at zero is square (`sw-data-bar-zero-start`
|
|
49
|
+
* or `-end`), so it meets the line as BarChart's diverging bars do.
|
|
50
|
+
* Forced colours draw the bar and the line in the system text colour;
|
|
51
|
+
* the figure beside it carries the meaning there, as always.
|
|
52
|
+
*/
|
|
53
|
+
export function cssDataBarClasses() {
|
|
54
|
+
return `.sw-data-bar {
|
|
55
|
+
display: block;
|
|
56
|
+
height: var(--sw-space-4);
|
|
57
|
+
min-width: var(--sw-space-6);
|
|
58
|
+
position: relative;
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.sw-data-bar-fill {
|
|
63
|
+
border-radius: calc(var(--sw-radius-sm) / 2);
|
|
64
|
+
inset-block: 0;
|
|
65
|
+
inset-inline-start: calc(var(--sw-data-bar-start, 0) * 100%);
|
|
66
|
+
position: absolute;
|
|
67
|
+
width: calc(var(--sw-data-bar-size, 0) * 100%);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
${toneRules()}
|
|
71
|
+
|
|
72
|
+
.sw-data-bar-diverging::before {
|
|
73
|
+
background: var(--sw-color-border);
|
|
74
|
+
content: '';
|
|
75
|
+
inset-block: calc(-1 * var(--sw-space-1));
|
|
76
|
+
inset-inline-start: calc(var(--sw-data-bar-zero, 0) * 100%);
|
|
77
|
+
/* Centred on zero, mirrored right to left. */
|
|
78
|
+
margin-inline-start: -0.5px;
|
|
79
|
+
position: absolute;
|
|
80
|
+
width: 1px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.sw-data-bar-zero-start > .sw-data-bar-fill {
|
|
84
|
+
border-end-start-radius: 0;
|
|
85
|
+
border-start-start-radius: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sw-data-bar-zero-end > .sw-data-bar-fill {
|
|
89
|
+
border-end-end-radius: 0;
|
|
90
|
+
border-start-end-radius: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@media (forced-colors: active) {
|
|
94
|
+
.sw-data-bar > .sw-data-bar-fill,
|
|
95
|
+
.sw-data-bar-diverging::before { background: CanvasText; }
|
|
96
|
+
}`;
|
|
97
|
+
}
|
|
98
|
+
export function dataBarClassCatalog() {
|
|
99
|
+
return [
|
|
100
|
+
'sw-data-bar',
|
|
101
|
+
'sw-data-bar-fill',
|
|
102
|
+
'sw-data-bar-diverging',
|
|
103
|
+
'sw-data-bar-zero-start',
|
|
104
|
+
'sw-data-bar-zero-end',
|
|
105
|
+
...dataBarTones.map((tone) => `sw-data-bar-${tone}`),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { chevronStrokeIn } from './chevron.js';
|
|
3
|
+
import { iconButtonRules } from './css-icon-button.js';
|
|
4
|
+
const caption = typographyVariants.caption;
|
|
5
|
+
/*
|
|
6
|
+
* A pager: the consumer's range words at the inline start in the muted
|
|
7
|
+
* caption, the previous and next steps at the end, wrapping under the
|
|
8
|
+
* words on a narrow column. The steps are icon-only neutral Buttons,
|
|
9
|
+
* square at their size and the touch target on a coarse pointer
|
|
10
|
+
* (`iconButtonRules`).
|
|
11
|
+
*
|
|
12
|
+
* Their glyph is private control chrome: the shared stroked chevron
|
|
13
|
+
* (`chevron.ts`) in the button's colour, pointing toward the inline start
|
|
14
|
+
* for the previous page and the end for the next; right to left one flip
|
|
15
|
+
* of the turned chevron mirrors both. It is the chevron Accordion, Select
|
|
16
|
+
* and the menus draw, not DateField's month steps (an SVG at a larger
|
|
17
|
+
* size inside the calendar).
|
|
18
|
+
*/
|
|
19
|
+
export function cssPaginationClasses() {
|
|
20
|
+
return `.sw-pagination {
|
|
21
|
+
align-items: center;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-wrap: wrap;
|
|
24
|
+
gap: var(--sw-space-2) var(--sw-space-3);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sw-pagination-range {
|
|
28
|
+
color: var(--sw-color-muted);
|
|
29
|
+
font-family: var(--sw-font-sans);
|
|
30
|
+
font-size: ${caption.fontSize}px;
|
|
31
|
+
font-variant-numeric: tabular-nums;
|
|
32
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
33
|
+
line-height: ${caption.lineHeight}px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sw-pagination-steps {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: var(--sw-space-2);
|
|
39
|
+
margin-inline-start: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
${iconButtonRules('sw-pagination-step')}
|
|
43
|
+
|
|
44
|
+
.sw-pagination-glyph {
|
|
45
|
+
${chevronStrokeIn('currentColor')}
|
|
46
|
+
display: block;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sw-pagination-glyph-previous { transform: translateX(25%) rotate(135deg); }
|
|
50
|
+
.sw-pagination-glyph-next { transform: translateX(-25%) rotate(-45deg); }
|
|
51
|
+
.sw-pagination-glyph:dir(rtl) { scale: -1 1; }`;
|
|
52
|
+
}
|
|
53
|
+
export function paginationClassCatalog() {
|
|
54
|
+
return [
|
|
55
|
+
'sw-pagination',
|
|
56
|
+
'sw-pagination-range',
|
|
57
|
+
'sw-pagination-steps',
|
|
58
|
+
'sw-pagination-step',
|
|
59
|
+
'sw-pagination-glyph',
|
|
60
|
+
'sw-pagination-glyph-previous',
|
|
61
|
+
'sw-pagination-glyph-next',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* inherited variables, so a scroll region inside another never takes its
|
|
8
8
|
* ancestor's shade.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
10
|
+
export const scrollShadeColor = 'color-mix(in srgb, var(--sw-color-text) 28%, transparent)';
|
|
11
11
|
function edgeShade(side) {
|
|
12
12
|
const offset = side === 'left' ? 'var(--sw-space-6)' : 'calc(-1 * var(--sw-space-6))';
|
|
13
|
-
return `inset ${offset} 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) ${
|
|
13
|
+
return `inset ${offset} 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) ${scrollShadeColor}`;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* The shade each inline edge of `selector` draws while `useScrollOverflow`
|
|
@@ -61,7 +61,12 @@ ${selector}:focus-visible {
|
|
|
61
61
|
${scrollShadeRules(selector)}`;
|
|
62
62
|
}
|
|
63
63
|
export function scrollRegionClassCatalog() {
|
|
64
|
-
return [
|
|
64
|
+
return [
|
|
65
|
+
'sw-scroll-more-start',
|
|
66
|
+
'sw-scroll-more-end',
|
|
67
|
+
'sw-scroll-more-top',
|
|
68
|
+
'sw-scroll-more-bottom',
|
|
69
|
+
];
|
|
65
70
|
}
|
|
66
71
|
/**
|
|
67
72
|
* A region whose start column is pinned (sticky, on an opaque fill) hides
|
|
@@ -76,7 +81,7 @@ export function pinnedStartShadeRules(region, pinned) {
|
|
|
76
81
|
.map((cell) => `${region}.sw-scroll-more-start${direction} ${cell}::after`)
|
|
77
82
|
.join(',\n');
|
|
78
83
|
return `${after('')} {
|
|
79
|
-
background: linear-gradient(to right, ${
|
|
84
|
+
background: linear-gradient(to right, ${scrollShadeColor}, transparent);
|
|
80
85
|
content: '';
|
|
81
86
|
inset-block: 0;
|
|
82
87
|
inset-inline-start: 100%;
|
|
@@ -86,7 +91,7 @@ export function pinnedStartShadeRules(region, pinned) {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
${after(':dir(rtl)')} {
|
|
89
|
-
background: linear-gradient(to left, ${
|
|
94
|
+
background: linear-gradient(to left, ${scrollShadeColor}, transparent);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
@media (forced-colors: active) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cssTableBoundedEdgeClasses(): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { scrollShadeColor } from './css-scroll-region.js';
|
|
2
|
+
const region = '.sw-table-bounded';
|
|
3
|
+
/*
|
|
4
|
+
* While rows are scrolled out past the top or bottom of a bounded region
|
|
5
|
+
* (`sw-scroll-more-top`, `sw-scroll-more-bottom`), that edge draws the
|
|
6
|
+
* scroll shade, a spacing step 5 deep, in the colour of the inline edges'
|
|
7
|
+
* shade. A pinned row covers the region's own edge, so the shade is cast
|
|
8
|
+
* from the pinned row instead: the header's last row extends its rule's
|
|
9
|
+
* `::before` downward into a gradient, and a sticky footer's first row
|
|
10
|
+
* extends its 2 px rule's `::before` upward into one. Both are drawn
|
|
11
|
+
* inside their pinned group's layer, over the rows that scroll under
|
|
12
|
+
* them, and no cell moves. Only the header's last row casts it, so a
|
|
13
|
+
* header cell that spans rows (`rowSpan`) from an earlier row casts none
|
|
14
|
+
* under its column. An edge with nothing pinned on it, such as a bounded
|
|
15
|
+
* table without a sticky footer, shades the region's own edge with a
|
|
16
|
+
* background layer fixed to the box, under the rows as the inline shade
|
|
17
|
+
* is. Forced colours draw no shade; the system scrollbar is the cue.
|
|
18
|
+
*/
|
|
19
|
+
function shadeRules() {
|
|
20
|
+
const shade = (direction) => `linear-gradient(to ${direction}, ${scrollShadeColor}, transparent)`;
|
|
21
|
+
const headCells = `${region}.sw-scroll-more-top > .sw-table-sticky > thead > tr:last-child > :is(th, td)::before`;
|
|
22
|
+
const footCells = `${region}.sw-scroll-more-bottom > .sw-table > .sw-table-footer-sticky > tr:first-child > :is(th, td)::before`;
|
|
23
|
+
const noHead = ':not(:has(> .sw-table > thead))';
|
|
24
|
+
const noPinnedFoot = ':not(:has(> .sw-table > .sw-table-footer-sticky))';
|
|
25
|
+
const ownTop = `${region}.sw-scroll-more-top${noHead}`;
|
|
26
|
+
const ownBottom = `${region}.sw-scroll-more-bottom${noPinnedFoot}`;
|
|
27
|
+
const ownBoth = `${region}.sw-scroll-more-top.sw-scroll-more-bottom${noHead}${noPinnedFoot}`;
|
|
28
|
+
return `${headCells} {
|
|
29
|
+
background: ${shade('bottom')};
|
|
30
|
+
border-bottom: 0;
|
|
31
|
+
border-top: 1px solid var(--sw-color-border);
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
height: var(--sw-space-5);
|
|
34
|
+
inset-block-end: auto;
|
|
35
|
+
inset-block-start: calc(100% - 1px);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${footCells} {
|
|
39
|
+
background: ${shade('top')};
|
|
40
|
+
border-bottom: 2px solid var(--sw-color-border);
|
|
41
|
+
border-top: 0;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
height: var(--sw-space-5);
|
|
44
|
+
inset-block-end: calc(100% - 1px);
|
|
45
|
+
inset-block-start: auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
${ownTop},
|
|
49
|
+
${ownBottom} {
|
|
50
|
+
background-repeat: no-repeat;
|
|
51
|
+
background-size: 100% var(--sw-space-5);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
${ownTop} {
|
|
55
|
+
background-image: ${shade('bottom')};
|
|
56
|
+
background-position: top;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
${ownBottom} {
|
|
60
|
+
background-image: ${shade('top')};
|
|
61
|
+
background-position: bottom;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
${ownBoth} {
|
|
65
|
+
background-image: ${shade('bottom')}, ${shade('top')};
|
|
66
|
+
background-position: top, bottom;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
${headCells},
|
|
71
|
+
${footCells} { background: none; border-color: CanvasText; }
|
|
72
|
+
${ownTop},
|
|
73
|
+
${ownBottom},
|
|
74
|
+
${ownBoth} { background-image: none; }
|
|
75
|
+
}`;
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* The region's focus ring is its own outline, drawn inside its edge, and
|
|
79
|
+
* the pinned header and sticky footer paint over it. While the region has
|
|
80
|
+
* keyboard focus, those pinned cells carry the ring's part they cover: an
|
|
81
|
+
* inset line at the top of the header's first row and the bottom of the
|
|
82
|
+
* footer's last row, and at the outer side of each pinned row's first and
|
|
83
|
+
* last cell (mirrored right to left). The parts are custom properties set
|
|
84
|
+
* on the cell they belong to and joined in one shadow, so a corner cell
|
|
85
|
+
* carries two. The side parts sit on the first and last cells, so they
|
|
86
|
+
* show while the region is scrolled to that inline edge, and a header
|
|
87
|
+
* cell spanning rows (`rowSpan`) puts a later row's first cell off the
|
|
88
|
+
* edge, where its side part would land mid-header. Forced colours drop
|
|
89
|
+
* shadows; the outline shows beside the rows there.
|
|
90
|
+
*/
|
|
91
|
+
function focusRules() {
|
|
92
|
+
const focused = `${region}:focus-visible`;
|
|
93
|
+
const head = `${focused} > .sw-table-sticky > thead`;
|
|
94
|
+
const foot = `${focused} > .sw-table > .sw-table-footer-sticky`;
|
|
95
|
+
const cells = (group, row = 'tr', cell = ':is(th, td)') => `${group} > ${row} > ${cell}`;
|
|
96
|
+
const both = (row, cell) => `${cells(head, row, cell)},\n${cells(foot, row, cell)}`;
|
|
97
|
+
const ring = 'var(--sw-focus-ring-width)';
|
|
98
|
+
const line = (x, y) => `inset ${x} ${y} 0 0 var(--sw-color-accent)`;
|
|
99
|
+
const none = '0 0 transparent';
|
|
100
|
+
return `${both('tr', ':is(th, td)')} {
|
|
101
|
+
box-shadow: var(--sw-table-ring-block, ${none}), var(--sw-table-ring-start, ${none}), var(--sw-table-ring-end, ${none});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
${cells(head, 'tr:first-child')} {
|
|
105
|
+
--sw-table-ring-block: ${line('0', ring)};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
${cells(foot, 'tr:last-child')} {
|
|
109
|
+
--sw-table-ring-block: ${line('0', `calc(-1 * ${ring})`)};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
${both('tr', ':first-child')} {
|
|
113
|
+
--sw-table-ring-start: ${line(ring, '0')};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
${both('tr', ':last-child')} {
|
|
117
|
+
--sw-table-ring-end: ${line(`calc(-1 * ${ring})`, '0')};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
${both('tr', ':first-child:dir(rtl)')} {
|
|
121
|
+
--sw-table-ring-start: ${line(`calc(-1 * ${ring})`, '0')};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
${both('tr', ':last-child:dir(rtl)')} {
|
|
125
|
+
--sw-table-ring-end: ${line(ring, '0')};
|
|
126
|
+
}`;
|
|
127
|
+
}
|
|
128
|
+
export function cssTableBoundedEdgeClasses() {
|
|
129
|
+
return `${shadeRules()}
|
|
130
|
+
|
|
131
|
+
${focusRules()}`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { cssTableBoundedEdgeClasses } from './css-table-bounded-edges.js';
|
|
3
|
+
import { tableCellPadding, tableRuleWidth, tableSortMinHeight, } from './css-table-metrics.js';
|
|
4
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
5
|
+
const body = typographyVariants.body;
|
|
6
|
+
const caption = typographyVariants.caption;
|
|
7
|
+
/** A header row whose line is `line` tall: that line and its padding twice. */
|
|
8
|
+
function headBlock(line) {
|
|
9
|
+
return `calc(${line} + 2 * var(--sw-table-cell-pad))`;
|
|
10
|
+
}
|
|
11
|
+
/*
|
|
12
|
+
* A bounded table (`Table maxHeight`, `sw-table-bounded` on its region)
|
|
13
|
+
* is as tall as its row count (`--sw-table-max-rows`) of single-line body
|
|
14
|
+
* rows, plus one header row when it has a `thead` and one footer row when
|
|
15
|
+
* it has a `tfoot`, from the measures its cells are drawn with
|
|
16
|
+
* (`css-table-metrics.ts`): a body row is the body line, the cell padding
|
|
17
|
+
* twice and its rule; a header row is the caption line and the padding
|
|
18
|
+
* twice (a pinned header cell draws its rule inside itself), or the sort
|
|
19
|
+
* button's height where a header sorts (the touch target on a coarse
|
|
20
|
+
* pointer). Compact density swaps the padding. Rows of two lines or with
|
|
21
|
+
* controls, a second header or footer row, and a horizontal scrollbar
|
|
22
|
+
* that takes room (classic scrollbars) leave fewer rows showing.
|
|
23
|
+
*
|
|
24
|
+
* The region already scrolls (`overflow: auto`), so the rows past that
|
|
25
|
+
* height scroll inside it, and the pinned header group (`thead`, top 0)
|
|
26
|
+
* and a sticky footer (bottom 0) pin to the region's own edges: it is
|
|
27
|
+
* their nearest scroll box. A bounded region is never marked
|
|
28
|
+
* `sw-scroll-fits`, so it never stops being a scroll box for the page
|
|
29
|
+
* pinning in `css-table-page-pin.ts`. Printed, every row shows.
|
|
30
|
+
*/
|
|
31
|
+
function heightRules() {
|
|
32
|
+
const sorting = '.sw-table-bounded:has(> .sw-table > thead .sw-table-sort)';
|
|
33
|
+
return `.sw-table-bounded {
|
|
34
|
+
--sw-table-cell-pad: ${tableCellPadding.comfortable.block};
|
|
35
|
+
--sw-table-head-block: 0px;
|
|
36
|
+
--sw-table-foot-rows: 0;
|
|
37
|
+
max-height: calc((var(--sw-table-max-rows) + var(--sw-table-foot-rows)) * (${body.lineHeight}px + 2 * var(--sw-table-cell-pad) + ${tableRuleWidth}) + var(--sw-table-head-block));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sw-table-bounded:has(> .sw-table-compact) {
|
|
41
|
+
--sw-table-cell-pad: ${tableCellPadding.compact.block};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sw-table-bounded:has(> .sw-table > thead) {
|
|
45
|
+
--sw-table-head-block: ${headBlock(`${caption.lineHeight}px`)};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
${sorting} {
|
|
49
|
+
--sw-table-head-block: ${headBlock(`max(${caption.lineHeight}px, ${tableSortMinHeight})`)};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media ${coarsePointerQuery} {
|
|
53
|
+
${sorting} {
|
|
54
|
+
--sw-table-head-block: ${headBlock(`max(${caption.lineHeight}px, ${touchTarget})`)};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.sw-table-bounded:has(> .sw-table > tfoot) {
|
|
59
|
+
--sw-table-foot-rows: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media print {
|
|
63
|
+
.sw-table-bounded { max-height: none; }
|
|
64
|
+
}`;
|
|
65
|
+
}
|
|
66
|
+
export function cssTableBoundedClasses() {
|
|
67
|
+
return `${heightRules()}
|
|
68
|
+
|
|
69
|
+
${cssTableBoundedEdgeClasses()}`;
|
|
70
|
+
}
|
|
71
|
+
export function tableBoundedClassCatalog() {
|
|
72
|
+
return ['sw-table-bounded'];
|
|
73
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/** The colours a cell's text can take: a signed figure or a quiet value. */
|
|
2
2
|
export declare const tableCellTones: readonly ["success", "danger", "muted", "accent"];
|
|
3
3
|
export type TableCellTone = (typeof tableCellTones)[number];
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A row's place in the table: a row of its own, one under the row above,
|
|
6
|
+
* or a total of the rows above it, mid-table.
|
|
7
|
+
*/
|
|
8
|
+
export declare const tableRowVariants: readonly ["default", "sub", "total"];
|
|
6
9
|
export type TableRowVariant = (typeof tableRowVariants)[number];
|
|
7
10
|
export declare function assertTableCellTone(tone: unknown): void;
|
|
8
11
|
export declare function assertTableRowVariant(variant: unknown): void;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** The colours a cell's text can take: a signed figure or a quiet value. */
|
|
2
2
|
export const tableCellTones = ['success', 'danger', 'muted', 'accent'];
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A row's place in the table: a row of its own, one under the row above,
|
|
5
|
+
* or a total of the rows above it, mid-table.
|
|
6
|
+
*/
|
|
7
|
+
export const tableRowVariants = ['default', 'sub', 'total'];
|
|
5
8
|
export function assertTableCellTone(tone) {
|
|
6
9
|
if (tone !== undefined && !tableCellTones.includes(tone))
|
|
7
10
|
throw new RangeError(`Unknown TableCell tone: ${String(tone)}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const tableCellPadding: {
|
|
2
|
+
readonly comfortable: {
|
|
3
|
+
readonly block: "var(--sw-space-2)";
|
|
4
|
+
readonly inline: "var(--sw-space-3)";
|
|
5
|
+
};
|
|
6
|
+
readonly compact: {
|
|
7
|
+
readonly block: "var(--sw-space-1)";
|
|
8
|
+
readonly inline: "var(--sw-space-2)";
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/** The rule under every row (a collapsed cell border). */
|
|
12
|
+
export declare const tableRuleWidth = "1px";
|
|
13
|
+
/** The least height of a sortable header's button on a fine pointer. */
|
|
14
|
+
export declare const tableSortMinHeight = "var(--sw-control-xs-min-height)";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The measures a table's cells are drawn with, shared by the cell rules
|
|
3
|
+
* (`css-table.ts`, `css-table-sort.ts`) and the bounded table's height
|
|
4
|
+
* (`css-table-bounded.ts`), so a change to one moves the other with it.
|
|
5
|
+
*/
|
|
6
|
+
export const tableCellPadding = {
|
|
7
|
+
comfortable: { block: 'var(--sw-space-2)', inline: 'var(--sw-space-3)' },
|
|
8
|
+
compact: { block: 'var(--sw-space-1)', inline: 'var(--sw-space-2)' },
|
|
9
|
+
};
|
|
10
|
+
/** The rule under every row (a collapsed cell border). */
|
|
11
|
+
export const tableRuleWidth = '1px';
|
|
12
|
+
/** The least height of a sortable header's button on a fine pointer. */
|
|
13
|
+
export const tableSortMinHeight = 'var(--sw-control-xs-min-height)';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chevronStroke } from './chevron.js';
|
|
2
|
+
import { tableSortMinHeight } from './css-table-metrics.js';
|
|
2
3
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
3
4
|
/*
|
|
4
5
|
* A sortable header's button keeps the header's own text style (caption,
|
|
@@ -63,7 +64,7 @@ export function cssTableSortClasses() {
|
|
|
63
64
|
letter-spacing: inherit;
|
|
64
65
|
/* A step of room for the focus ring, taken back so the name stays on the cell's edge. */
|
|
65
66
|
margin-inline: calc(-1 * var(--sw-space-1));
|
|
66
|
-
min-height:
|
|
67
|
+
min-height: ${tableSortMinHeight};
|
|
67
68
|
padding: 0 var(--sw-space-1);
|
|
68
69
|
text-align: inherit;
|
|
69
70
|
}
|