@scalewing/react 1.29.0 → 1.30.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 +41 -0
- package/README.md +9 -3
- package/dist/components/ActionMenu.d.ts +14 -0
- package/dist/components/ActionMenu.js +4 -0
- package/dist/components/ButtonGroup.d.ts +9 -1
- package/dist/components/ButtonGroup.js +2 -2
- package/dist/components/FilterChips.d.ts +17 -1
- package/dist/components/FilterChips.js +11 -6
- package/dist/components/ScrollRegion.d.ts +6 -2
- package/dist/components/ScrollRegion.js +9 -3
- package/dist/components/SectionNav.d.ts +25 -2
- package/dist/components/SectionNav.js +13 -4
- package/dist/components/Table.d.ts +49 -5
- package/dist/components/Table.js +11 -4
- package/dist/components/action-menu/ActionMenuList.js +7 -7
- package/dist/components/table/TableCell.d.ts +8 -0
- package/dist/components/table/TableCell.js +4 -2
- package/dist/components/table/TableRow.d.ts +39 -0
- package/dist/components/table/TableRow.js +8 -0
- package/dist/components/use-scroll-overflow.d.ts +8 -3
- package/dist/components/use-scroll-overflow.js +15 -4
- package/dist/css/css-action-menu.js +41 -0
- package/dist/css/css-button-group-attached.d.ts +2 -0
- package/dist/css/css-button-group-attached.js +70 -0
- package/dist/css/css-button.js +5 -1
- package/dist/css/css-components.js +1 -0
- package/dist/css/css-filter-chips.js +26 -0
- package/dist/css/css-section-nav-filled.d.ts +6 -0
- package/dist/css/css-section-nav-filled.js +80 -0
- package/dist/css/css-section-nav.js +4 -0
- package/dist/css/css-table-footer.d.ts +2 -0
- package/dist/css/css-table-footer.js +57 -0
- package/dist/css/css-table-ledger.d.ts +10 -0
- package/dist/css/css-table-ledger.js +122 -0
- package/dist/css/css-table.js +8 -0
- package/dist/css/stacking.d.ts +9 -3
- package/dist/css/stacking.js +24 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/styles.css +278 -5
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type TableRowVariant } from '../../css/css-table-ledger.js';
|
|
3
|
+
export type { TableRowVariant };
|
|
4
|
+
export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* `sub` sets the row under the one above it, as an item of a batch: its
|
|
8
|
+
* row header (`TableCell as="th" scope="row"`) is indented with a guide
|
|
9
|
+
* bar at the indent; a sub row without a row header is not indented.
|
|
10
|
+
* `default` is a row of its own.
|
|
11
|
+
*/
|
|
12
|
+
variant?: TableRowVariant;
|
|
13
|
+
/**
|
|
14
|
+
* A voided row kept on the record: the subtle fill and the text muted
|
|
15
|
+
* and struck through, toned cells included. A line-through is not read
|
|
16
|
+
* aloud, so say why in the row, such as a `Badge` "Voided" in the
|
|
17
|
+
* title's line (a Badge in a line of text is not struck; one that is a
|
|
18
|
+
* flex item, a direct child of `Inline`, is). Works with either variant.
|
|
19
|
+
*/
|
|
20
|
+
struck?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableRowElement> & {
|
|
23
|
+
selected?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* `sub` sets the row under the one above it, as an item of a batch: its
|
|
26
|
+
* row header (`TableCell as="th" scope="row"`) is indented with a guide
|
|
27
|
+
* bar at the indent; a sub row without a row header is not indented.
|
|
28
|
+
* `default` is a row of its own.
|
|
29
|
+
*/
|
|
30
|
+
variant?: TableRowVariant;
|
|
31
|
+
/**
|
|
32
|
+
* A voided row kept on the record: the subtle fill and the text muted
|
|
33
|
+
* and struck through, toned cells included. A line-through is not read
|
|
34
|
+
* aloud, so say why in the row, such as a `Badge` "Voided" in the
|
|
35
|
+
* title's line (a Badge in a line of text is not struck; one that is a
|
|
36
|
+
* flex item, a direct child of `Inline`, is). Works with either variant.
|
|
37
|
+
*/
|
|
38
|
+
struck?: boolean;
|
|
39
|
+
} & import("react").RefAttributes<HTMLTableRowElement>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../../class-names.js';
|
|
4
|
+
import { assertTableRowVariant, } from '../../css/css-table-ledger.js';
|
|
5
|
+
export const TableRow = forwardRef(function TableRow({ className, selected = false, struck = false, variant = 'default', ...rest }, ref) {
|
|
6
|
+
assertTableRowVariant(variant);
|
|
7
|
+
return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', struck && 'sw-table-row-struck', className), ...rest }));
|
|
8
|
+
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
2
|
import { type ScrollOverflow } from '../scroll-overflow.js';
|
|
3
|
+
/** A box's overflow, and whether it has been measured yet. */
|
|
4
|
+
export type MeasuredScrollOverflow = ScrollOverflow & {
|
|
5
|
+
measured: boolean;
|
|
6
|
+
};
|
|
3
7
|
/**
|
|
4
8
|
* Follows which inline edges of a scroll box have more content past them:
|
|
5
9
|
* on mount, on every scroll, and whenever the box or its content changes
|
|
6
|
-
* size. Before mount (and on the server) it reports no overflow
|
|
7
|
-
* box renders as it always has until it is
|
|
10
|
+
* size. Before mount (and on the server) it reports no overflow and
|
|
11
|
+
* `measured: false`, so the box renders as it always has until it is
|
|
12
|
+
* measured; a measured box with neither edge set fits its content.
|
|
8
13
|
*/
|
|
9
|
-
export declare function useScrollOverflow(ref: RefObject<HTMLElement | null>):
|
|
14
|
+
export declare function useScrollOverflow(ref: RefObject<HTMLElement | null>): MeasuredScrollOverflow;
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { noScrollOverflow, sameScrollOverflow, scrollOverflow, } from '../scroll-overflow.js';
|
|
4
|
+
const unmeasured = {
|
|
5
|
+
...noScrollOverflow,
|
|
6
|
+
measured: false,
|
|
7
|
+
};
|
|
4
8
|
/**
|
|
5
9
|
* Follows which inline edges of a scroll box have more content past them:
|
|
6
10
|
* on mount, on every scroll, and whenever the box or its content changes
|
|
7
|
-
* size. Before mount (and on the server) it reports no overflow
|
|
8
|
-
* box renders as it always has until it is
|
|
11
|
+
* size. Before mount (and on the server) it reports no overflow and
|
|
12
|
+
* `measured: false`, so the box renders as it always has until it is
|
|
13
|
+
* measured; a measured box with neither edge set fits its content.
|
|
9
14
|
*/
|
|
10
15
|
export function useScrollOverflow(ref) {
|
|
11
|
-
const [overflow, setOverflow] = useState(
|
|
16
|
+
const [overflow, setOverflow] = useState(unmeasured);
|
|
12
17
|
useEffect(() => {
|
|
13
18
|
const box = ref.current;
|
|
14
19
|
if (!box)
|
|
15
20
|
return;
|
|
21
|
+
// The metrics hold on a box that does not scroll (a fitting table
|
|
22
|
+
// region set to overflow: visible): scrollWidth still reports content
|
|
23
|
+
// past its edge, so the box turns back into a scroll box as it widens.
|
|
24
|
+
// Keep it that way; a scroll-container-only check would pin it open.
|
|
16
25
|
function measure() {
|
|
17
26
|
if (!box)
|
|
18
27
|
return;
|
|
19
28
|
const next = scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth);
|
|
20
|
-
setOverflow((current) => sameScrollOverflow(current, next)
|
|
29
|
+
setOverflow((current) => current.measured && sameScrollOverflow(current, next)
|
|
30
|
+
? current
|
|
31
|
+
: { ...next, measured: true });
|
|
21
32
|
}
|
|
22
33
|
measure();
|
|
23
34
|
box.addEventListener('scroll', measure, { passive: true });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { chevronStroke } from './chevron.js';
|
|
2
3
|
import { zIndex } from './stacking.js';
|
|
3
4
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
4
5
|
const caption = typographyVariants.caption;
|
|
@@ -114,6 +115,43 @@ export function cssActionMenuClasses() {
|
|
|
114
115
|
color: var(--sw-color-danger);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/*
|
|
119
|
+
* A command's detail: its current value at the inline end in the muted
|
|
120
|
+
* colour, then a chevron pointing to the inline end (mirrored right to
|
|
121
|
+
* left). A long value wraps between words, never inside one (the command
|
|
122
|
+
* itself may break anywhere), and the chevron stays last.
|
|
123
|
+
*/
|
|
124
|
+
.sw-action-menu-detail {
|
|
125
|
+
align-items: center;
|
|
126
|
+
color: var(--sw-color-muted);
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
gap: var(--sw-space-2);
|
|
129
|
+
margin-inline-start: auto;
|
|
130
|
+
overflow-wrap: normal;
|
|
131
|
+
padding-inline-start: var(--sw-space-3);
|
|
132
|
+
text-align: end;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.sw-action-menu-chevron {
|
|
136
|
+
${chevronStroke}
|
|
137
|
+
flex: none;
|
|
138
|
+
transform: rotate(-45deg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.sw-action-menu-chevron:dir(rtl) {
|
|
142
|
+
transform: rotate(135deg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* A hairline that sets a command apart from the ones above. */
|
|
146
|
+
.sw-action-menu-separator {
|
|
147
|
+
border-top: 1px solid var(--sw-color-border);
|
|
148
|
+
margin-block: var(--sw-space-1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@media (forced-colors: active) {
|
|
152
|
+
.sw-action-menu-chevron { border-color: CanvasText; }
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
/* A coarse pointer gets the full touch target: the trigger at least square, and every command as tall. */
|
|
118
156
|
@media ${coarsePointerQuery} {
|
|
119
157
|
.sw-action-menu-trigger {
|
|
@@ -135,5 +173,8 @@ export function actionMenuClassCatalog() {
|
|
|
135
173
|
'sw-action-menu-items',
|
|
136
174
|
'sw-action-menu-item',
|
|
137
175
|
'sw-action-menu-item-danger',
|
|
176
|
+
'sw-action-menu-detail',
|
|
177
|
+
'sw-action-menu-chevron',
|
|
178
|
+
'sw-action-menu-separator',
|
|
138
179
|
];
|
|
139
180
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
|
+
import { joinedZIndex } from './stacking.js';
|
|
3
|
+
/*
|
|
4
|
+
* An attached ButtonGroup is one joined control, such as Previous, Today
|
|
5
|
+
* and Next: its direct Button children side by side with no gap and one
|
|
6
|
+
* height, their borders overlapping into single dividers, the inner
|
|
7
|
+
* corners square and the outer ones the sm radius. It keeps its row on a
|
|
8
|
+
* phone (it never stacks) and sizes to its buttons, even in a Stack that
|
|
9
|
+
* would stretch it, so `justify` does not apply.
|
|
10
|
+
*
|
|
11
|
+
* A borderless variant (primary, tertiary, ghost, danger) draws its
|
|
12
|
+
* divider in its own text colour at low strength, so a joined row of them
|
|
13
|
+
* still reads as separate buttons; forced colours draw every border.
|
|
14
|
+
*
|
|
15
|
+
* The group is its own stacking context: a pressed button's ring paints
|
|
16
|
+
* over its neighbours and the focused button's standard outer ring over
|
|
17
|
+
* both, so neither is covered by the next button.
|
|
18
|
+
*/
|
|
19
|
+
export function cssButtonGroupAttachedClasses() {
|
|
20
|
+
const group = '.sw-button-group.sw-button-group-attached';
|
|
21
|
+
return `${group} {
|
|
22
|
+
align-items: stretch;
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
flex-wrap: nowrap;
|
|
25
|
+
gap: 0;
|
|
26
|
+
isolation: isolate;
|
|
27
|
+
max-width: 100%;
|
|
28
|
+
width: fit-content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
${group} > .sw-button {
|
|
32
|
+
border-radius: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
${group} > .sw-button + .sw-button {
|
|
36
|
+
margin-inline-start: -1px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
${group} > .sw-button:first-child {
|
|
40
|
+
border-end-start-radius: var(--sw-radius-sm);
|
|
41
|
+
border-start-start-radius: var(--sw-radius-sm);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
${group} > .sw-button:last-child {
|
|
45
|
+
border-end-end-radius: var(--sw-radius-sm);
|
|
46
|
+
border-start-end-radius: var(--sw-radius-sm);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
${group} > .sw-button + .sw-button:not(.sw-button-secondary) {
|
|
50
|
+
border-inline-start-color: color-mix(in srgb, currentColor 30%, transparent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
${group} > .sw-button[aria-pressed='true'] {
|
|
54
|
+
position: relative;
|
|
55
|
+
${joinedZIndex('pressed')}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
${group} > .sw-button:focus-visible {
|
|
59
|
+
position: relative;
|
|
60
|
+
${joinedZIndex('focused')}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media ${breakpointQuery('below', 'md')} {
|
|
64
|
+
${group} { align-items: stretch; flex-direction: row; }
|
|
65
|
+
${group} > * { width: auto; }
|
|
66
|
+
}`;
|
|
67
|
+
}
|
|
68
|
+
export function buttonGroupAttachedClassCatalog() {
|
|
69
|
+
return ['sw-button-group-attached'];
|
|
70
|
+
}
|
package/dist/css/css-button.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buttonSizes, buttonVariants, controlScale, typographyVariants, } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
|
+
import { buttonGroupAttachedClassCatalog, cssButtonGroupAttachedClasses, } from './css-button-group-attached.js';
|
|
3
4
|
const label = typographyVariants.label;
|
|
4
5
|
export function buttonClassNames(options) {
|
|
5
6
|
return [
|
|
@@ -93,7 +94,9 @@ export function cssButtonClasses() {
|
|
|
93
94
|
${variantRules()}
|
|
94
95
|
${pressedRules()}
|
|
95
96
|
${sizeRules()}
|
|
96
|
-
${groupRules()}
|
|
97
|
+
${groupRules()}
|
|
98
|
+
|
|
99
|
+
${cssButtonGroupAttachedClasses()}`;
|
|
97
100
|
}
|
|
98
101
|
export const buttonGroupJustifies = ['start', 'end', 'between'];
|
|
99
102
|
/** The action row: one line from md up, full-width stacked buttons below it. */
|
|
@@ -119,5 +122,6 @@ export function buttonClassCatalog() {
|
|
|
119
122
|
...buttonSizes.map((size) => `sw-button-${size}`),
|
|
120
123
|
'sw-button-group',
|
|
121
124
|
...buttonGroupJustifies.map((name) => `sw-button-group-${name}`),
|
|
125
|
+
...buttonGroupAttachedClassCatalog(),
|
|
122
126
|
];
|
|
123
127
|
}
|
|
@@ -55,6 +55,7 @@ function reducedTransparencyRules() {
|
|
|
55
55
|
.sw-badge-neutral,
|
|
56
56
|
.sw-segmented,
|
|
57
57
|
.sw-table-sticky thead th,
|
|
58
|
+
.sw-table .sw-table-footer-sticky :is(th, td),
|
|
58
59
|
.sw-denomination-strip .sw-denomination-label,
|
|
59
60
|
.sw-denomination-strip thead .sw-denomination-corner:first-child,
|
|
60
61
|
.sw-bar-chart-track,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
const label = typographyVariants.label;
|
|
3
3
|
const data = typographyVariants.data;
|
|
4
|
+
const caption = typographyVariants.caption;
|
|
4
5
|
export function cssFilterChipsClasses() {
|
|
5
6
|
return `.sw-filter-chips {
|
|
6
7
|
border: 0;
|
|
@@ -101,11 +102,35 @@ export function cssFilterChipsClasses() {
|
|
|
101
102
|
opacity: var(--sw-disabled-opacity);
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/*
|
|
106
|
+
* An error: the message under the chips in the danger colour (a polite live
|
|
107
|
+
* region, out of the layout while empty) and every chip's edge in danger,
|
|
108
|
+
* the chosen one too (it keeps its accent fill), in normal and forced
|
|
109
|
+
* colours alike.
|
|
110
|
+
*/
|
|
111
|
+
.sw-filter-chips-error {
|
|
112
|
+
color: var(--sw-color-danger);
|
|
113
|
+
display: block;
|
|
114
|
+
font-family: var(--sw-font-sans);
|
|
115
|
+
font-size: ${caption.fontSize}px;
|
|
116
|
+
line-height: ${caption.lineHeight}px;
|
|
117
|
+
padding-top: var(--sw-space-2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.sw-filter-chips-error:empty { position: absolute; }
|
|
121
|
+
|
|
122
|
+
.sw-filter-chips[aria-invalid='true'] .sw-filter-chip-face {
|
|
123
|
+
border-color: var(--sw-color-danger);
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
@media (forced-colors: active) {
|
|
105
127
|
.sw-filter-chip-face { background: Canvas; border-color: CanvasText; color: CanvasText; }
|
|
128
|
+
.sw-filter-chips-error { color: Mark; }
|
|
106
129
|
.sw-filter-chip-input:checked + .sw-filter-chip-face { background: Highlight; border-color: Highlight; color: HighlightText; }
|
|
107
130
|
.sw-filter-chip-input:focus-visible + .sw-filter-chip-face { outline-color: Highlight; }
|
|
108
131
|
.sw-filter-chip-count { color: inherit; }
|
|
132
|
+
/* After the checked rule, so every chip, the chosen one too, takes the edge. */
|
|
133
|
+
.sw-filter-chips[aria-invalid='true'] .sw-filter-chip-face { border-color: Mark; }
|
|
109
134
|
}`;
|
|
110
135
|
}
|
|
111
136
|
export function filterChipsClassCatalog() {
|
|
@@ -118,5 +143,6 @@ export function filterChipsClassCatalog() {
|
|
|
118
143
|
'sw-filter-chip-face',
|
|
119
144
|
'sw-filter-chip-count',
|
|
120
145
|
'sw-filter-chip-quiet',
|
|
146
|
+
'sw-filter-chips-error',
|
|
121
147
|
];
|
|
122
148
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** How a section navigation marks its current item. */
|
|
2
|
+
export declare const sectionNavAppearances: readonly ["underline", "filled"];
|
|
3
|
+
export type SectionNavAppearance = (typeof sectionNavAppearances)[number];
|
|
4
|
+
export declare function assertSectionNavAppearance(appearance: unknown): void;
|
|
5
|
+
export declare function cssSectionNavFilledClasses(): string;
|
|
6
|
+
export declare function sectionNavFilledClassCatalog(): string[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
3
|
+
const caption = typographyVariants.caption;
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
/** How a section navigation marks its current item. */
|
|
6
|
+
export const sectionNavAppearances = ['underline', 'filled'];
|
|
7
|
+
export function assertSectionNavAppearance(appearance) {
|
|
8
|
+
if (!sectionNavAppearances.includes(appearance))
|
|
9
|
+
throw new RangeError(`SectionNav appearance must be one of ${sectionNavAppearances.join(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
/*
|
|
12
|
+
* `filled`: a quiet bar of page buttons, as an area's sub-navigation beside
|
|
13
|
+
* its name. No hairline under the row and no underline or start bar, in
|
|
14
|
+
* the row and the side list; every link is the sm control (32; the 44 px
|
|
15
|
+
* target on a coarse pointer) with the sm radius and the control's
|
|
16
|
+
* padding at weight 500, and the current one takes a soft muted fill in
|
|
17
|
+
* the text colour at the label weight (600), so it reads apart from a
|
|
18
|
+
* hovered link. A 2 px transparent border keeps room for forced colours,
|
|
19
|
+
* where the current link's border turns Highlight.
|
|
20
|
+
*
|
|
21
|
+
* A count after a label (a number waiting there) is a filled pill: the
|
|
22
|
+
* danger colour with its on-colour figures (4.5:1 in every palette, as a
|
|
23
|
+
* danger Button's label), caption size at weight 700, a space-5 less
|
|
24
|
+
* space-1 (20) high and at least as wide. Forced colours draw its edge.
|
|
25
|
+
*/
|
|
26
|
+
export function cssSectionNavFilledClasses() {
|
|
27
|
+
// Two classes, so every filled rule outranks the side list's (one class
|
|
28
|
+
// and :dir), not by source order alone.
|
|
29
|
+
const filled = '.sw-section-nav.sw-section-nav-filled';
|
|
30
|
+
return `${filled} .sw-section-nav-list {
|
|
31
|
+
border-bottom: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* (0,4,0): outranks the underline and the side list's start bar. */
|
|
35
|
+
[data-theme] ${filled} .sw-section-nav-link {
|
|
36
|
+
border: 2px solid transparent;
|
|
37
|
+
border-radius: var(--sw-radius-sm);
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
margin-bottom: 0;
|
|
40
|
+
min-height: var(--sw-control-sm-min-height);
|
|
41
|
+
padding-inline: var(--sw-control-sm-padding-inline);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-theme] ${filled} .sw-section-nav-link[aria-current] {
|
|
45
|
+
background: color-mix(in srgb, var(--sw-color-muted) 18%, transparent);
|
|
46
|
+
border-color: transparent;
|
|
47
|
+
color: var(--sw-color-text);
|
|
48
|
+
font-weight: ${label.fontWeight};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[data-theme] .sw-section-nav-count {
|
|
52
|
+
align-items: center;
|
|
53
|
+
background: var(--sw-color-danger);
|
|
54
|
+
border-radius: var(--sw-radius-pill);
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
color: var(--sw-color-onDanger);
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
font-size: ${caption.fontSize}px;
|
|
59
|
+
font-variant-numeric: tabular-nums;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
63
|
+
line-height: 1;
|
|
64
|
+
min-height: calc(var(--sw-space-5) - var(--sw-space-1));
|
|
65
|
+
min-width: calc(var(--sw-space-5) - var(--sw-space-1));
|
|
66
|
+
padding-inline: var(--sw-space-1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media ${coarsePointerQuery} {
|
|
70
|
+
[data-theme] ${filled} .sw-section-nav-link { min-height: ${touchTarget}; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (forced-colors: active) {
|
|
74
|
+
[data-theme] ${filled} .sw-section-nav-link[aria-current] { border-color: Highlight; }
|
|
75
|
+
[data-theme] .sw-section-nav-count { border: 1px solid CanvasText; }
|
|
76
|
+
}`;
|
|
77
|
+
}
|
|
78
|
+
export function sectionNavFilledClassCatalog() {
|
|
79
|
+
return ['sw-section-nav-filled', 'sw-section-nav-count'];
|
|
80
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery, breakpoints, } from './breakpoints.js';
|
|
3
|
+
import { cssSectionNavFilledClasses, sectionNavFilledClassCatalog, } from './css-section-nav-filled.js';
|
|
3
4
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
4
5
|
const label = typographyVariants.label;
|
|
5
6
|
export function sectionNavVerticalClass(breakpoint) {
|
|
@@ -100,6 +101,8 @@ export function cssSectionNavClasses() {
|
|
|
100
101
|
|
|
101
102
|
${verticalRules()}
|
|
102
103
|
|
|
104
|
+
${cssSectionNavFilledClasses()}
|
|
105
|
+
|
|
103
106
|
@media ${coarsePointerQuery} {
|
|
104
107
|
[data-theme] .sw-section-nav-link { min-height: ${touchTarget}; }
|
|
105
108
|
}
|
|
@@ -116,5 +119,6 @@ export function sectionNavClassCatalog() {
|
|
|
116
119
|
'sw-section-nav-link',
|
|
117
120
|
'sw-section-nav-icon',
|
|
118
121
|
...breakpoints.map(sectionNavVerticalClass),
|
|
122
|
+
...sectionNavFilledClassCatalog(),
|
|
119
123
|
];
|
|
120
124
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { zIndex } from './stacking.js';
|
|
2
|
+
/*
|
|
3
|
+
* A table footer holds a total: its cells at weight 600 under a heavier
|
|
4
|
+
* rule, a 2 px line in the border colour across the top of its first row.
|
|
5
|
+
* The line is each cell's ::before (absolutely placed, a border, so forced
|
|
6
|
+
* colours keep it), not the cell's own collapsed border, which a sticky
|
|
7
|
+
* cell would leave behind as it moved.
|
|
8
|
+
*
|
|
9
|
+
* A sticky footer pins its cells to the bottom edge on the header's glass
|
|
10
|
+
* (solid surface under reduced transparency, the system canvas in forced
|
|
11
|
+
* colours, as the pinned header cells), on the `stickyFooter` layer: over
|
|
12
|
+
* the cells and the pinned header cells, under the ActionBar, which covers
|
|
13
|
+
* it, so a page does not pair the two. A table's region is a scroll box,
|
|
14
|
+
* which would pin it to the region's own bottom; while the table fits its
|
|
15
|
+
* width (`sw-scroll-fits`) a region that holds a sticky footer stops being
|
|
16
|
+
* one, so the footer pins to the page's bottom and the sticky header to
|
|
17
|
+
* its top, under any sticky top chrome (an AppHeader covers it). A table
|
|
18
|
+
* wider than its region scrolls sideways in it and pins to the region.
|
|
19
|
+
*/
|
|
20
|
+
export function cssTableFooterClasses() {
|
|
21
|
+
return `.sw-table tfoot :is(th, td) {
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
position: relative;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.sw-table tfoot > tr:first-child > :is(th, td)::before {
|
|
27
|
+
border-top: 2px solid var(--sw-color-border);
|
|
28
|
+
content: '';
|
|
29
|
+
/* Over the last body row's 1 px border, so the rule reads 2 px. */
|
|
30
|
+
inset-block-start: -1px;
|
|
31
|
+
inset-inline: 0;
|
|
32
|
+
pointer-events: none;
|
|
33
|
+
position: absolute;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sw-table .sw-table-footer-sticky :is(th, td) {
|
|
37
|
+
background: var(--sw-glass-fill);
|
|
38
|
+
backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
39
|
+
-webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
40
|
+
bottom: 0;
|
|
41
|
+
position: sticky;
|
|
42
|
+
${zIndex('stickyFooter')}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sw-table-wrap.sw-scroll-fits:has(> .sw-table > .sw-table-footer-sticky) {
|
|
46
|
+
overflow: visible;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (forced-colors: active) {
|
|
50
|
+
.sw-table tfoot > tr:first-child > :is(th, td)::before { border-color: CanvasText; }
|
|
51
|
+
.sw-table .sw-table-footer-sticky :is(th, td),
|
|
52
|
+
.sw-table-sticky thead th { background: Canvas; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
53
|
+
}`;
|
|
54
|
+
}
|
|
55
|
+
export function tableFooterClassCatalog() {
|
|
56
|
+
return ['sw-table-footer-sticky', 'sw-scroll-fits'];
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The colours a cell's text can take: a signed figure or a quiet value. */
|
|
2
|
+
export declare const tableCellTones: readonly ["success", "danger", "muted", "accent"];
|
|
3
|
+
export type TableCellTone = (typeof tableCellTones)[number];
|
|
4
|
+
/** A row's place in the table: a row of its own, or one under the row above. */
|
|
5
|
+
export declare const tableRowVariants: readonly ["default", "sub"];
|
|
6
|
+
export type TableRowVariant = (typeof tableRowVariants)[number];
|
|
7
|
+
export declare function assertTableCellTone(tone: unknown): void;
|
|
8
|
+
export declare function assertTableRowVariant(variant: unknown): void;
|
|
9
|
+
export declare function cssTableLedgerClasses(): string;
|
|
10
|
+
export declare function tableLedgerClassCatalog(): string[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** The colours a cell's text can take: a signed figure or a quiet value. */
|
|
2
|
+
export const tableCellTones = ['success', 'danger', 'muted', 'accent'];
|
|
3
|
+
/** A row's place in the table: a row of its own, or one under the row above. */
|
|
4
|
+
export const tableRowVariants = ['default', 'sub'];
|
|
5
|
+
export function assertTableCellTone(tone) {
|
|
6
|
+
if (tone !== undefined && !tableCellTones.includes(tone))
|
|
7
|
+
throw new RangeError(`Unknown TableCell tone: ${String(tone)}`);
|
|
8
|
+
}
|
|
9
|
+
export function assertTableRowVariant(variant) {
|
|
10
|
+
if (!tableRowVariants.includes(variant))
|
|
11
|
+
throw new RangeError(`Unknown TableRow variant: ${String(variant)}`);
|
|
12
|
+
}
|
|
13
|
+
function toneRules() {
|
|
14
|
+
return tableCellTones
|
|
15
|
+
.map((tone) => `.sw-table .sw-table-tone-${tone} {
|
|
16
|
+
color: var(--sw-color-${tone});
|
|
17
|
+
}`)
|
|
18
|
+
.join('\n\n');
|
|
19
|
+
}
|
|
20
|
+
/*
|
|
21
|
+
* Ledger rules, for a table read across as well as down.
|
|
22
|
+
*
|
|
23
|
+
* A cell tone colours the cell's text in the theme's semantic colour, as
|
|
24
|
+
* Text color does: 4.5:1 on the surface and the canvas in the default
|
|
25
|
+
* palettes and most named ones (the test names the few palette gaps). A
|
|
26
|
+
* struck row mutes its toned cells, so no tone sits on the subtle fill.
|
|
27
|
+
*
|
|
28
|
+
* Column rules draw a hairline at the inline start of every cell after a
|
|
29
|
+
* row's first, so bill columns read as columns. A border, so forced
|
|
30
|
+
* colours keep it and RTL mirrors it; a sticky header cell draws it on
|
|
31
|
+
* its ::after.
|
|
32
|
+
*
|
|
33
|
+
* A row header in the body (`th scope="row"`, such as an entry's name)
|
|
34
|
+
* reads as body text, not as a column header. The reset is no stronger
|
|
35
|
+
* than `.sw-table th`, so a cell's numeric, tone and struck rules win.
|
|
36
|
+
*
|
|
37
|
+
* A sub row sits under the row above it: its row header is indented a
|
|
38
|
+
* space-6 step with a guide bar at the indent, so a batch's items or a
|
|
39
|
+
* change's parts read as parts of one entry. The bar is the header's
|
|
40
|
+
* ::after (::before is the selected row's bar) and a border, so forced
|
|
41
|
+
* colours keep it.
|
|
42
|
+
*
|
|
43
|
+
* A struck row is a voided one kept on the record: the subtle fill, the
|
|
44
|
+
* text muted and struck through. A Badge in it (an inline-flex box) is not
|
|
45
|
+
* struck when it sits in a line of text (it is an atomic inline box; one
|
|
46
|
+
* that is itself a flex item is struck), so its tag, such as "Voided",
|
|
47
|
+
* stays plain; its words say why the row is struck, since a line-through
|
|
48
|
+
* is not read aloud.
|
|
49
|
+
*/
|
|
50
|
+
export function cssTableLedgerClasses() {
|
|
51
|
+
return `${toneRules()}
|
|
52
|
+
|
|
53
|
+
.sw-table-column-rules :is(th, td) + :is(th, td) {
|
|
54
|
+
border-inline-start: 1px solid var(--sw-color-border);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A sticky header cell is positioned, so the next cell's fill covers a
|
|
58
|
+
collapsed border; its rule is a border on its own ::after instead, so
|
|
59
|
+
forced colours keep it and the logical inset mirrors it. */
|
|
60
|
+
.sw-table-sticky.sw-table-column-rules thead :is(th, td) + :is(th, td)::after {
|
|
61
|
+
border-inline-start: 1px solid var(--sw-color-border);
|
|
62
|
+
content: '';
|
|
63
|
+
inset-block: 0;
|
|
64
|
+
inset-inline-start: 0;
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
position: absolute;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:where(.sw-table tbody, .sw-table tfoot) th[scope='row'] {
|
|
70
|
+
color: inherit;
|
|
71
|
+
font: inherit;
|
|
72
|
+
letter-spacing: inherit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.sw-table-row-sub > th[scope='row'] {
|
|
76
|
+
padding-inline-start: calc(var(--sw-space-3) + var(--sw-space-6) + var(--sw-space-3));
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sw-table-compact .sw-table-row-sub > th[scope='row'] {
|
|
81
|
+
padding-inline-start: calc(var(--sw-space-2) + var(--sw-space-6) + var(--sw-space-3));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.sw-table-row-sub > th[scope='row']::after {
|
|
85
|
+
block-size: var(--sw-space-5);
|
|
86
|
+
border-inline-start: 2px solid var(--sw-color-border);
|
|
87
|
+
content: '';
|
|
88
|
+
inset-block-start: 50%;
|
|
89
|
+
inset-inline-start: calc(var(--sw-space-3) + var(--sw-space-6));
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
position: absolute;
|
|
92
|
+
transform: translateY(-50%);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sw-table-compact .sw-table-row-sub > th[scope='row']::after {
|
|
96
|
+
inset-inline-start: calc(var(--sw-space-2) + var(--sw-space-6));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.sw-table .sw-table-row-struck > :is(td, th) {
|
|
100
|
+
background: var(--sw-color-subtle);
|
|
101
|
+
color: var(--sw-color-muted);
|
|
102
|
+
text-decoration: line-through;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sw-table .sw-table-row-struck > [class*='sw-table-tone-'] {
|
|
106
|
+
color: var(--sw-color-muted);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (forced-colors: active) {
|
|
110
|
+
.sw-table-column-rules :is(th, td) + :is(th, td),
|
|
111
|
+
.sw-table-sticky.sw-table-column-rules thead :is(th, td) + :is(th, td)::after { border-color: CanvasText; }
|
|
112
|
+
.sw-table-row-sub > th[scope='row']::after { border-color: CanvasText; }
|
|
113
|
+
}`;
|
|
114
|
+
}
|
|
115
|
+
export function tableLedgerClassCatalog() {
|
|
116
|
+
return [
|
|
117
|
+
...tableCellTones.map((tone) => `sw-table-tone-${tone}`),
|
|
118
|
+
'sw-table-column-rules',
|
|
119
|
+
'sw-table-row-sub',
|
|
120
|
+
'sw-table-row-struck',
|
|
121
|
+
];
|
|
122
|
+
}
|
package/dist/css/css-table.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { scrollRegionRules } from './css-scroll-region.js';
|
|
3
3
|
import { cssTableLayoutClasses, tableLayoutClassCatalog, } from './css-table-layout.js';
|
|
4
|
+
import { cssTableLedgerClasses, tableLedgerClassCatalog, } from './css-table-ledger.js';
|
|
5
|
+
import { cssTableFooterClasses, tableFooterClassCatalog, } from './css-table-footer.js';
|
|
4
6
|
import { cssTableSortClasses, tableSortClassCatalog, } from './css-table-sort.js';
|
|
5
7
|
import { zIndex } from './stacking.js';
|
|
6
8
|
const caption = typographyVariants.caption;
|
|
@@ -89,6 +91,10 @@ ${cssTableLayoutClasses()}
|
|
|
89
91
|
|
|
90
92
|
${cssTableSortClasses()}
|
|
91
93
|
|
|
94
|
+
${cssTableLedgerClasses()}
|
|
95
|
+
|
|
96
|
+
${cssTableFooterClasses()}
|
|
97
|
+
|
|
92
98
|
${selectedRowRules()}`;
|
|
93
99
|
}
|
|
94
100
|
export function tableClassCatalog() {
|
|
@@ -103,5 +109,7 @@ export function tableClassCatalog() {
|
|
|
103
109
|
'sw-table-row-selected',
|
|
104
110
|
...tableLayoutClassCatalog(),
|
|
105
111
|
...tableSortClassCatalog(),
|
|
112
|
+
...tableLedgerClassCatalog(),
|
|
113
|
+
...tableFooterClassCatalog(),
|
|
106
114
|
];
|
|
107
115
|
}
|