@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +39 -1
  3. package/dist/accessible-name.d.ts +13 -0
  4. package/dist/accessible-name.js +1 -0
  5. package/dist/components/DataBar.d.ts +48 -0
  6. package/dist/components/DataBar.js +28 -0
  7. package/dist/components/Pagination.d.ts +34 -0
  8. package/dist/components/Pagination.js +27 -0
  9. package/dist/components/ScrollRegion.d.ts +9 -5
  10. package/dist/components/ScrollRegion.js +8 -6
  11. package/dist/components/SectionNav.d.ts +2 -8
  12. package/dist/components/SegmentedControl.d.ts +2 -8
  13. package/dist/components/Table.d.ts +48 -4
  14. package/dist/components/Table.js +9 -3
  15. package/dist/components/Tabs.d.ts +2 -9
  16. package/dist/components/table/TableRow.d.ts +8 -2
  17. package/dist/components/table/TableRow.js +1 -1
  18. package/dist/components/table/table-max-height.d.ts +20 -0
  19. package/dist/components/table/table-max-height.js +23 -0
  20. package/dist/components/use-scroll-overflow.d.ts +10 -8
  21. package/dist/components/use-scroll-overflow.js +23 -10
  22. package/dist/css/chevron.d.ts +6 -3
  23. package/dist/css/chevron.js +9 -4
  24. package/dist/css/css-components.js +6 -0
  25. package/dist/css/css-data-bar.d.ts +16 -0
  26. package/dist/css/css-data-bar.js +107 -0
  27. package/dist/css/css-pagination.d.ts +2 -0
  28. package/dist/css/css-pagination.js +63 -0
  29. package/dist/css/css-scroll-region.d.ts +1 -0
  30. package/dist/css/css-scroll-region.js +10 -5
  31. package/dist/css/css-table-bounded-edges.d.ts +1 -0
  32. package/dist/css/css-table-bounded-edges.js +132 -0
  33. package/dist/css/css-table-bounded.d.ts +2 -0
  34. package/dist/css/css-table-bounded.js +73 -0
  35. package/dist/css/css-table-ledger.d.ts +5 -2
  36. package/dist/css/css-table-ledger.js +5 -2
  37. package/dist/css/css-table-metrics.d.ts +14 -0
  38. package/dist/css/css-table-metrics.js +13 -0
  39. package/dist/css/css-table-sort.js +2 -1
  40. package/dist/css/css-table-total-row.d.ts +2 -0
  41. package/dist/css/css-table-total-row.js +29 -0
  42. package/dist/css/css-table.js +14 -3
  43. package/dist/css/stylesheet.js +4 -0
  44. package/dist/data-bar-geometry.d.ts +24 -0
  45. package/dist/data-bar-geometry.js +41 -0
  46. package/dist/index.d.ts +3 -1
  47. package/dist/index.js +2 -0
  48. package/dist/pagination-pages.d.ts +13 -0
  49. package/dist/pagination-pages.js +22 -0
  50. package/dist/scroll-overflow.d.ts +12 -0
  51. package/dist/scroll-overflow.js +31 -7
  52. package/dist/styles.css +223 -0
  53. 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 inline edges of a scroll box have more content past them:
10
- * on mount, on every scroll, and whenever the box or its content changes
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.
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 = scrollOverflow(box.scrollLeft, box.scrollWidth, box.clientWidth);
29
- setOverflow((current) => current.measured && sameScrollOverflow(current, next)
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 });
@@ -1,5 +1,8 @@
1
1
  /**
2
- * The stroked chevron Accordion and Select share: two muted sides of a
3
- * `space-2` square, turned by each surface to point where it needs.
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 const chevronStroke = "border-bottom: 2px solid var(--sw-color-muted);\n border-right: 2px solid var(--sw-color-muted);\n box-sizing: border-box;\n height: var(--sw-space-2);\n width: var(--sw-space-2);";
6
+ export declare function chevronStrokeIn(color: string): string;
7
+ /** The muted chevron Accordion, Select and the menus share. */
8
+ export declare const chevronStroke: string;
@@ -1,9 +1,14 @@
1
1
  /**
2
- * The stroked chevron Accordion and Select share: two muted sides of a
3
- * `space-2` square, turned by each surface to point where it needs.
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 const chevronStroke = `border-bottom: 2px solid var(--sw-color-muted);
6
- border-right: 2px solid var(--sw-color-muted);
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,2 @@
1
+ export declare function cssPaginationClasses(): string;
2
+ export declare function paginationClassCatalog(): string[];
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ export declare const scrollShadeColor = "color-mix(in srgb, var(--sw-color-text) 28%, transparent)";
1
2
  /**
2
3
  * The shade each inline edge of `selector` draws while `useScrollOverflow`
3
4
  * marks it (`sw-scroll-more-start`, `sw-scroll-more-end`). The start edge is
@@ -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 shadeColor = 'color-mix(in srgb, var(--sw-color-text) 28%, transparent)';
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)) ${shadeColor}`;
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 ['sw-scroll-more-start', 'sw-scroll-more-end'];
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, ${shadeColor}, transparent);
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, ${shadeColor}, transparent);
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,2 @@
1
+ export declare function cssTableBoundedClasses(): string;
2
+ export declare function tableBoundedClassCatalog(): string[];
@@ -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
- /** 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"];
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
- /** A row's place in the table: a row of its own, or one under the row above. */
4
- export const tableRowVariants = ['default', 'sub'];
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: var(--sw-control-xs-min-height);
67
+ min-height: ${tableSortMinHeight};
67
68
  padding: 0 var(--sw-space-1);
68
69
  text-align: inherit;
69
70
  }
@@ -0,0 +1,2 @@
1
+ export declare function cssTableTotalRowClasses(): string;
2
+ export declare function tableTotalRowClassCatalog(): string[];