@scalewing/react 1.11.0 → 1.13.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 (66) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +62 -7
  3. package/dist/anchored-position.d.ts +14 -6
  4. package/dist/anchored-position.js +10 -9
  5. package/dist/components/ActionMenu.d.ts +9 -1
  6. package/dist/components/ActionMenu.js +2 -2
  7. package/dist/components/Checkbox.js +2 -1
  8. package/dist/components/DateField.js +4 -2
  9. package/dist/components/DenominationGrid.d.ts +1 -1
  10. package/dist/components/DenominationGrid.js +4 -2
  11. package/dist/components/Field.js +9 -6
  12. package/dist/components/FieldErrorRegion.d.ts +14 -0
  13. package/dist/components/FieldErrorRegion.js +13 -0
  14. package/dist/components/FieldLabelText.d.ts +13 -0
  15. package/dist/components/FieldLabelText.js +12 -0
  16. package/dist/components/RadioGroup.d.ts +7 -0
  17. package/dist/components/RadioGroup.js +2 -1
  18. package/dist/components/SegmentedControl.d.ts +8 -0
  19. package/dist/components/SegmentedControl.js +16 -11
  20. package/dist/components/Select.d.ts +10 -1
  21. package/dist/components/Select.js +17 -86
  22. package/dist/components/Toast.d.ts +15 -1
  23. package/dist/components/Toast.js +15 -5
  24. package/dist/components/action-menu/ActionMenuList.d.ts +6 -4
  25. package/dist/components/action-menu/ActionMenuList.js +5 -4
  26. package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
  27. package/dist/components/denomination-grid/DenominationStrip.js +13 -3
  28. package/dist/components/denomination-grid/DenominationTiles.js +27 -8
  29. package/dist/components/denomination-grid/types.d.ts +5 -0
  30. package/dist/components/select/SelectListbox.d.ts +20 -0
  31. package/dist/components/select/SelectListbox.js +16 -0
  32. package/dist/components/select/SelectValue.d.ts +15 -0
  33. package/dist/components/select/SelectValue.js +16 -0
  34. package/dist/components/select/use-select-list.d.ts +26 -0
  35. package/dist/components/select/use-select-list.js +85 -0
  36. package/dist/components/use-anchored-popover.d.ts +5 -2
  37. package/dist/components/use-anchored-popover.js +6 -5
  38. package/dist/css/chevron.d.ts +5 -0
  39. package/dist/css/chevron.js +9 -0
  40. package/dist/css/css-accordion.js +2 -5
  41. package/dist/css/css-button.d.ts +2 -0
  42. package/dist/css/css-button.js +28 -4
  43. package/dist/css/css-checkbox.js +1 -0
  44. package/dist/css/css-components.js +6 -0
  45. package/dist/css/css-data.js +17 -163
  46. package/dist/css/css-date-field.js +14 -6
  47. package/dist/css/css-denomination-grid.js +11 -1
  48. package/dist/css/css-document.d.ts +2 -0
  49. package/dist/css/css-document.js +35 -0
  50. package/dist/css/css-field.js +15 -2
  51. package/dist/css/css-filter-chips.js +8 -2
  52. package/dist/css/css-radio-group.js +12 -2
  53. package/dist/css/css-segmented.d.ts +2 -0
  54. package/dist/css/css-segmented.js +127 -0
  55. package/dist/css/css-select.js +57 -19
  56. package/dist/css/css-table.d.ts +2 -0
  57. package/dist/css/css-table.js +99 -0
  58. package/dist/css/css-toast.d.ts +2 -0
  59. package/dist/css/css-toast.js +42 -1
  60. package/dist/css/css-utilities.d.ts +2 -0
  61. package/dist/css/css-utilities.js +3 -1
  62. package/dist/css/stylesheet.js +4 -0
  63. package/dist/index.d.ts +3 -3
  64. package/dist/index.js +1 -1
  65. package/dist/styles.css +216 -48
  66. package/package.json +1 -1
@@ -13,11 +13,12 @@ function layoutViewport() {
13
13
  height: root.clientHeight || window.innerHeight,
14
14
  };
15
15
  }
16
- function placePopover(node, anchor, { flipInline = false }) {
16
+ function placePopover(node, anchor, { align = 'start', flipInline = false }) {
17
17
  const position = anchoredPosition(anchor.getBoundingClientRect(), { width: node.offsetWidth, height: node.offsetHeight }, layoutViewport(), {
18
18
  gap: spacingScale[1],
19
19
  inset: spacingScale[2],
20
20
  rtl: getComputedStyle(anchor).direction === 'rtl',
21
+ align,
21
22
  flipInline,
22
23
  });
23
24
  node.style.left = `${position.left}px`;
@@ -31,7 +32,7 @@ function placePopover(node, anchor, { flipInline = false }) {
31
32
  * outside it and outside `ignoreRef` (the button that toggles it).
32
33
  * Browsers without the popover API get the same fixed placement.
33
34
  */
34
- export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss, { flipInline = false } = {}) {
35
+ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss, { align = 'start', flipInline = false } = {}) {
35
36
  const dismissRef = useRef(onDismiss);
36
37
  useLayoutEffect(() => {
37
38
  dismissRef.current = onDismiss;
@@ -49,7 +50,7 @@ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss,
49
50
  if (!node.matches(':popover-open'))
50
51
  node.showPopover();
51
52
  }
52
- placePopover(node, anchor, { flipInline });
53
+ placePopover(node, anchor, { align, flipInline });
53
54
  function onPointerDown(event) {
54
55
  const target = event.target;
55
56
  if (!(target instanceof Node))
@@ -60,7 +61,7 @@ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss,
60
61
  }
61
62
  function onMove() {
62
63
  if (node && anchor)
63
- placePopover(node, anchor, { flipInline });
64
+ placePopover(node, anchor, { align, flipInline });
64
65
  }
65
66
  // A popover or anchor that changes size while open (new commands, a
66
67
  // month with another row of weeks, a relabelled trigger) moves too.
@@ -79,5 +80,5 @@ export function useAnchoredPopover(popoverRef, anchorRef, ignoreRef, onDismiss,
79
80
  if (supportsPopover && node.matches(':popover-open'))
80
81
  node.hidePopover();
81
82
  };
82
- }, [anchorRef, flipInline, ignoreRef, popoverRef]);
83
+ }, [align, anchorRef, flipInline, ignoreRef, popoverRef]);
83
84
  }
@@ -0,0 +1,5 @@
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.
4
+ */
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);";
@@ -0,0 +1,9 @@
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.
4
+ */
5
+ export const chevronStroke = `border-bottom: 2px solid var(--sw-color-muted);
6
+ border-right: 2px solid var(--sw-color-muted);
7
+ box-sizing: border-box;
8
+ height: var(--sw-space-2);
9
+ width: var(--sw-space-2);`;
@@ -1,17 +1,14 @@
1
+ import { chevronStroke } from './chevron.js';
1
2
  function markerRules() {
2
3
  return `/* A token chevron replaces the user-agent triangle: it points to the inline end when closed and down when open. */
3
4
  .sw-accordion-summary::-webkit-details-marker { display: none; }
4
5
 
5
6
  .sw-accordion-marker {
6
- border-bottom: 2px solid var(--sw-color-muted);
7
- border-right: 2px solid var(--sw-color-muted);
8
- box-sizing: border-box;
7
+ ${chevronStroke}
9
8
  flex: none;
10
- height: var(--sw-space-2);
11
9
  margin-inline-end: var(--sw-space-1);
12
10
  transform: rotate(-45deg);
13
11
  transition: transform var(--sw-motion-default) var(--sw-motion-easing);
14
- width: var(--sw-space-2);
15
12
  }
16
13
 
17
14
  .sw-accordion-marker:dir(rtl) { transform: rotate(135deg); }
@@ -3,6 +3,8 @@ export declare function buttonClassNames(options: {
3
3
  size: ButtonSize;
4
4
  variant: ButtonVariant;
5
5
  }): string[];
6
+ export declare const pressedRingGap = 2;
7
+ export declare const pressedRingWidth = 4;
6
8
  export declare function cssButtonClasses(): string;
7
9
  export declare const buttonGroupJustifies: readonly ["start", "end", "between"];
8
10
  export declare function buttonClassCatalog(): string[];
@@ -17,6 +17,33 @@ function variantRules() {
17
17
  `.sw-button-danger { background: var(--sw-color-danger); border-color: transparent; color: var(--sw-color-onDanger); }`,
18
18
  ].join('\n');
19
19
  }
20
+ /*
21
+ * A toggle button's pressed state is an accent ring drawn outside the fill,
22
+ * past a 2 px gap in the page background: the ring meets the background on
23
+ * both sides, so it keeps the accent's contrast on the canvas (4.5:1 or more
24
+ * in every palette, above WCAG 1.4.11's 3:1) whatever the variant's fill.
25
+ * A focused pressed button moves its focus outline out past the ring, so the
26
+ * two stay apart. The unpressed button keeps its full label contrast: no
27
+ * opacity.
28
+ *
29
+ * Forced colors drop box shadows, so there the same ring is a Highlight
30
+ * border on an out-of-flow ::after, and the button's own border turns
31
+ * Highlight. The button keeps the forced ButtonFace and ButtonText of every
32
+ * other button: a Highlight fill cannot be used, because Chromium paints
33
+ * the forced backplate behind a button's text in Canvas, which erased a
34
+ * HighlightText label (1.00:1, Teisoro's pressed toggles in 1.12.0), and
35
+ * forced-color-adjust: none would let every descendant keep author colors.
36
+ */
37
+ export const pressedRingGap = 2;
38
+ export const pressedRingWidth = 4;
39
+ function pressedRules() {
40
+ return `.sw-button[aria-pressed='true'] { box-shadow: 0 0 0 ${pressedRingGap}px var(--sw-color-background), 0 0 0 ${pressedRingWidth}px var(--sw-color-accent); }
41
+ .sw-button[aria-pressed='true']:focus-visible { outline-offset: calc(var(--sw-focus-ring-offset) + ${pressedRingWidth}px); }
42
+ @media (forced-colors: active) {
43
+ .sw-button[aria-pressed='true'] { border-color: Highlight; position: relative; }
44
+ .sw-button[aria-pressed='true']::after { border: ${pressedRingWidth - pressedRingGap}px solid Highlight; border-radius: inherit; content: ''; inset: calc(-1px - ${pressedRingWidth}px); pointer-events: none; position: absolute; }
45
+ }`;
46
+ }
20
47
  function sizeRules() {
21
48
  return buttonSizes
22
49
  .map((size) => {
@@ -54,11 +81,8 @@ export function cssButtonClasses() {
54
81
  opacity: var(--sw-disabled-opacity);
55
82
  }
56
83
 
57
- .sw-button[aria-pressed='false'] {
58
- opacity: var(--sw-quiet-opacity);
59
- }
60
-
61
84
  ${variantRules()}
85
+ ${pressedRules()}
62
86
  ${sizeRules()}
63
87
  ${groupRules()}`;
64
88
  }
@@ -100,6 +100,7 @@ export function cssCheckboxClasses() {
100
100
 
101
101
  .sw-checkbox-description { color: var(--sw-color-muted); }
102
102
  .sw-checkbox-error { color: var(--sw-color-danger); }
103
+ .sw-checkbox-error:empty { position: absolute; }
103
104
 
104
105
  @media (prefers-reduced-transparency: reduce) {
105
106
  .sw-checkbox-mark { background: var(--sw-color-surface); }
@@ -8,6 +8,8 @@ import { cssCheckboxClasses } from './css-checkbox.js';
8
8
  import { cssRadioGroupClasses } from './css-radio-group.js';
9
9
  import { cssChromeClasses } from './css-chrome.js';
10
10
  import { cssDataClasses } from './css-data.js';
11
+ import { cssSegmentedClasses } from './css-segmented.js';
12
+ import { cssTableClasses } from './css-table.js';
11
13
  import { cssDateFieldClasses } from './css-date-field.js';
12
14
  import { cssDialogClasses } from './css-dialog.js';
13
15
  import { cssSelectClasses } from './css-select.js';
@@ -109,6 +111,10 @@ ${cssRadioGroupClasses()}
109
111
 
110
112
  ${cssDataClasses()}
111
113
 
114
+ ${cssSegmentedClasses()}
115
+
116
+ ${cssTableClasses()}
117
+
112
118
  ${cssDateFieldClasses()}
113
119
 
114
120
  ${cssCalendarButtonClasses()}
@@ -1,6 +1,4 @@
1
- import { trackInset, typographyVariants } from '@scalewing/tokens';
2
- import { scrollRegionRules } from './css-scroll-region.js';
3
- import { zIndex } from './stacking.js';
1
+ import { buttonVariants, typographyVariants } from '@scalewing/tokens';
4
2
  export const badgeTones = [
5
3
  'neutral',
6
4
  'accent',
@@ -10,11 +8,24 @@ export const badgeTones = [
10
8
  ];
11
9
  export const badgeSizes = ['sm', 'md'];
12
10
  const caption = typographyVariants.caption;
13
- const data = typographyVariants.data;
14
- const body = typographyVariants.body;
15
11
  export function badgeClassNames(tone, size = 'md') {
16
12
  return ['sw-badge', `sw-badge-${tone}`, `sw-badge-${size}`];
17
13
  }
14
+ /*
15
+ * A badge inside a filled button would otherwise draw its tone on the
16
+ * button's fill (a warning badge on the accent is about 1.1:1), so it takes
17
+ * the surface and sets its words in the text color, which is 4.5:1 on the
18
+ * surface in every palette; its tone stays on its border (3:1 or more), as
19
+ * some palettes' tones are under 4.5:1 as text on the surface. A ghost
20
+ * button has no fill.
21
+ */
22
+ function badgeOnFilledButtonRules() {
23
+ const filled = buttonVariants
24
+ .filter((variant) => variant !== 'ghost')
25
+ .map((variant) => `.sw-button-${variant}`)
26
+ .join(', ');
27
+ return `:is(${filled}) .sw-badge { background: var(--sw-color-surface); color: var(--sw-color-text); }`;
28
+ }
18
29
  export function cssDataClasses() {
19
30
  return `.sw-badge {
20
31
  align-items: center;
@@ -68,169 +79,12 @@ export function cssDataClasses() {
68
79
  color: var(--sw-color-warning);
69
80
  }
70
81
 
71
- .sw-segmented {
72
- background: var(--sw-glass-fill);
73
- border: 1px solid var(--sw-glass-border);
74
- border-radius: var(--sw-radius-pill);
75
- box-sizing: border-box;
76
- display: inline-flex;
77
- gap: ${trackInset}px;
78
- padding: ${trackInset}px;
79
- }
80
-
81
- .sw-segmented-item {
82
- appearance: none;
83
- background: transparent;
84
- border: 0;
85
- border-radius: var(--sw-radius-pill);
86
- color: var(--sw-color-muted);
87
- cursor: pointer;
88
- font-family: var(--sw-font-sans);
89
- font-size: ${data.fontSize}px;
90
- font-weight: 600;
91
- letter-spacing: ${data.letterSpacing}px;
92
- line-height: ${data.lineHeight}px;
93
- min-height: var(--sw-control-xs-min-height);
94
- padding-inline: var(--sw-control-xs-padding-inline);
95
- }
96
-
97
- .sw-segmented-item:focus-visible {
98
- outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
99
- outline-offset: var(--sw-focus-ring-offset);
100
- }
101
-
102
- .sw-segmented-item-selected {
103
- background: var(--sw-color-surface);
104
- color: var(--sw-color-text);
105
- }
106
-
107
- .sw-segmented-filled {
108
- display: grid;
109
- grid-auto-columns: minmax(0, 1fr);
110
- grid-auto-flow: column;
111
- }
112
-
113
- .sw-segmented-filled .sw-segmented-item {
114
- align-items: center;
115
- color: var(--sw-color-text);
116
- display: inline-flex;
117
- font-size: ${body.fontSize}px;
118
- justify-content: center;
119
- letter-spacing: ${body.letterSpacing}px;
120
- line-height: ${body.lineHeight}px;
121
- min-height: var(--sw-control-md-min-height);
122
- min-width: 0;
123
- padding-inline: var(--sw-control-md-padding-inline);
124
- }
125
-
126
- .sw-segmented-filled .sw-segmented-item-selected {
127
- background: var(--sw-color-accent);
128
- color: var(--sw-color-onAccent);
129
- }
130
-
131
- .sw-segmented-disabled {
132
- opacity: var(--sw-disabled-opacity);
133
- }
134
-
135
- .sw-segmented-item:disabled {
136
- cursor: not-allowed;
137
- }
138
-
139
- ${scrollRegionRules('.sw-table-wrap')}
140
-
141
- .sw-table {
142
- border-collapse: collapse;
143
- width: 100%;
144
- }
145
-
146
- .sw-table th,
147
- .sw-table td {
148
- border-bottom: 1px solid var(--sw-color-border);
149
- padding: var(--sw-space-2) var(--sw-space-3);
150
- text-align: start;
151
- vertical-align: middle;
152
- }
153
-
154
- .sw-table th {
155
- color: var(--sw-color-muted);
156
- font-family: var(--sw-font-sans);
157
- font-size: ${caption.fontSize}px;
158
- font-weight: 600;
159
- letter-spacing: ${caption.letterSpacing}px;
160
- line-height: ${caption.lineHeight}px;
161
- }
162
-
163
- /* Descendant form outranks .sw-table th / .sw-table td, which set text-align: start. */
164
- .sw-table .sw-table-end {
165
- text-align: end;
166
- }
167
-
168
- .sw-table .sw-table-numeric {
169
- font-variant-numeric: tabular-nums;
170
- text-align: end;
171
- }
172
-
173
- .sw-table-clip {
174
- max-width: 0;
175
- overflow: hidden;
176
- }
177
-
178
- .sw-table-sticky thead th {
179
- background: var(--sw-glass-fill);
180
- backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
181
- -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
182
- position: sticky;
183
- top: 0;
184
- ${zIndex('stickyCell')}
185
- }
186
-
187
- .sw-table-compact th,
188
- .sw-table-compact td {
189
- padding: var(--sw-space-1) var(--sw-space-2);
190
- }
191
-
192
- .sw-table-row-selected > :first-child {
193
- padding-inline-start: var(--sw-space-5);
194
- position: relative;
195
- }
196
-
197
- .sw-table-compact .sw-table-row-selected > :first-child {
198
- padding-inline-start: var(--sw-space-4);
199
- }
200
-
201
- .sw-table-row-selected > :first-child::before {
202
- background: var(--sw-color-accent);
203
- border-radius: var(--sw-radius-pill);
204
- content: '';
205
- height: var(--sw-space-2);
206
- inset-block-start: 50%;
207
- inset-inline-start: var(--sw-space-2);
208
- position: absolute;
209
- transform: translateY(-50%);
210
- width: var(--sw-space-2);
211
- }
212
-
213
- .sw-table-compact .sw-table-row-selected > :first-child::before {
214
- inset-inline-start: var(--sw-space-1);
215
- }`;
82
+ ${badgeOnFilledButtonRules()}`;
216
83
  }
217
84
  export function dataClassCatalog() {
218
85
  return [
219
86
  'sw-badge',
220
87
  ...badgeTones.map((tone) => `sw-badge-${tone}`),
221
88
  ...badgeSizes.map((size) => `sw-badge-${size}`),
222
- 'sw-segmented',
223
- 'sw-segmented-item',
224
- 'sw-segmented-item-selected',
225
- 'sw-segmented-filled',
226
- 'sw-segmented-disabled',
227
- 'sw-table-wrap',
228
- 'sw-table',
229
- 'sw-table-end',
230
- 'sw-table-numeric',
231
- 'sw-table-clip',
232
- 'sw-table-sticky',
233
- 'sw-table-compact',
234
- 'sw-table-row-selected',
235
89
  ];
236
90
  }
@@ -1,4 +1,5 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
2
3
  import { zIndex } from './stacking.js';
3
4
  const body = typographyVariants.body;
4
5
  const label = typographyVariants.label;
@@ -19,12 +20,11 @@ function fieldRules() {
19
20
  width: max-content;
20
21
  }
21
22
 
23
+ /* The label inherits the canvas's body type, as Field's does; the label
24
+ words inside it are a Text label span. The same line box gives the same
25
+ gap to the entry as a Field beside it. */
22
26
  .sw-date-field-label {
23
27
  color: var(--sw-color-text);
24
- font-family: var(--sw-font-sans);
25
- font-size: ${label.fontSize}px;
26
- font-weight: ${label.fontWeight};
27
- line-height: ${label.lineHeight}px;
28
28
  }
29
29
 
30
30
  .sw-date-field-control {
@@ -71,7 +71,11 @@ ${input}[aria-invalid='true'] {
71
71
  border-color: var(--sw-color-danger);
72
72
  }
73
73
 
74
- ${input}:disabled,
74
+ /* The entry looks locked as every text control does; its button fades as buttons do. */
75
+ ${input}:disabled {
76
+ ${disabledControlSurface}
77
+ }
78
+
75
79
  .sw-date-field-button:disabled {
76
80
  cursor: not-allowed;
77
81
  opacity: var(--sw-disabled-opacity);
@@ -123,7 +127,8 @@ ${input}:disabled,
123
127
  }
124
128
 
125
129
  .sw-date-field-description { color: var(--sw-color-muted); }
126
- .sw-date-field-error { color: var(--sw-color-danger); }`;
130
+ .sw-date-field-error { color: var(--sw-color-danger); }
131
+ .sw-date-field-error:empty { position: absolute; }`;
127
132
  }
128
133
  function calendarRules() {
129
134
  return `.sw-date-field-calendar {
@@ -269,6 +274,9 @@ function adaptiveRules() {
269
274
 
270
275
  @media (forced-colors: active) {
271
276
  ${input}[aria-invalid='true'] { border-color: CanvasText; }
277
+ ${input}:disabled {
278
+ ${disabledControlForcedColors}
279
+ }
272
280
  .sw-date-field-calendar { border-color: CanvasText; }
273
281
  .sw-date-field-day[aria-current='date'] { border-color: CanvasText; }
274
282
  .sw-date-field-day[aria-selected='true'] {
@@ -2,6 +2,7 @@ import { typographyVariants } from '@scalewing/tokens';
2
2
  import { breakpointQuery } from './breakpoints.js';
3
3
  import { badgeTones } from './css-data.js';
4
4
  import { scrollRegionRules } from './css-scroll-region.js';
5
+ import { visuallyHiddenDeclarations } from './css-utilities.js';
5
6
  import { zIndex } from './stacking.js';
6
7
  const caption = typographyVariants.caption;
7
8
  const data = typographyVariants.data;
@@ -209,8 +210,15 @@ ${scrollRegionRules('.sw-denomination-scroll')}
209
210
  text-align: center;
210
211
  }
211
212
 
213
+ /*
214
+ * Below md the aria-hidden copy under each row label is what shows; the
215
+ * total cell stays in the table, visually hidden, so a screen reader still
216
+ * reads each total in its row and under its column header.
217
+ */
212
218
  @media ${breakpointQuery('below', 'md')} {
213
- .sw-denomination-strip .sw-denomination-total { display: none; }
219
+ .sw-denomination-strip .sw-denomination-total,
220
+ .sw-denomination-strip .sw-denomination-total-head { padding: 0; position: relative; }
221
+ .sw-denomination-total-value { ${visuallyHiddenDeclarations} }
214
222
  .sw-denomination-total-inline { display: block; }
215
223
  .sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
216
224
  .sw-denomination-strip th,
@@ -249,6 +257,8 @@ export function denominationGridClassCatalog() {
249
257
  'sw-denomination-cell-negative',
250
258
  'sw-denomination-total',
251
259
  'sw-denomination-total-inline',
260
+ 'sw-denomination-total-head',
261
+ 'sw-denomination-total-value',
252
262
  'sw-denomination-tiles',
253
263
  'sw-denomination-tile-list',
254
264
  'sw-denomination-tile',
@@ -1,3 +1,5 @@
1
1
  /** The frame every native text control draws: fill, hairline border, radius. */
2
2
  export declare const controlSurface = "background-color: var(--sw-glass-fill);\n border: 1px solid var(--sw-color-border);\n border-radius: var(--sw-radius-sm);\n color: inherit;\n font-family: inherit;\n font-size: inherit;\n padding-inline: var(--sw-control-md-padding-inline);";
3
+ export declare const disabledControlSurface = "background-color: var(--sw-color-subtle);\n border-style: dashed;\n cursor: not-allowed;\n opacity: 1;\n -webkit-text-fill-color: currentColor;";
4
+ export declare const disabledControlForcedColors = "border-color: GrayText;\n color: GrayText;\n -webkit-text-fill-color: GrayText;";
3
5
  export declare function cssDocumentCanvas(): string;
@@ -18,6 +18,22 @@ export const controlSurface = `background-color: var(--sw-glass-fill);
18
18
  font-family: inherit;
19
19
  font-size: inherit;
20
20
  padding-inline: var(--sw-control-md-padding-inline);`;
21
+ /*
22
+ * A disabled text control keeps its value in the text color, so a locked
23
+ * value stays readable (4.5:1 or more on the subtle fill in every palette),
24
+ * and says it is locked with the quiet subtle fill and a dashed hairline
25
+ * instead of the opacity buttons use. WebKit fades a disabled entry's text
26
+ * with its own fill color and opacity, so both are set back.
27
+ */
28
+ export const disabledControlSurface = `background-color: var(--sw-color-subtle);
29
+ border-style: dashed;
30
+ cursor: not-allowed;
31
+ opacity: 1;
32
+ -webkit-text-fill-color: currentColor;`;
33
+ /* Forced colors keep the dashed border; the system's disabled color draws it. */
34
+ export const disabledControlForcedColors = `border-color: GrayText;
35
+ color: GrayText;
36
+ -webkit-text-fill-color: GrayText;`;
21
37
  export function cssDocumentCanvas() {
22
38
  return `html,
23
39
  body {
@@ -99,5 +115,24 @@ body {
99
115
  [data-theme] select:focus-visible {
100
116
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
101
117
  outline-offset: var(--sw-focus-ring-offset);
118
+ }
119
+
120
+ /* The browser's placeholder gray is one fixed #757575 (2.3:1 on a dark
121
+ field); muted is tuned per palette and scheme. */
122
+ [data-theme] :is(${typedInputs})::placeholder {
123
+ color: var(--sw-color-muted);
124
+ opacity: 1;
125
+ }
126
+
127
+ [data-theme] :is(${typedInputs}):disabled,
128
+ [data-theme] select:disabled {
129
+ ${disabledControlSurface}
130
+ }
131
+
132
+ @media (forced-colors: active) {
133
+ [data-theme] :is(${typedInputs}):disabled,
134
+ [data-theme] select:disabled {
135
+ ${disabledControlForcedColors}
136
+ }
102
137
  }`;
103
138
  }
@@ -1,5 +1,5 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
- import { controlSurface } from './css-document.js';
2
+ import { controlSurface, disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
3
  const caption = typographyVariants.caption;
4
4
  /*
5
5
  * A prefix or suffix sits inside the control frame: the wrapper draws the
@@ -51,7 +51,14 @@ function adornmentRules() {
51
51
  padding-inline: var(--sw-control-xs-padding-inline);
52
52
  }
53
53
 
54
- .sw-field-invalid .sw-field-adorned { border-color: var(--sw-color-danger); }`;
54
+ .sw-field-invalid .sw-field-adorned { border-color: var(--sw-color-danger); }
55
+
56
+ /* The frame draws the surface, so it carries the disabled look for its input. */
57
+ .sw-field-adorned:has(> input:disabled) {
58
+ ${disabledControlSurface}
59
+ }
60
+
61
+ [data-theme] .sw-field-adorned > input:disabled { background: transparent; }`;
55
62
  }
56
63
  export function cssFieldClasses() {
57
64
  return `.sw-field-required { color: var(--sw-color-danger); }
@@ -66,6 +73,9 @@ export function cssFieldClasses() {
66
73
  .sw-field-description { color: var(--sw-color-muted); }
67
74
  .sw-field-error { color: var(--sw-color-danger); }
68
75
 
76
+ /* An error region is always rendered as a live region; empty, it leaves the layout (and its gap) but stays in the accessibility tree. */
77
+ .sw-field-error:empty { position: absolute; }
78
+
69
79
  [data-theme] .sw-field-invalid :is(input, select, textarea) {
70
80
  border-color: var(--sw-color-danger);
71
81
  }
@@ -76,6 +86,9 @@ ${adornmentRules()}
76
86
  [data-theme] .sw-field-invalid :is(input, select, textarea),
77
87
  .sw-field-invalid .sw-field-adorned { border-color: Mark; }
78
88
  .sw-field-prefix, .sw-field-suffix { color: CanvasText; }
89
+ .sw-field-adorned:has(> input:disabled) {
90
+ ${disabledControlForcedColors}
91
+ }
79
92
  .sw-field-error, .sw-field-required { color: Mark; }
80
93
  }`;
81
94
  }
@@ -70,8 +70,15 @@ export function cssFilterChipsClasses() {
70
70
  line-height: ${data.lineHeight}px;
71
71
  }
72
72
 
73
+ /*
74
+ * A zero-count chip is quiet without fading: it drops the glass fill and
75
+ * sets its label in the muted color, which is 4.5:1 on the canvas, surface
76
+ * and subtle fill in every palette. It can still be chosen, so it never
77
+ * reads as disabled.
78
+ */
73
79
  .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face {
74
- opacity: var(--sw-quiet-opacity);
80
+ background: transparent;
81
+ color: var(--sw-color-muted);
75
82
  }
76
83
 
77
84
  .sw-filter-chip-input:checked + .sw-filter-chip-face {
@@ -99,7 +106,6 @@ export function cssFilterChipsClasses() {
99
106
  .sw-filter-chip-input:checked + .sw-filter-chip-face { background: Highlight; border-color: Highlight; color: HighlightText; }
100
107
  .sw-filter-chip-input:focus-visible + .sw-filter-chip-face { outline-color: Highlight; }
101
108
  .sw-filter-chip-count { color: inherit; }
102
- .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face { opacity: 1; color: GrayText; border-color: GrayText; }
103
109
  }`;
104
110
  }
105
111
  export function filterChipsClassCatalog() {
@@ -92,7 +92,7 @@ export function cssRadioGroupClasses() {
92
92
  outline-offset: var(--sw-focus-ring-offset);
93
93
  }
94
94
 
95
- .sw-radio-group:has(.sw-radio-group-error) .sw-radio-group-mark {
95
+ .sw-radio-group[aria-invalid='true'] .sw-radio-group-mark {
96
96
  border-color: var(--sw-color-danger);
97
97
  }
98
98
 
@@ -101,6 +101,14 @@ export function cssRadioGroupClasses() {
101
101
  opacity: var(--sw-disabled-opacity);
102
102
  }
103
103
 
104
+ /* A consumer glyph beside the label, in the label's color and the option's gap. */
105
+ .sw-radio-group-icon {
106
+ align-items: center;
107
+ color: var(--sw-color-text);
108
+ display: inline-flex;
109
+ flex: none;
110
+ }
111
+
104
112
  .sw-radio-group-text {
105
113
  color: var(--sw-color-text);
106
114
  font-family: var(--sw-font-sans);
@@ -119,6 +127,7 @@ export function cssRadioGroupClasses() {
119
127
 
120
128
  .sw-radio-group-description { color: var(--sw-color-muted); }
121
129
  .sw-radio-group-error { color: var(--sw-color-danger); }
130
+ .sw-radio-group-error:empty { position: absolute; }
122
131
 
123
132
  @media (prefers-reduced-transparency: reduce) {
124
133
  .sw-radio-group-mark { background: var(--sw-color-surface); }
@@ -129,7 +138,7 @@ export function cssRadioGroupClasses() {
129
138
  .sw-radio-group-mark { background: Canvas; border-color: CanvasText; }
130
139
  .sw-radio-group-input:checked + .sw-radio-group-mark { background: Highlight; border-color: Highlight; }
131
140
  .sw-radio-group-input:checked + .sw-radio-group-mark::after { background: HighlightText; }
132
- .sw-radio-group:has(.sw-radio-group-error) .sw-radio-group-mark { border-color: CanvasText; }
141
+ .sw-radio-group[aria-invalid='true'] .sw-radio-group-mark { border-color: CanvasText; }
133
142
  }`;
134
143
  }
135
144
  export function radioGroupClassCatalog() {
@@ -141,6 +150,7 @@ export function radioGroupClassCatalog() {
141
150
  'sw-radio-group-control',
142
151
  'sw-radio-group-input',
143
152
  'sw-radio-group-mark',
153
+ 'sw-radio-group-icon',
144
154
  'sw-radio-group-text',
145
155
  'sw-radio-group-description',
146
156
  'sw-radio-group-error',
@@ -0,0 +1,2 @@
1
+ export declare function cssSegmentedClasses(): string;
2
+ export declare function segmentedClassCatalog(): string[];