@scalewing/react 1.20.0 → 1.22.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 (69) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +47 -5
  3. package/dist/components/ActionMenu.d.ts +13 -1
  4. package/dist/components/ActionMenu.js +2 -2
  5. package/dist/components/BarChart.d.ts +28 -0
  6. package/dist/components/BarChart.js +5 -4
  7. package/dist/components/CalendarButton.d.ts +16 -0
  8. package/dist/components/CalendarButton.js +5 -3
  9. package/dist/components/DateField.d.ts +8 -1
  10. package/dist/components/DateField.js +2 -1
  11. package/dist/components/DenominationGrid.d.ts +2 -2
  12. package/dist/components/DenominationGrid.js +5 -2
  13. package/dist/components/Field.d.ts +9 -0
  14. package/dist/components/Field.js +3 -2
  15. package/dist/components/SectionNav.d.ts +44 -0
  16. package/dist/components/SectionNav.js +48 -0
  17. package/dist/components/Table.d.ts +38 -13
  18. package/dist/components/Table.js +7 -12
  19. package/dist/components/Tabs.d.ts +3 -1
  20. package/dist/components/Tabs.js +8 -2
  21. package/dist/components/Tooltip.js +19 -3
  22. package/dist/components/action-menu/ActionMenuList.d.ts +3 -2
  23. package/dist/components/action-menu/ActionMenuList.js +16 -4
  24. package/dist/components/date-field/CalendarDialog.d.ts +4 -1
  25. package/dist/components/date-field/CalendarDialog.js +2 -2
  26. package/dist/components/date-field/CalendarGrid.d.ts +4 -1
  27. package/dist/components/date-field/CalendarGrid.js +11 -3
  28. package/dist/components/date-field/use-calendar-popup.d.ts +6 -1
  29. package/dist/components/date-field/use-calendar-popup.js +5 -3
  30. package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
  31. package/dist/components/denomination-grid/DenominationStrip.js +17 -6
  32. package/dist/components/denomination-grid/types.d.ts +15 -2
  33. package/dist/components/table/TableCell.d.ts +42 -0
  34. package/dist/components/table/TableCell.js +27 -0
  35. package/dist/components/table/table-layout.d.ts +10 -0
  36. package/dist/components/table/table-layout.js +14 -0
  37. package/dist/components/tooltip/tooltip-open.d.ts +8 -0
  38. package/dist/components/tooltip/tooltip-open.js +60 -0
  39. package/dist/components/tooltip/use-tooltip-placement.d.ts +11 -0
  40. package/dist/components/tooltip/use-tooltip-placement.js +46 -0
  41. package/dist/components/use-anchored-popover.d.ts +2 -0
  42. package/dist/components/use-anchored-popover.js +2 -1
  43. package/dist/css/css-action-menu.js +24 -0
  44. package/dist/css/css-chart.js +45 -1
  45. package/dist/css/css-components.js +3 -0
  46. package/dist/css/css-date-field.js +47 -1
  47. package/dist/css/css-denomination-aligned.d.ts +7 -0
  48. package/dist/css/css-denomination-aligned.js +68 -0
  49. package/dist/css/css-denomination-grid.js +7 -1
  50. package/dist/css/css-field.js +14 -0
  51. package/dist/css/css-scroll-region.d.ts +8 -0
  52. package/dist/css/css-scroll-region.js +9 -3
  53. package/dist/css/css-section-nav.d.ts +4 -0
  54. package/dist/css/css-section-nav.js +120 -0
  55. package/dist/css/css-table-layout.d.ts +20 -0
  56. package/dist/css/css-table-layout.js +59 -0
  57. package/dist/css/css-table-sort.d.ts +2 -0
  58. package/dist/css/css-table-sort.js +105 -0
  59. package/dist/css/css-table.js +8 -0
  60. package/dist/css/css-tabs.d.ts +6 -1
  61. package/dist/css/css-tabs.js +9 -1
  62. package/dist/css/css-tooltip.js +7 -4
  63. package/dist/css/stylesheet.js +2 -0
  64. package/dist/date-only.d.ts +12 -0
  65. package/dist/date-only.js +24 -0
  66. package/dist/index.d.ts +4 -3
  67. package/dist/index.js +1 -0
  68. package/dist/styles.css +363 -6
  69. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A trigger that acts when pressed: a link, a button, a form control or an
3
+ * element with a widget role. A tap on it does what it does and does not
4
+ * also open the tooltip, which would stay over the result (or, after a
5
+ * link, over the next page).
6
+ */
7
+ const actingRoles = [
8
+ 'button',
9
+ 'checkbox',
10
+ 'combobox',
11
+ 'link',
12
+ 'menuitem',
13
+ 'menuitemcheckbox',
14
+ 'menuitemradio',
15
+ 'option',
16
+ 'radio',
17
+ 'searchbox',
18
+ 'slider',
19
+ 'spinbutton',
20
+ 'switch',
21
+ 'tab',
22
+ 'textbox',
23
+ 'treeitem',
24
+ ];
25
+ const actingTrigger = [
26
+ 'a[href]',
27
+ 'area[href]',
28
+ 'audio[controls]',
29
+ 'button',
30
+ 'input',
31
+ 'label',
32
+ 'select',
33
+ 'summary',
34
+ 'textarea',
35
+ 'video[controls]',
36
+ '[contenteditable]:not([contenteditable="false"])',
37
+ ...actingRoles.map((role) => `[role="${role}"]`),
38
+ ].join(', ');
39
+ /** Whether a tap at `target` lands on a control inside the tooltip's anchor. */
40
+ export function tapActs(target, anchor) {
41
+ if (!(target instanceof Element))
42
+ return false;
43
+ const control = target.closest(actingTrigger);
44
+ return control !== null && anchor.contains(control);
45
+ }
46
+ /**
47
+ * Whether the focus that just landed on `target` is visible: from the
48
+ * keyboard or a script, not a click or a tap, which hover and the tap's own
49
+ * rule already cover. A browser without `:focus-visible` counts it visible.
50
+ */
51
+ export function focusIsVisible(target) {
52
+ if (!(target instanceof Element))
53
+ return true;
54
+ try {
55
+ return target.matches(':focus-visible');
56
+ }
57
+ catch {
58
+ return true;
59
+ }
60
+ }
@@ -0,0 +1,11 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * While `shown`, puts the tooltip on the top layer as a manual popover and
4
+ * keeps it beside its anchor and inside the viewport: under the anchor (over
5
+ * it when there is no room below), lined up with its start or, where that
6
+ * would leave the screen, its end, a `space-2` inset from every edge. It
7
+ * follows scrolling, resizing, and a change in either element's size. While
8
+ * hidden the tooltip stays in the page (it may name its trigger) and is not
9
+ * a popover. Without the popover API it keeps the same fixed placement.
10
+ */
11
+ export declare function useTooltipPlacement(shown: boolean, tooltipRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>): void;
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+ import { useLayoutEffect } from 'react';
3
+ import { placePopover } from '../use-anchored-popover.js';
4
+ /**
5
+ * While `shown`, puts the tooltip on the top layer as a manual popover and
6
+ * keeps it beside its anchor and inside the viewport: under the anchor (over
7
+ * it when there is no room below), lined up with its start or, where that
8
+ * would leave the screen, its end, a `space-2` inset from every edge. It
9
+ * follows scrolling, resizing, and a change in either element's size. While
10
+ * hidden the tooltip stays in the page (it may name its trigger) and is not
11
+ * a popover. Without the popover API it keeps the same fixed placement.
12
+ */
13
+ export function useTooltipPlacement(shown, tooltipRef, anchorRef) {
14
+ useLayoutEffect(() => {
15
+ const node = tooltipRef.current;
16
+ const anchor = anchorRef.current;
17
+ if (!shown || !node || !anchor)
18
+ return;
19
+ const supportsPopover = typeof node.showPopover === 'function';
20
+ if (supportsPopover) {
21
+ node.setAttribute('popover', 'manual');
22
+ if (!node.matches(':popover-open'))
23
+ node.showPopover();
24
+ }
25
+ const place = () => {
26
+ if (node && anchor)
27
+ placePopover(node, anchor, { flipInline: true });
28
+ };
29
+ place();
30
+ const resizes = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null;
31
+ resizes?.observe(node);
32
+ resizes?.observe(anchor);
33
+ window.addEventListener('resize', place);
34
+ window.addEventListener('scroll', place, true);
35
+ return () => {
36
+ resizes?.disconnect();
37
+ window.removeEventListener('resize', place);
38
+ window.removeEventListener('scroll', place, true);
39
+ if (supportsPopover) {
40
+ if (node.matches(':popover-open'))
41
+ node.hidePopover();
42
+ node.removeAttribute('popover');
43
+ }
44
+ };
45
+ }, [anchorRef, shown, tooltipRef]);
46
+ }
@@ -6,6 +6,8 @@ export type AnchoredPopoverPlacement = {
6
6
  /** Line up with the anchor's other edge when the preferred one overflows. */
7
7
  flipInline?: boolean;
8
8
  };
9
+ /** Places a fixed (or top-layer) `node` beside `anchor`, inside the viewport. */
10
+ export declare function placePopover(node: HTMLElement, anchor: HTMLElement, { align, flipInline }: AnchoredPopoverPlacement): void;
9
11
  /**
10
12
  * Shows a mounted element on the top layer as a manual popover, keeps it
11
13
  * beside `anchorRef` through scroll, window resize, and a change in its own
@@ -13,7 +13,8 @@ function layoutViewport() {
13
13
  height: root.clientHeight || window.innerHeight,
14
14
  };
15
15
  }
16
- function placePopover(node, anchor, { align = 'start', flipInline = false }) {
16
+ /** Places a fixed (or top-layer) `node` beside `anchor`, inside the viewport. */
17
+ export function placePopover(node, anchor, { align = 'start', flipInline = false }) {
17
18
  const position = anchoredPosition(anchor.getBoundingClientRect(), { width: node.offsetWidth, height: node.offsetHeight }, layoutViewport(), {
18
19
  gap: spacingScale[1],
19
20
  inset: spacingScale[2],
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { zIndex } from './stacking.js';
3
3
  import { coarsePointerQuery, touchTarget } from './touch-target.js';
4
+ const caption = typographyVariants.caption;
4
5
  const label = typographyVariants.label;
5
6
  export function cssActionMenuClasses() {
6
7
  return `.sw-action-menu {
@@ -56,6 +57,27 @@ export function cssActionMenuClasses() {
56
57
  ${zIndex('popup')}
57
58
  }
58
59
 
60
+ /* Each child of the header is a line of its own: a name, then a role. */
61
+ .sw-action-menu-header {
62
+ border-bottom: 1px solid var(--sw-color-border);
63
+ color: var(--sw-color-muted);
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: var(--sw-space-1);
67
+ font-family: var(--sw-font-sans);
68
+ font-size: ${caption.fontSize}px;
69
+ letter-spacing: ${caption.letterSpacing}px;
70
+ line-height: ${caption.lineHeight}px;
71
+ margin-bottom: var(--sw-space-1);
72
+ /* The commands' inline padding, so the header's words line up with theirs. */
73
+ padding: var(--sw-space-1) var(--sw-control-xs-padding-inline) var(--sw-space-2);
74
+ }
75
+
76
+ /* With a header, the commands are the menu inside the popover. */
77
+ .sw-action-menu-items {
78
+ display: block;
79
+ }
80
+
59
81
  .sw-action-menu-item {
60
82
  align-items: center;
61
83
  appearance: none;
@@ -109,6 +131,8 @@ export function actionMenuClassCatalog() {
109
131
  'sw-action-menu',
110
132
  'sw-action-menu-trigger',
111
133
  'sw-action-menu-list',
134
+ 'sw-action-menu-header',
135
+ 'sw-action-menu-items',
112
136
  'sw-action-menu-item',
113
137
  'sw-action-menu-item-danger',
114
138
  ];
@@ -1,6 +1,47 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  const caption = typographyVariants.caption;
3
3
  const data = typographyVariants.data;
4
+ /*
5
+ * A diverging chart puts zero in the middle of each track, marked by a
6
+ * hairline: a positive bar starts there and grows toward the inline end, a
7
+ * negative one ends there and grows toward the start, each at most half the
8
+ * track. The fill keeps its share of the scale (`--sw-bar-fill`), so a
9
+ * bar is half as long as in the magnitude layout, on the same scale. The
10
+ * end at zero is square, so the bars meet the hairline.
11
+ */
12
+ function divergingRules() {
13
+ return `.sw-bar-chart-diverging .sw-bar-chart-track {
14
+ position: relative;
15
+ }
16
+
17
+ .sw-bar-chart-diverging .sw-bar-chart-track::before {
18
+ background: var(--sw-color-border);
19
+ content: '';
20
+ inset-block: 0;
21
+ inset-inline-start: 50%;
22
+ position: absolute;
23
+ width: 1px;
24
+ }
25
+
26
+ .sw-bar-chart-diverging .sw-bar-chart-fill {
27
+ border-end-start-radius: 0;
28
+ border-start-start-radius: 0;
29
+ margin-inline-start: 50%;
30
+ width: calc(var(--sw-bar-fill, 0) * 50%);
31
+ }
32
+
33
+ .sw-bar-chart-diverging .sw-bar-chart-fill-negative {
34
+ border-end-end-radius: 0;
35
+ border-end-start-radius: inherit;
36
+ border-start-end-radius: 0;
37
+ border-start-start-radius: inherit;
38
+ margin-inline-start: calc(50% - var(--sw-bar-fill, 0) * 50%);
39
+ }
40
+
41
+ @media (forced-colors: active) {
42
+ .sw-bar-chart-diverging .sw-bar-chart-track::before { background: CanvasText; }
43
+ }`;
44
+ }
4
45
  export function cssChartClasses() {
5
46
  return `.sw-bar-chart {
6
47
  display: grid;
@@ -80,7 +121,9 @@ export function cssChartClasses() {
80
121
  .sw-bar-chart-axis-track {
81
122
  display: flex;
82
123
  justify-content: space-between;
83
- }`;
124
+ }
125
+
126
+ ${divergingRules()}`;
84
127
  }
85
128
  export function chartClassCatalog() {
86
129
  return [
@@ -94,5 +137,6 @@ export function chartClassCatalog() {
94
137
  'sw-bar-chart-fill-negative',
95
138
  'sw-bar-chart-value',
96
139
  'sw-bar-chart-axis-track',
140
+ 'sw-bar-chart-diverging',
97
141
  ];
98
142
  }
@@ -7,6 +7,7 @@ import { cssChartClasses } from './css-chart.js';
7
7
  import { cssCheckboxClasses } from './css-checkbox.js';
8
8
  import { cssRadioGroupClasses } from './css-radio-group.js';
9
9
  import { cssChromeClasses } from './css-chrome.js';
10
+ import { cssSectionNavClasses } from './css-section-nav.js';
10
11
  import { cssDataClasses } from './css-data.js';
11
12
  import { cssSegmentedClasses } from './css-segmented.js';
12
13
  import { cssTableClasses } from './css-table.js';
@@ -123,6 +124,8 @@ ${cssChartClasses()}
123
124
 
124
125
  ${cssChromeClasses()}
125
126
 
127
+ ${cssSectionNavClasses()}
128
+
126
129
  ${cssDialogClasses()}
127
130
 
128
131
  ${cssAccordionClasses()}
@@ -229,16 +229,51 @@ function calendarRules() {
229
229
  background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
230
230
  }
231
231
 
232
+ /*
233
+ * A range's days are one band in each week row: square-cornered, the accent
234
+ * at 16 % over the calendar's glass, rounded at the range's start and end.
235
+ * accentSubtle is mixed against the surface, not the glass, and fades to
236
+ * nothing where the accent is near AA, so the band would vanish in most
237
+ * dark palettes. Every number on the band is in the text colour, which
238
+ * keeps 4.5:1 on it in every palette (muted and accent do not); a
239
+ * neighbouring month's day and today are marked by the band and today's
240
+ * accent ring (3:1 or more on the band). The selected day sits on top.
241
+ */
242
+ .sw-date-field-day-in-range {
243
+ background: color-mix(in srgb, var(--sw-color-accent) 16%, transparent);
244
+ border-radius: 0;
245
+ color: var(--sw-color-text);
246
+ }
247
+
248
+ .sw-date-field-day-range-start {
249
+ border-end-start-radius: var(--sw-radius-pill);
250
+ border-start-start-radius: var(--sw-radius-pill);
251
+ }
252
+
253
+ .sw-date-field-day-range-end {
254
+ border-end-end-radius: var(--sw-radius-pill);
255
+ border-start-end-radius: var(--sw-radius-pill);
256
+ }
257
+
232
258
  .sw-date-field-day[aria-current='date'] {
233
259
  border-color: var(--sw-color-accent);
234
260
  color: var(--sw-color-accent);
235
261
  font-weight: ${label.fontWeight};
236
262
  }
237
263
 
264
+ .sw-date-field-day-in-range[aria-current='date'] {
265
+ color: var(--sw-color-text);
266
+ }
267
+
268
+ .sw-date-field-day-in-range:hover:not([aria-disabled='true']) {
269
+ background: color-mix(in srgb, var(--sw-color-accent) 24%, transparent);
270
+ }
271
+
238
272
  .sw-date-field-day[aria-selected='true'],
239
273
  .sw-date-field-day[aria-selected='true']:hover {
240
274
  background: var(--sw-color-accent);
241
275
  border-color: var(--sw-color-accent);
276
+ border-radius: var(--sw-radius-pill);
242
277
  color: var(--sw-color-onAccent);
243
278
  font-weight: ${label.fontWeight};
244
279
  }
@@ -279,7 +314,15 @@ function adaptiveRules() {
279
314
  }
280
315
  .sw-date-field-calendar { border-color: CanvasText; }
281
316
  .sw-date-field-day[aria-current='date'] { border-color: CanvasText; }
282
- .sw-date-field-day[aria-selected='true'] {
317
+ .sw-date-field-day-in-range,
318
+ .sw-date-field-day-in-range:hover:not([aria-disabled='true']),
319
+ .sw-date-field-day-in-range[aria-current='date'] {
320
+ background: Mark;
321
+ color: MarkText;
322
+ forced-color-adjust: none;
323
+ }
324
+ .sw-date-field-day[aria-selected='true'],
325
+ .sw-date-field-day[aria-selected='true']:hover {
283
326
  background: Highlight;
284
327
  color: HighlightText;
285
328
  forced-color-adjust: none;
@@ -311,6 +354,9 @@ export function dateFieldClassCatalog() {
311
354
  'sw-date-field-grid',
312
355
  'sw-date-field-day',
313
356
  'sw-date-field-day-outside',
357
+ 'sw-date-field-day-in-range',
358
+ 'sw-date-field-day-range-start',
359
+ 'sw-date-field-day-range-end',
314
360
  'sw-date-field-footer',
315
361
  ];
316
362
  }
@@ -0,0 +1,7 @@
1
+ import { type TableColumnSize } from './css-table-layout.js';
2
+ /** The row-label widths a strip lines up at: the Table column sizes. */
3
+ export type DenominationLabelWidth = TableColumnSize;
4
+ export declare const denominationLabelWidths: DenominationLabelWidth[];
5
+ export declare function denominationLabelWidthClass(width: DenominationLabelWidth): string;
6
+ export declare function cssDenominationAlignedClasses(): string;
7
+ export declare function denominationAlignedClassCatalog(): string[];
@@ -0,0 +1,68 @@
1
+ import { breakpointQuery } from './breakpoints.js';
2
+ import { tableColumnSizes } from './css-table-layout.js';
3
+ export const denominationLabelWidths = Object.keys(tableColumnSizes);
4
+ export function denominationLabelWidthClass(width) {
5
+ return `sw-denomination-label-${width}`;
6
+ }
7
+ /*
8
+ * A strip with a label width lines its columns up with every other strip
9
+ * of the same columns in a container of the same width, such as the cards
10
+ * of a feed: the label column is that width, and the count columns (and the
11
+ * total column from md up, where it is drawn) share the rest of the scroll
12
+ * region's width equally, measured from the region itself (a container, so
13
+ * 100cqi is its width) rather than from the cells' content. Every column is
14
+ * sized, so the browser has no spare width to share by content. A count
15
+ * wider than its share widens its column, and the strip then scrolls as a
16
+ * plain one does. The component sets the column count and whether there is
17
+ * a total column (`--sw-denomination-columns`, `--sw-denomination-total`);
18
+ * below md the total column collapses, so it takes no share.
19
+ */
20
+ export function cssDenominationAlignedClasses() {
21
+ const widths = Object.entries(tableColumnSizes)
22
+ .map(([size, rem]) => `.${denominationLabelWidthClass(size)} { --sw-denomination-label-width: ${rem}rem; }`)
23
+ .join('\n');
24
+ const strip = '.sw-denomination-strip-aligned';
25
+ return `.sw-denomination-scroll-aligned {
26
+ container-type: inline-size;
27
+ }
28
+
29
+ ${widths}
30
+
31
+ ${strip} {
32
+ --sw-denomination-total-shown: 1;
33
+ --sw-denomination-share: calc(
34
+ (100cqi - var(--sw-denomination-label-width)) /
35
+ (var(--sw-denomination-columns) + var(--sw-denomination-total) * var(--sw-denomination-total-shown))
36
+ );
37
+ }
38
+
39
+ ${strip} th,
40
+ ${strip} td {
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ ${strip} .sw-denomination-label,
45
+ ${strip} thead .sw-denomination-corner:first-child {
46
+ width: var(--sw-denomination-label-width);
47
+ }
48
+
49
+ ${strip} .sw-denomination-cell,
50
+ ${strip} .sw-denomination-head:not(.sw-denomination-total-head),
51
+ ${strip} .sw-denomination-total,
52
+ ${strip} .sw-denomination-total-head {
53
+ width: var(--sw-denomination-share);
54
+ }
55
+
56
+ @media ${breakpointQuery('below', 'md')} {
57
+ ${strip} { --sw-denomination-total-shown: 0; }
58
+ ${strip} .sw-denomination-total,
59
+ ${strip} .sw-denomination-total-head { width: 0; }
60
+ }`;
61
+ }
62
+ export function denominationAlignedClassCatalog() {
63
+ return [
64
+ 'sw-denomination-scroll-aligned',
65
+ 'sw-denomination-strip-aligned',
66
+ ...denominationLabelWidths.map(denominationLabelWidthClass),
67
+ ];
68
+ }
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { breakpointQuery } from './breakpoints.js';
3
3
  import { badgeTones } from './css-data.js';
4
+ import { cssDenominationAlignedClasses, denominationAlignedClassCatalog, } from './css-denomination-aligned.js';
4
5
  import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
5
6
  import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
6
7
  import { visuallyHiddenDeclarations } from './css-utilities.js';
@@ -235,7 +236,8 @@ ${pinnedStartShadeRules('.sw-denomination-scroll', [
235
236
  @media ${breakpointQuery('below', 'md')} {
236
237
  .sw-denomination-strip .sw-denomination-total,
237
238
  .sw-denomination-strip .sw-denomination-total-head { padding: 0; position: relative; }
238
- .sw-denomination-total-value { ${visuallyHiddenDeclarations} }
239
+ .sw-denomination-total-value,
240
+ .sw-denomination-total-label { ${visuallyHiddenDeclarations} }
239
241
  .sw-denomination-total-inline { display: block; }
240
242
  .sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
241
243
  .sw-denomination-strip .sw-denomination-label-line { gap: var(--sw-space-1); }
@@ -247,6 +249,8 @@ ${pinnedStartShadeRules('.sw-denomination-scroll', [
247
249
 
248
250
  ${cssDenominationCellToneClasses()}
249
251
 
252
+ ${cssDenominationAlignedClasses()}
253
+
250
254
  @media (forced-colors: active) {
251
255
  .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
252
256
  .sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
@@ -279,11 +283,13 @@ export function denominationGridClassCatalog() {
279
283
  'sw-denomination-total',
280
284
  'sw-denomination-total-inline',
281
285
  'sw-denomination-total-head',
286
+ 'sw-denomination-total-label',
282
287
  'sw-denomination-total-value',
283
288
  'sw-denomination-tiles',
284
289
  'sw-denomination-tile-list',
285
290
  'sw-denomination-tile',
286
291
  'sw-denomination-subtotal',
287
292
  ...denominationCellToneClassCatalog(),
293
+ ...denominationAlignedClassCatalog(),
288
294
  ];
289
295
  }
@@ -80,11 +80,24 @@ export function cssFieldClasses() {
80
80
  border-color: var(--sw-color-danger);
81
81
  }
82
82
 
83
+ /*
84
+ * A changed value: the accent border, a hairline thicker through an inset
85
+ * shadow so nothing moves, on the control or on an adorned frame (whose
86
+ * input draws no border of its own).
87
+ */
88
+ [data-theme] .sw-field-changed > :is(input, select, textarea),
89
+ .sw-field-changed .sw-field-adorned {
90
+ border-color: var(--sw-color-accent);
91
+ box-shadow: inset 0 0 0 1px var(--sw-color-accent);
92
+ }
93
+
83
94
  ${adornmentRules()}
84
95
 
85
96
  @media (forced-colors: active) {
86
97
  [data-theme] .sw-field-invalid :is(input, select, textarea),
87
98
  .sw-field-invalid .sw-field-adorned { border-color: Mark; }
99
+ [data-theme] .sw-field-changed > :is(input, select, textarea),
100
+ .sw-field-changed .sw-field-adorned { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
88
101
  .sw-field-prefix, .sw-field-suffix { color: CanvasText; }
89
102
  .sw-field-adorned:has(> input:disabled) {
90
103
  ${disabledControlForcedColors}
@@ -99,6 +112,7 @@ export function fieldClassCatalog() {
99
112
  'sw-field-description',
100
113
  'sw-field-error',
101
114
  'sw-field-invalid',
115
+ 'sw-field-changed',
102
116
  'sw-field-adorned',
103
117
  'sw-field-prefix',
104
118
  'sw-field-suffix',
@@ -1,3 +1,11 @@
1
+ /**
2
+ * The shade each inline edge of `selector` draws while `useScrollOverflow`
3
+ * marks it (`sw-scroll-more-start`, `sw-scroll-more-end`). The start edge is
4
+ * the left in a left-to-right box and the right in RTL. A box that scrolls
5
+ * itself (a `Tabs` strip) takes these alone; a scroll region takes them
6
+ * through `scrollRegionRules`.
7
+ */
8
+ export declare function scrollShadeRules(selector: string): string;
1
9
  /**
2
10
  * A scroll region is a full-width box that scrolls its own content when the
3
11
  * content is wider (or taller) than the box, so a wide table never widens the
@@ -12,8 +12,14 @@ function edgeShade(side) {
12
12
  const offset = side === 'left' ? 'var(--sw-space-6)' : 'calc(-1 * var(--sw-space-6))';
13
13
  return `inset ${offset} 0 var(--sw-space-5) calc(-1 * var(--sw-space-5)) ${shadeColor}`;
14
14
  }
15
- /** The start edge is the left in a left-to-right box and the right in RTL. */
16
- function edgeShadeRules(selector) {
15
+ /**
16
+ * The shade each inline edge of `selector` draws while `useScrollOverflow`
17
+ * marks it (`sw-scroll-more-start`, `sw-scroll-more-end`). The start edge is
18
+ * the left in a left-to-right box and the right in RTL. A box that scrolls
19
+ * itself (a `Tabs` strip) takes these alone; a scroll region takes them
20
+ * through `scrollRegionRules`.
21
+ */
22
+ export function scrollShadeRules(selector) {
17
23
  const more = (edge) => `${selector}.sw-scroll-more-${edge}`;
18
24
  const both = `${more('start')}.sw-scroll-more-end`;
19
25
  return [
@@ -47,7 +53,7 @@ ${selector}:focus-visible {
47
53
  outline-offset: calc(-1 * var(--sw-focus-ring-width));
48
54
  }
49
55
 
50
- ${edgeShadeRules(selector)}`;
56
+ ${scrollShadeRules(selector)}`;
51
57
  }
52
58
  export function scrollRegionClassCatalog() {
53
59
  return ['sw-scroll-more-start', 'sw-scroll-more-end'];
@@ -0,0 +1,4 @@
1
+ import { type Breakpoint } from './breakpoints.js';
2
+ export declare function sectionNavVerticalClass(breakpoint: Breakpoint): string;
3
+ export declare function cssSectionNavClasses(): string;
4
+ export declare function sectionNavClassCatalog(): string[];
@@ -0,0 +1,120 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery, breakpoints, } from './breakpoints.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
4
+ const label = typographyVariants.label;
5
+ export function sectionNavVerticalClass(breakpoint) {
6
+ return `sw-section-nav-vertical-from-${breakpoint}`;
7
+ }
8
+ /*
9
+ * From the breakpoint up the row becomes a side list: one item per line,
10
+ * the full width of its column, the current one marked by an accent bar at
11
+ * its inline start instead of the underline.
12
+ */
13
+ function verticalRules() {
14
+ return breakpoints
15
+ .map((breakpoint) => {
16
+ const nav = `.${sectionNavVerticalClass(breakpoint)}`;
17
+ return `@media ${breakpointQuery('from', breakpoint)} {
18
+ ${nav} .sw-section-nav-list {
19
+ border-bottom: 0;
20
+ flex-direction: column;
21
+ gap: var(--sw-space-1);
22
+ }
23
+
24
+ ${nav} .sw-section-nav-link {
25
+ border-bottom: 0;
26
+ border-inline-start: 2px solid transparent;
27
+ border-radius: 0 var(--sw-radius-sm) var(--sw-radius-sm) 0;
28
+ margin-bottom: 0;
29
+ width: 100%;
30
+ }
31
+
32
+ ${nav} .sw-section-nav-link:dir(rtl) {
33
+ border-radius: var(--sw-radius-sm) 0 0 var(--sw-radius-sm);
34
+ }
35
+
36
+ ${nav} .sw-section-nav-link[aria-current] {
37
+ border-inline-start-color: var(--sw-color-accent);
38
+ }
39
+ }`;
40
+ })
41
+ .join('\n\n');
42
+ }
43
+ /*
44
+ * A section navigation is quieter than a workspace's: label-size words in
45
+ * the muted color with no fill, the current item in the text color over an
46
+ * accent underline on the row's hairline (as a tab strip marks its tab),
47
+ * and a soft fill on hover. A link that is the current page or holds it is
48
+ * marked the same way. A coarse pointer gets the 44 px target. The link
49
+ * rules start at the canvas (`[data-theme]`) to outrank its accent links.
50
+ */
51
+ export function cssSectionNavClasses() {
52
+ return `.sw-section-nav-list {
53
+ border-bottom: 1px solid var(--sw-color-border);
54
+ display: flex;
55
+ flex-wrap: wrap;
56
+ gap: var(--sw-space-1) var(--sw-space-2);
57
+ list-style: none;
58
+ margin: 0;
59
+ padding: 0;
60
+ }
61
+
62
+ [data-theme] .sw-section-nav-link {
63
+ align-items: center;
64
+ border-bottom: 2px solid transparent;
65
+ border-radius: var(--sw-radius-sm) var(--sw-radius-sm) 0 0;
66
+ box-sizing: border-box;
67
+ color: var(--sw-color-muted);
68
+ display: inline-flex;
69
+ font-family: var(--sw-font-sans);
70
+ font-size: ${label.fontSize}px;
71
+ font-weight: ${label.fontWeight};
72
+ gap: var(--sw-space-2);
73
+ letter-spacing: ${label.letterSpacing}px;
74
+ line-height: ${label.lineHeight}px;
75
+ margin-bottom: -1px;
76
+ min-height: var(--sw-control-sm-min-height);
77
+ padding-inline: var(--sw-control-xs-padding-inline);
78
+ text-decoration: none;
79
+ }
80
+
81
+ [data-theme] .sw-section-nav-link:hover {
82
+ background: color-mix(in srgb, var(--sw-color-muted) 12%, transparent);
83
+ color: var(--sw-color-text);
84
+ }
85
+
86
+ [data-theme] .sw-section-nav-link:focus-visible {
87
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
88
+ outline-offset: calc(-1 * var(--sw-focus-ring-width));
89
+ }
90
+
91
+ [data-theme] .sw-section-nav-link[aria-current] {
92
+ border-bottom-color: var(--sw-color-accent);
93
+ color: var(--sw-color-text);
94
+ }
95
+
96
+ .sw-section-nav-icon {
97
+ display: inline-flex;
98
+ flex: none;
99
+ }
100
+
101
+ ${verticalRules()}
102
+
103
+ @media ${coarsePointerQuery} {
104
+ [data-theme] .sw-section-nav-link { min-height: ${touchTarget}; }
105
+ }
106
+
107
+ @media (forced-colors: active) {
108
+ [data-theme] .sw-section-nav-link { color: LinkText; }
109
+ [data-theme] .sw-section-nav-link[aria-current] { border-color: Highlight; color: CanvasText; }
110
+ }`;
111
+ }
112
+ export function sectionNavClassCatalog() {
113
+ return [
114
+ 'sw-section-nav',
115
+ 'sw-section-nav-list',
116
+ 'sw-section-nav-link',
117
+ 'sw-section-nav-icon',
118
+ ...breakpoints.map(sectionNavVerticalClass),
119
+ ];
120
+ }