@scalewing/react 1.16.0 → 1.18.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 (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +27 -4
  3. package/dist/components/ActionBar.d.ts +16 -6
  4. package/dist/components/ActionBar.js +1 -1
  5. package/dist/components/DenominationGrid.d.ts +2 -2
  6. package/dist/components/DenominationGrid.js +4 -2
  7. package/dist/components/Dialog.d.ts +32 -1
  8. package/dist/components/Dialog.js +4 -3
  9. package/dist/components/ScrollRegion.d.ts +4 -1
  10. package/dist/components/ScrollRegion.js +11 -2
  11. package/dist/components/Tabs.d.ts +22 -2
  12. package/dist/components/Tabs.js +4 -3
  13. package/dist/components/denomination-grid/DenominationStrip.js +2 -1
  14. package/dist/components/denomination-grid/DenominationTiles.d.ts +2 -2
  15. package/dist/components/denomination-grid/DenominationTiles.js +12 -7
  16. package/dist/components/denomination-grid/types.d.ts +19 -0
  17. package/dist/components/denomination-grid/validate.js +17 -0
  18. package/dist/components/dialog/DialogTitle.d.ts +15 -0
  19. package/dist/components/dialog/DialogTitle.js +14 -0
  20. package/dist/components/use-scroll-overflow.d.ts +9 -0
  21. package/dist/components/use-scroll-overflow.js +44 -0
  22. package/dist/css/css-action-bar.js +7 -0
  23. package/dist/css/css-denomination-cell-tones.d.ts +9 -0
  24. package/dist/css/css-denomination-cell-tones.js +52 -0
  25. package/dist/css/css-denomination-grid.js +10 -1
  26. package/dist/css/css-dialog.d.ts +2 -0
  27. package/dist/css/css-dialog.js +125 -3
  28. package/dist/css/css-scroll-region.d.ts +16 -2
  29. package/dist/css/css-scroll-region.js +68 -2
  30. package/dist/css/css-tabs.d.ts +1 -0
  31. package/dist/css/css-tabs.js +42 -1
  32. package/dist/css/stacking.js +3 -2
  33. package/dist/css/stylesheet.js +2 -0
  34. package/dist/denomination-cells.d.ts +3 -1
  35. package/dist/denomination-cells.js +6 -2
  36. package/dist/index.d.ts +2 -2
  37. package/dist/scroll-overflow.d.ts +14 -0
  38. package/dist/scroll-overflow.js +26 -0
  39. package/dist/styles.css +158 -0
  40. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ import { type BadgeTone } from './css-data.js';
2
+ /**
3
+ * The classes that tone one count: a marker that applies the shared rules and
4
+ * the tone that sets their color. A strip cell (`td`) and a tile (`li`) take
5
+ * the same pair; null or `neutral` leaves the cell untoned.
6
+ */
7
+ export declare function denominationCellToneClassNames(tone: BadgeTone | null | undefined): string[];
8
+ export declare function cssDenominationCellToneClasses(): string;
9
+ export declare function denominationCellToneClassCatalog(): string[];
@@ -0,0 +1,52 @@
1
+ import { badgeTones } from './css-data.js';
2
+ /**
3
+ * The tones a single count can take. `neutral` is the cell's own look, so it
4
+ * adds no class: a cell is toned only by a tone that says something.
5
+ */
6
+ const cellTones = badgeTones.filter((tone) => tone !== 'neutral');
7
+ /**
8
+ * The classes that tone one count: a marker that applies the shared rules and
9
+ * the tone that sets their color. A strip cell (`td`) and a tile (`li`) take
10
+ * the same pair; null or `neutral` leaves the cell untoned.
11
+ */
12
+ export function denominationCellToneClassNames(tone) {
13
+ if (!tone || tone === 'neutral')
14
+ return [];
15
+ return ['sw-denomination-cell-toned', `sw-denomination-cell-tone-${tone}`];
16
+ }
17
+ /*
18
+ * A toned count is set in its tone, over a row's signed coloring and a
19
+ * zero's quiet opacity, and a toned tile draws its border in the tone, one
20
+ * hairline thicker with an inset shadow so no tile moves. The tone never
21
+ * carries meaning alone: the consumer's words say why a count is toned.
22
+ * Forced colors replace the tone with the system text color, and a toned
23
+ * zero keeps the zero's GrayText there, so it does not read as a count.
24
+ */
25
+ export function cssDenominationCellToneClasses() {
26
+ const toneRules = cellTones
27
+ .map((tone) => `.sw-denomination-cell-tone-${tone} { --sw-denomination-cell-tone: var(--sw-color-${tone}); }`)
28
+ .join('\n');
29
+ return `${toneRules}
30
+
31
+ .sw-denomination-grid .sw-denomination-cell.sw-denomination-cell-toned,
32
+ .sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell {
33
+ color: var(--sw-denomination-cell-tone);
34
+ opacity: 1;
35
+ }
36
+
37
+ .sw-denomination-tile.sw-denomination-cell-toned {
38
+ border-color: var(--sw-denomination-cell-tone);
39
+ box-shadow: inset 0 0 0 1px var(--sw-denomination-cell-tone);
40
+ }
41
+
42
+ @media (forced-colors: active) {
43
+ .sw-denomination-grid .sw-denomination-cell-zero.sw-denomination-cell-toned,
44
+ .sw-denomination-grid .sw-denomination-cell-toned .sw-denomination-cell-zero { color: GrayText; }
45
+ }`;
46
+ }
47
+ export function denominationCellToneClassCatalog() {
48
+ return [
49
+ 'sw-denomination-cell-toned',
50
+ ...cellTones.map((tone) => `sw-denomination-cell-tone-${tone}`),
51
+ ];
52
+ }
@@ -1,7 +1,8 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { breakpointQuery } from './breakpoints.js';
3
3
  import { badgeTones } from './css-data.js';
4
- import { scrollRegionRules } from './css-scroll-region.js';
4
+ import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
5
+ import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
5
6
  import { visuallyHiddenDeclarations } from './css-utilities.js';
6
7
  import { zIndex } from './stacking.js';
7
8
  const caption = typographyVariants.caption;
@@ -25,6 +26,11 @@ ${toneRules()}
25
26
 
26
27
  ${scrollRegionRules('.sw-denomination-scroll')}
27
28
 
29
+ ${pinnedStartShadeRules('.sw-denomination-scroll', [
30
+ '.sw-denomination-strip .sw-denomination-label',
31
+ '.sw-denomination-strip thead .sw-denomination-corner:first-child',
32
+ ])}
33
+
28
34
  .sw-denomination-strip {
29
35
  border-collapse: collapse;
30
36
  width: 100%;
@@ -239,6 +245,8 @@ ${scrollRegionRules('.sw-denomination-scroll')}
239
245
  .sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
240
246
  }
241
247
 
248
+ ${cssDenominationCellToneClasses()}
249
+
242
250
  @media (forced-colors: active) {
243
251
  .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
244
252
  .sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
@@ -276,5 +284,6 @@ export function denominationGridClassCatalog() {
276
284
  'sw-denomination-tile-list',
277
285
  'sw-denomination-tile',
278
286
  'sw-denomination-subtotal',
287
+ ...denominationCellToneClassCatalog(),
279
288
  ];
280
289
  }
@@ -1,2 +1,4 @@
1
+ import { type Breakpoint } from './breakpoints.js';
2
+ export declare function dialogSheetClass(sheetBelow: Breakpoint): string;
1
3
  export declare function cssDialogClasses(): string;
2
4
  export declare function dialogClassCatalog(): string[];
@@ -1,3 +1,112 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery, breakpoints, } from './breakpoints.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
4
+ const title = typographyVariants.title;
5
+ /* The modal's height cap: a space-8 strip of backdrop stays in view. */
6
+ const maxHeight = 'min(100vh - var(--sw-space-8), 100dvh - var(--sw-space-8))';
7
+ export function dialogSheetClass(sheetBelow) {
8
+ return `sw-dialog-sheet-below-${sheetBelow}`;
9
+ }
10
+ /*
11
+ * Below the breakpoint the dialog is a bottom sheet: docked to the bottom
12
+ * edge at the full viewport width, the top corners rounded and the bottom
13
+ * ones square, the same height cap (so the whole space-8 strip of backdrop
14
+ * shows above it), and padding that clears the safe areas. Emitted after
15
+ * `sw-dialog-lg` so it wins at equal specificity. No grabber: there is no
16
+ * drag gesture.
17
+ */
18
+ function sheetRules() {
19
+ return breakpoints
20
+ .map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
21
+ .${dialogSheetClass(breakpoint)} {
22
+ border-radius: var(--sw-radius-lg) var(--sw-radius-lg) 0 0;
23
+ border-width: 1px 0 0;
24
+ margin: auto 0 0;
25
+ max-height: ${maxHeight};
26
+ max-width: none;
27
+ padding-bottom: calc(var(--sw-space-5) + env(safe-area-inset-bottom, 0px));
28
+ padding-left: max(var(--sw-space-5), env(safe-area-inset-left, 0px));
29
+ padding-right: max(var(--sw-space-5), env(safe-area-inset-right, 0px));
30
+ width: 100%;
31
+ }
32
+
33
+ .${dialogSheetClass(breakpoint)}[open] {
34
+ animation: sw-dialog-sheet-in var(--sw-motion-default) var(--sw-motion-easing);
35
+ }
36
+ }`)
37
+ .join('\n\n');
38
+ }
39
+ function sheetMotionRules() {
40
+ const still = breakpoints
41
+ .map((breakpoint) => ` .${dialogSheetClass(breakpoint)}[open] { animation: none; }`)
42
+ .join('\n');
43
+ return `@keyframes sw-dialog-sheet-in {
44
+ from { translate: 0 100%; }
45
+ }
46
+
47
+ @media (prefers-reduced-motion: reduce) {
48
+ ${still}
49
+ }`;
50
+ }
51
+ /*
52
+ * The title row when the dialog has a close button: the title takes the
53
+ * row and the button ends it. The button's negative margins keep the row
54
+ * as tall as one title line and put the glyph's end on the padding edge,
55
+ * at the sm control size and at the 44 px touch target alike.
56
+ */
57
+ function closeButtonRules() {
58
+ const square = (size) => `min-width: ${size};
59
+ margin-block: calc((${title.lineHeight}px - ${size}) / 2);
60
+ margin-inline-end: calc((var(--sw-space-4) - ${size}) / 2);`;
61
+ return `.sw-dialog-header {
62
+ align-items: flex-start;
63
+ display: flex;
64
+ gap: var(--sw-space-2);
65
+ }
66
+
67
+ .sw-dialog-header > :first-child {
68
+ flex: 1 1 auto;
69
+ min-width: 0;
70
+ }
71
+
72
+ .sw-button.sw-dialog-close {
73
+ flex: none;
74
+ padding-inline: 0;
75
+ ${square('var(--sw-control-sm-min-height)')}
76
+ }
77
+
78
+ @media ${coarsePointerQuery} {
79
+ .sw-button.sw-dialog-close {
80
+ min-height: ${touchTarget};
81
+ ${square(touchTarget)}
82
+ }
83
+ }
84
+
85
+ /* Private control chrome: two strokes crossed in a space-4 square, in the
86
+ button's color; borders, so forced colors keep them. */
87
+ .sw-dialog-close-glyph {
88
+ flex: none;
89
+ height: var(--sw-space-4);
90
+ position: relative;
91
+ width: var(--sw-space-4);
92
+ }
93
+
94
+ .sw-dialog-close-glyph::before,
95
+ .sw-dialog-close-glyph::after {
96
+ border-top: 2px solid currentColor;
97
+ border-radius: 1px;
98
+ box-sizing: border-box;
99
+ content: '';
100
+ left: 0;
101
+ margin-top: -1px;
102
+ position: absolute;
103
+ top: 50%;
104
+ width: 100%;
105
+ }
106
+
107
+ .sw-dialog-close-glyph::before { transform: rotate(45deg); }
108
+ .sw-dialog-close-glyph::after { transform: rotate(-45deg); }`;
109
+ }
1
110
  export function cssDialogClasses() {
2
111
  return `.sw-dialog {
3
112
  background: var(--sw-glass-fill);
@@ -9,7 +118,7 @@ export function cssDialogClasses() {
9
118
  box-sizing: border-box;
10
119
  color: var(--sw-color-text);
11
120
  margin: auto;
12
- max-height: min(100vh - var(--sw-space-8), 100dvh - var(--sw-space-8));
121
+ max-height: ${maxHeight};
13
122
  max-width: min(var(--sw-dialog-max), calc(100vw - var(--sw-space-8)));
14
123
  overflow: auto;
15
124
  width: calc(100% - var(--sw-space-8));
@@ -19,6 +128,10 @@ export function cssDialogClasses() {
19
128
  max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
20
129
  }
21
130
 
131
+ ${sheetRules()}
132
+
133
+ ${sheetMotionRules()}
134
+
22
135
  .sw-dialog::backdrop {
23
136
  background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
24
137
  }
@@ -26,8 +139,17 @@ export function cssDialogClasses() {
26
139
  .sw-dialog:focus-visible {
27
140
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
28
141
  outline-offset: var(--sw-focus-ring-offset);
29
- }`;
142
+ }
143
+
144
+ ${closeButtonRules()}`;
30
145
  }
31
146
  export function dialogClassCatalog() {
32
- return ['sw-dialog', 'sw-dialog-lg'];
147
+ return [
148
+ 'sw-dialog',
149
+ 'sw-dialog-lg',
150
+ ...breakpoints.map((breakpoint) => dialogSheetClass(breakpoint)),
151
+ 'sw-dialog-header',
152
+ 'sw-dialog-close',
153
+ 'sw-dialog-close-glyph',
154
+ ];
33
155
  }
@@ -2,7 +2,21 @@
2
2
  * A scroll region is a full-width box that scrolls its own content when the
3
3
  * content is wider (or taller) than the box, so a wide table never widens the
4
4
  * page. It is a keyboard stop, so its focus ring is drawn inside its edge
5
- * where an ancestor's clipping cannot hide it. Table and the DenominationGrid
6
- * strip share it.
5
+ * where an ancestor's clipping cannot hide it. While its content is scrolled
6
+ * out past an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
7
+ * `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
8
+ * goes on, which overlay scrollbars (phones, macOS) do not give. In forced
9
+ * colors the shade is not drawn and the system's scrollbar is the cue. Table
10
+ * and the DenominationGrid strip share it.
7
11
  */
8
12
  export declare function scrollRegionRules(selector: string): string;
13
+ export declare function scrollRegionClassCatalog(): string[];
14
+ /**
15
+ * A region whose start column is pinned (sticky, on an opaque fill) hides
16
+ * its own start shade under that column, so the column casts the shade
17
+ * instead: a gradient a spacing step wide just past its inline end, drawn by
18
+ * its `::after` (the pinned cell is positioned, so it holds the gradient).
19
+ * `pinned` lists the pinned cells' selectors inside the region. Forced colors
20
+ * draw no shade here either.
21
+ */
22
+ export declare function pinnedStartShadeRules(region: string, pinned: readonly string[]): string;
@@ -1,9 +1,40 @@
1
+ /*
2
+ * The shade an edge draws while content is scrolled out past it: an inset
3
+ * shadow a spacing step wide in the text color at low strength, so it reads
4
+ * on every palette and in dark mode. It is the box's own shadow, painted
5
+ * under the content, so no cell moves. A box scrolled to its middle shades
6
+ * both edges. The rules name the shadows outright rather than through
7
+ * inherited variables, so a scroll region inside another never takes its
8
+ * ancestor's shade.
9
+ */
10
+ const shadeColor = 'color-mix(in srgb, var(--sw-color-text) 28%, transparent)';
11
+ function edgeShade(side) {
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}`;
14
+ }
15
+ /** The start edge is the left in a left-to-right box and the right in RTL. */
16
+ function edgeShadeRules(selector) {
17
+ const more = (edge) => `${selector}.sw-scroll-more-${edge}`;
18
+ const both = `${more('start')}.sw-scroll-more-end`;
19
+ return [
20
+ `${more('start')} { box-shadow: ${edgeShade('left')}; }`,
21
+ `${more('end')} { box-shadow: ${edgeShade('right')}; }`,
22
+ `${more('start')}:dir(rtl) { box-shadow: ${edgeShade('right')}; }`,
23
+ `${more('end')}:dir(rtl) { box-shadow: ${edgeShade('left')}; }`,
24
+ // Last: it ties the :dir(rtl) rules on specificity and must win them.
25
+ `${both} { box-shadow: ${edgeShade('left')}, ${edgeShade('right')}; }`,
26
+ ].join('\n');
27
+ }
1
28
  /**
2
29
  * A scroll region is a full-width box that scrolls its own content when the
3
30
  * content is wider (or taller) than the box, so a wide table never widens the
4
31
  * page. It is a keyboard stop, so its focus ring is drawn inside its edge
5
- * where an ancestor's clipping cannot hide it. Table and the DenominationGrid
6
- * strip share it.
32
+ * where an ancestor's clipping cannot hide it. While its content is scrolled
33
+ * out past an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
34
+ * `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
35
+ * goes on, which overlay scrollbars (phones, macOS) do not give. In forced
36
+ * colors the shade is not drawn and the system's scrollbar is the cue. Table
37
+ * and the DenominationGrid strip share it.
7
38
  */
8
39
  export function scrollRegionRules(selector) {
9
40
  return `${selector} {
@@ -14,5 +45,40 @@ export function scrollRegionRules(selector) {
14
45
  ${selector}:focus-visible {
15
46
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
16
47
  outline-offset: calc(-1 * var(--sw-focus-ring-width));
48
+ }
49
+
50
+ ${edgeShadeRules(selector)}`;
51
+ }
52
+ export function scrollRegionClassCatalog() {
53
+ return ['sw-scroll-more-start', 'sw-scroll-more-end'];
54
+ }
55
+ /**
56
+ * A region whose start column is pinned (sticky, on an opaque fill) hides
57
+ * its own start shade under that column, so the column casts the shade
58
+ * instead: a gradient a spacing step wide just past its inline end, drawn by
59
+ * its `::after` (the pinned cell is positioned, so it holds the gradient).
60
+ * `pinned` lists the pinned cells' selectors inside the region. Forced colors
61
+ * draw no shade here either.
62
+ */
63
+ export function pinnedStartShadeRules(region, pinned) {
64
+ const after = (direction) => pinned
65
+ .map((cell) => `${region}.sw-scroll-more-start${direction} ${cell}::after`)
66
+ .join(',\n');
67
+ return `${after('')} {
68
+ background: linear-gradient(to right, ${shadeColor}, transparent);
69
+ content: '';
70
+ inset-block: 0;
71
+ inset-inline-start: 100%;
72
+ pointer-events: none;
73
+ position: absolute;
74
+ width: var(--sw-space-5);
75
+ }
76
+
77
+ ${after(':dir(rtl)')} {
78
+ background: linear-gradient(to left, ${shadeColor}, transparent);
79
+ }
80
+
81
+ @media (forced-colors: active) {
82
+ ${after('').replaceAll('\n', '\n ')} { display: none; }
17
83
  }`;
18
84
  }
@@ -1,3 +1,4 @@
1
+ export declare const stuckCanvasShare = 0.9;
1
2
  /** The tab strip: a scrollable row of tabs with an accent underline under the current one. */
2
3
  export declare function cssTabsClasses(): string;
3
4
  export declare function tabsClassCatalog(): string[];
@@ -1,5 +1,37 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { zIndex } from './stacking.js';
2
3
  const label = typographyVariants.label;
4
+ /*
5
+ * The stuck strip is a full-bleed band of the page canvas, not a glass card:
6
+ * nine parts canvas color to one part see-through, with the glass blur
7
+ * behind it, so a panel scrolling under it shows as a faint smear, never as
8
+ * words beside the labels. At nine parts the muted labels keep at least
9
+ * 4.1:1, and the accent label 3.6:1, in every palette even over a solid
10
+ * block of the text, accent or danger color; over the canvas and ordinary
11
+ * type, which the blur spreads, they keep their canvas contrast. The first
12
+ * tab's own inline padding (the md control's, spacing step 4) lines its
13
+ * label up with a space-4 page gutter, so the strip adds no padding of its
14
+ * own. It rides the AppHeader's layer.
15
+ */
16
+ export const stuckCanvasShare = 0.9;
17
+ function stickyRules() {
18
+ return `.sw-tabs-sticky {
19
+ background: color-mix(in srgb, var(--sw-color-background) ${stuckCanvasShare * 100}%, transparent);
20
+ backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
21
+ -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
22
+ position: sticky;
23
+ top: env(safe-area-inset-top, 0px);
24
+ ${zIndex('topChrome')}
25
+ }
26
+
27
+ @media (prefers-reduced-transparency: reduce) {
28
+ .sw-tabs-sticky {
29
+ background: var(--sw-color-background);
30
+ backdrop-filter: none;
31
+ -webkit-backdrop-filter: none;
32
+ }
33
+ }`;
34
+ }
3
35
  /** The tab strip: a scrollable row of tabs with an accent underline under the current one. */
4
36
  export function cssTabsClasses() {
5
37
  return `.sw-tabs {
@@ -55,10 +87,19 @@ export function cssTabsClasses() {
55
87
  outline-offset: var(--sw-focus-ring-offset);
56
88
  }
57
89
 
90
+ ${stickyRules()}
91
+
58
92
  @media (forced-colors: active) {
59
93
  .sw-tab-selected { border-bottom-color: Highlight; color: Highlight; }
94
+ .sw-tabs-sticky { background: Canvas; backdrop-filter: none; -webkit-backdrop-filter: none; }
60
95
  }`;
61
96
  }
62
97
  export function tabsClassCatalog() {
63
- return ['sw-tabs', 'sw-tab', 'sw-tab-selected', 'sw-tab-panel'];
98
+ return [
99
+ 'sw-tabs',
100
+ 'sw-tabs-sticky',
101
+ 'sw-tab',
102
+ 'sw-tab-selected',
103
+ 'sw-tab-panel',
104
+ ];
64
105
  }
@@ -10,8 +10,9 @@
10
10
  * its popup can only paint as high as the surface does; lifting the surface
11
11
  * over the ActionBar keeps an open list tappable where the bar would cover
12
12
  * it, and over the next surface in the flow.
13
- * 4. `topChrome`: the sticky AppHeader, over a lifted surface that scrolls up
14
- * under it (the header is the page's way back, and a popup opens downward).
13
+ * 4. `topChrome`: the sticky AppHeader or sticky Tabs strip, over a lifted
14
+ * surface that scrolls up under it (the header is the page's way back, the
15
+ * strip its way between sections, and a popup opens downward).
15
16
  * 5. `popup`: a popup outside any lifted surface (Select list, ActionMenu
16
17
  * list, Tooltip, the DateField and CalendarButton calendar) over
17
18
  * everything in the page.
@@ -18,6 +18,7 @@ import { filterChipsClassCatalog } from './css-filter-chips.js';
18
18
  import { gridClassCatalog } from './css-grid.js';
19
19
  import { gridSpanClassCatalog } from './css-grid-span.js';
20
20
  import { denominationGridClassCatalog } from './css-denomination-grid.js';
21
+ import { scrollRegionClassCatalog } from './css-scroll-region.js';
21
22
  import { fieldClassCatalog } from './css-field.js';
22
23
  import { boxBorderClassCatalog } from './css-box-border.js';
23
24
  import { cssComponentClasses } from './css-components.js';
@@ -53,6 +54,7 @@ export function utilityClassCatalog() {
53
54
  ...dataClassCatalog(),
54
55
  ...segmentedClassCatalog(),
55
56
  ...tableClassCatalog(),
57
+ ...scrollRegionClassCatalog(),
56
58
  ...chartClassCatalog(),
57
59
  ...chromeClassCatalog(),
58
60
  ...dialogClassCatalog(),
@@ -6,7 +6,9 @@ export type DenominationCellView = {
6
6
  /**
7
7
  * Classifies one count for display. A null or zero count renders the zero
8
8
  * label; a signed row prefixes positive counts with "+" so movement reads as
9
- * a delta. Formatting stays plain digits: the consumer owns locale grouping.
9
+ * a delta, and every negative count takes the typographic minus "−", the
10
+ * sign a consumer's formatted money uses, never a hyphen. Formatting stays
11
+ * plain digits: the consumer owns locale grouping.
10
12
  */
11
13
  export declare function denominationCellView(count: number | null, signed: boolean, zeroLabel: string): DenominationCellView;
12
14
  export declare function isDenominationCount(count: number | null): boolean;
@@ -1,13 +1,17 @@
1
+ /** The typographic minus (U+2212), not the hyphen-minus a number prints. */
2
+ const minusSign = '\u2212';
1
3
  /**
2
4
  * Classifies one count for display. A null or zero count renders the zero
3
5
  * label; a signed row prefixes positive counts with "+" so movement reads as
4
- * a delta. Formatting stays plain digits: the consumer owns locale grouping.
6
+ * a delta, and every negative count takes the typographic minus "−", the
7
+ * sign a consumer's formatted money uses, never a hyphen. Formatting stays
8
+ * plain digits: the consumer owns locale grouping.
5
9
  */
6
10
  export function denominationCellView(count, signed, zeroLabel) {
7
11
  if (count === null || count === 0)
8
12
  return { state: 'zero', text: zeroLabel };
9
13
  if (count < 0)
10
- return { state: 'negative', text: String(count) };
14
+ return { state: 'negative', text: `${minusSign}${Math.abs(count)}` };
11
15
  return { state: 'positive', text: signed ? `+${count}` : String(count) };
12
16
  }
13
17
  export function isDenominationCount(count) {
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, } from '
17
17
  export { Box, type BoxBorder, type BoxElement, type BoxProps, } from './components/Box.js';
18
18
  export { ButtonGroup, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
19
19
  export { Card, type CardProps, type CardVariant } from './components/Card.js';
20
- export { Dialog, type DialogProps, type DialogSize, } from './components/Dialog.js';
20
+ export { Dialog, type DialogProps, type DialogSize, type DialogTitleLevel, } from './components/Dialog.js';
21
21
  export { StatTile, type StatTileEmphasis, type StatTileProps, type StatTileTone, statTileTones, } from './components/StatTile.js';
22
22
  export { TabPanel, type TabItem, type TabPanelProps, Tabs, type TabsProps, tabId, tabPanelId, } from './components/Tabs.js';
23
23
  export { Toast, toastTones } from './components/Toast.js';
@@ -44,6 +44,6 @@ export type { GridColumns, GridProps } from './components/Grid.js';
44
44
  export type { GridColumnSpan } from './css/css-grid-span.js';
45
45
  export type { ColumnSpanProps } from './column-span-classes.js';
46
46
  export { DenominationGrid } from './components/DenominationGrid.js';
47
- export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridTone, } from './components/DenominationGrid.js';
47
+ export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowRole, DenominationGridTone, } from './components/DenominationGrid.js';
48
48
  export { FilterChips } from './components/FilterChips.js';
49
49
  export type { FilterChipOption, FilterChipsProps, } from './components/FilterChips.js';
@@ -0,0 +1,14 @@
1
+ /** Which inline edges of a scroll box have content scrolled out past them. */
2
+ export type ScrollOverflow = {
3
+ start: boolean;
4
+ end: boolean;
5
+ };
6
+ export declare const noScrollOverflow: ScrollOverflow;
7
+ /**
8
+ * Reads a box's horizontal overflow from its scroll metrics. `scrollLeft` is
9
+ * 0 at the inline start and grows toward the end in a left-to-right box; in
10
+ * a right-to-left box it is 0 at the start and negative toward the end, so
11
+ * the distance travelled is its magnitude in both.
12
+ */
13
+ export declare function scrollOverflow(scrollLeft: number, scrollWidth: number, clientWidth: number): ScrollOverflow;
14
+ export declare function sameScrollOverflow(a: ScrollOverflow, b: ScrollOverflow): boolean;
@@ -0,0 +1,26 @@
1
+ export const noScrollOverflow = { start: false, end: false };
2
+ /**
3
+ * Sub-pixel layout can leave a box a fraction of a pixel wider than its
4
+ * content, or a scroll position a fraction short of its end; within this
5
+ * many pixels an edge counts as reached.
6
+ */
7
+ const edgeTolerance = 1;
8
+ /**
9
+ * Reads a box's horizontal overflow from its scroll metrics. `scrollLeft` is
10
+ * 0 at the inline start and grows toward the end in a left-to-right box; in
11
+ * a right-to-left box it is 0 at the start and negative toward the end, so
12
+ * the distance travelled is its magnitude in both.
13
+ */
14
+ export function scrollOverflow(scrollLeft, scrollWidth, clientWidth) {
15
+ const hidden = scrollWidth - clientWidth;
16
+ if (hidden <= edgeTolerance)
17
+ return noScrollOverflow;
18
+ const travelled = Math.abs(scrollLeft);
19
+ return {
20
+ start: travelled > edgeTolerance,
21
+ end: travelled < hidden - edgeTolerance,
22
+ };
23
+ }
24
+ export function sameScrollOverflow(a, b) {
25
+ return a.start === b.start && a.end === b.end;
26
+ }