@scalewing/react 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7d46059: A `DenominationGrid` `strip` wider than its container now scrolls sideways
8
+ inside its own region instead of widening the page (for example an
9
+ eleven-column strip on a 390 px phone, or a six-column strip inside an
10
+ outlined card inside another card). The region is a keyboard-focusable
11
+ `group` named after the grid's `label`, like `Table`'s scroll wrapper, and
12
+ the row labels and the header corner above them stay pinned at the start
13
+ while the counts scroll under them, so each column head stays over its
14
+ counts. The pinned label draws its tone stripe as its own box, so the stripe
15
+ moves with it in every browser.
16
+
17
+ Reduce Transparency now takes effect on every glass surface it lists: the
18
+ fallback block is emitted after all component rules, where before most glass
19
+ rules (app header, sticky table head, dialog, toast and others) came after it
20
+ and kept their blur. The block lists glass surfaces only: the filled
21
+ secondary button left it, so it keeps its own fill and text colors.
22
+
23
+ New generated classes: `.sw-denomination-scroll` (the scroll region) and
24
+ `.sw-denomination-label-body` (the icon, label, and phone total inside the
25
+ row label cell). `Table` and the strip now share one generated scroll-region
26
+ rule; `.sw-table-wrap` output is unchanged. No prop changes. See
27
+ `docs/requests/teisoro-denomination-grid.md`.
28
+
29
+ ## 1.7.0
30
+
31
+ ### Minor Changes
32
+
33
+ - 2444ec5: Button `variant="tertiary"` is a solid fill on `--sw-color-tertiary` with
34
+ `--sw-color-onTertiary`. `secondary` now fills with `--sw-color-secondary`
35
+ and draws `--sw-button-secondary-border`, which stays the hairline unless a
36
+ palette fills the secondary action (for example `signal`). The stylesheet
37
+ emits the new semantic colour properties. Default and cerulean themes look
38
+ unchanged.
39
+ - 622ab77: `Card variant="filled"` (`.sw-card-filled`) is a quiet `--sw-color-subtle`
40
+ fill with no border, for plain information apart from pressable rows.
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies [b5a56b7]
45
+ - Updated dependencies [2444ec5]
46
+ - Updated dependencies [622ab77]
47
+ - @scalewing/tokens@1.3.0
48
+
3
49
  ## 1.6.1
4
50
 
5
51
  ### Patch Changes
package/README.md CHANGED
@@ -66,6 +66,8 @@ Import the CSS once at the application entry. Do not copy it into your source tr
66
66
 
67
67
  `Separator` divides sections with a token-colored line. It is horizontal by default; use `orientation="vertical"` in a flex row. Set `decorative` when the line only supports layout so assistive technology ignores it.
68
68
 
69
+ `Card` defaults to `glass`; `outlined` and `elevated` are solid surfaces, and `filled` is a quiet `--sw-color-subtle` fill with no border for plain information apart from pressable rows.
70
+
69
71
  `Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
70
72
 
71
73
  `Text` takes an optional `align` (`start`, `center`, `end`) mapped to generated `sw-text-align-*` classes, for a heading that must stay centered when it wraps.
@@ -85,7 +87,7 @@ Every `Box`-based component (`Box`, `Stack`, `Inline`, `Card`, `Grid`, …) take
85
87
 
86
88
  Values outside the catalog throw a `RangeError`. There is no per-breakpoint span and no arbitrary column template.
87
89
 
88
- `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that moves under the label on a phone. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
90
+ `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that moves under the label on a phone. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start; the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
89
91
 
90
92
  `FilterChips` presents a long, wrapping single-choice filter set. Provide a localized group `label`, controlled `value`, `onChange`, and options with stable values and localized labels. An optional non-negative integer `count` renders as a tabular chicklet after the label and is part of the option's accessible name; a zero count quiets the chip until it is selected. Native radios provide arrow and Space navigation; disabled options are skipped.
91
93
 
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ type ScrollRegionProps = {
3
+ children: ReactNode;
4
+ className: string;
5
+ 'aria-label'?: string;
6
+ 'aria-labelledby'?: string;
7
+ };
8
+ /**
9
+ * Private wrapper that scrolls wide content inside its own box (see
10
+ * `scrollRegionRules`). It is a keyboard stop named after its content, so a
11
+ * wide table with no focusable cell can still be scrolled sideways from the
12
+ * keyboard and is announced as one group.
13
+ */
14
+ export declare function ScrollRegion({ children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }: ScrollRegionProps): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Private wrapper that scrolls wide content inside its own box (see
4
+ * `scrollRegionRules`). It is a keyboard stop named after its content, so a
5
+ * wide table with no focusable cell can still be scrolled sideways from the
6
+ * keyboard and is announced as one group.
7
+ */
8
+ export function ScrollRegion({ children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }) {
9
+ return (_jsx("div", { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: className, role: "group", tabIndex: 0, children: children }));
10
+ }
@@ -4,11 +4,7 @@ export type TableProps = HTMLAttributes<HTMLTableElement> & {
4
4
  density?: TableDensity;
5
5
  stickyHeader?: boolean;
6
6
  };
7
- /**
8
- * The scroll wrapper is a keyboard stop named after the table, so a wide
9
- * table with no focusable cell can still be scrolled sideways from the
10
- * keyboard and is announced as one group.
11
- */
7
+ /** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
12
8
  export declare const Table: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableElement> & {
13
9
  density?: TableDensity;
14
10
  stickyHeader?: boolean;
@@ -1,13 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, } from 'react';
3
3
  import { cx } from '../class-names.js';
4
- /**
5
- * The scroll wrapper is a keyboard stop named after the table, so a wide
6
- * table with no focusable cell can still be scrolled sideways from the
7
- * keyboard and is announced as one group.
8
- */
4
+ import { ScrollRegion } from './ScrollRegion.js';
5
+ /** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
9
6
  export const Table = forwardRef(function Table({ children, className, density = 'comfortable', stickyHeader = true, ...rest }, ref) {
10
- return (_jsx("div", { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", role: "group", tabIndex: 0, children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', stickyHeader && 'sw-table-sticky', className), ...rest, children: children }) }));
7
+ return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', stickyHeader && 'sw-table-sticky', className), ...rest, children: children }) }));
11
8
  });
12
9
  export const TableHeader = forwardRef(function TableHeader({ className, ...rest }, ref) {
13
10
  return _jsx("thead", { ref: ref, className: className, ...rest });
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellView } from '../../denomination-cells.js';
4
+ import { ScrollRegion } from '../ScrollRegion.js';
4
5
  export function DenominationStrip({ label, columns, rows, zeroLabel, }) {
5
6
  const hasTotals = rows.some((row) => row.total !== undefined);
6
- return (_jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: column.label }, column.key))), hasTotals ? _jsx("td", { className: "sw-denomination-corner" }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsxs("th", { className: "sw-denomination-label", scope: "row", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }), columns.map((column, index) => {
7
- const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
8
- return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
9
- }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }));
7
+ return (_jsx(ScrollRegion, { "aria-label": label, className: "sw-denomination-scroll", children: _jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: column.label }, column.key))), hasTotals ? _jsx("td", { className: "sw-denomination-corner" }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
8
+ const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
9
+ return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
10
+ }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }) }));
10
11
  }
@@ -11,7 +11,8 @@ export function buttonClassNames(options) {
11
11
  function variantRules() {
12
12
  return [
13
13
  `.sw-button-primary { background: var(--sw-color-accent); border-color: transparent; color: var(--sw-color-onAccent); }`,
14
- `.sw-button-secondary { background: var(--sw-color-surface); border-color: var(--sw-color-border); color: var(--sw-color-text); }`,
14
+ `.sw-button-secondary { background: var(--sw-color-secondary); border-color: var(--sw-button-secondary-border); color: var(--sw-color-onSecondary); }`,
15
+ `.sw-button-tertiary { background: var(--sw-color-tertiary); border-color: transparent; color: var(--sw-color-onTertiary); }`,
15
16
  `.sw-button-ghost { background: transparent; border-color: transparent; color: var(--sw-color-accent); }`,
16
17
  `.sw-button-danger { background: var(--sw-color-danger); border-color: transparent; color: var(--sw-color-onDanger); }`,
17
18
  ].join('\n');
@@ -26,6 +26,32 @@ import { cssGridSpanClasses } from './css-grid-span.js';
26
26
  import { cssDenominationGridClasses } from './css-denomination-grid.js';
27
27
  import { cssFieldClasses } from './css-field.js';
28
28
  import { cssToastClasses } from './css-toast.js';
29
+ /*
30
+ * Reduce Transparency swaps every glass surface for the solid surface. It is
31
+ * emitted after every component rule: at equal specificity the later rule wins,
32
+ * so a glass rule emitted after this block would keep its blur.
33
+ */
34
+ function reducedTransparencyRules() {
35
+ return `@media (prefers-reduced-transparency: reduce) {
36
+ .sw-card-glass,
37
+ .sw-app-header,
38
+ .sw-badge-neutral,
39
+ .sw-segmented,
40
+ .sw-table-sticky thead th,
41
+ .sw-denomination-strip .sw-denomination-label,
42
+ .sw-denomination-strip thead .sw-denomination-corner:first-child,
43
+ .sw-bar-chart-track,
44
+ .sw-dialog,
45
+ .sw-accordion,
46
+ .sw-select-list,
47
+ .sw-action-menu-list,
48
+ .sw-toast {
49
+ background: var(--sw-color-surface);
50
+ backdrop-filter: none;
51
+ -webkit-backdrop-filter: none;
52
+ }
53
+ }`;
54
+ }
29
55
  function textVariantRules() {
30
56
  return Object.entries(typographyVariants)
31
57
  .map(([name, variant]) => {
@@ -74,23 +100,9 @@ export function cssComponentClasses() {
74
100
  box-shadow: var(--sw-elevation-sm);
75
101
  }
76
102
 
77
- @media (prefers-reduced-transparency: reduce) {
78
- .sw-card-glass,
79
- .sw-app-header,
80
- .sw-button-secondary,
81
- .sw-badge-neutral,
82
- .sw-segmented,
83
- .sw-table-sticky thead th,
84
- .sw-bar-chart-track,
85
- .sw-dialog,
86
- .sw-accordion,
87
- .sw-select-list,
88
- .sw-action-menu-list,
89
- .sw-toast {
90
- background: var(--sw-color-surface);
91
- backdrop-filter: none;
92
- -webkit-backdrop-filter: none;
93
- }
103
+ /* Plain information on a quiet fill, apart from rows that press. */
104
+ .sw-card-filled {
105
+ background: var(--sw-color-subtle);
94
106
  }
95
107
 
96
108
  .sw-text-align-start { text-align: start; }
@@ -159,5 +171,7 @@ ${cssSplitClasses()}
159
171
 
160
172
  ${cssToastClasses()}
161
173
 
162
- ${cssResponsiveClasses()}`;
174
+ ${cssResponsiveClasses()}
175
+
176
+ ${reducedTransparencyRules()}`;
163
177
  }
@@ -1,4 +1,5 @@
1
1
  import { trackInset, typographyVariants } from '@scalewing/tokens';
2
+ import { scrollRegionRules } from './css-scroll-region.js';
2
3
  export const badgeTones = [
3
4
  'neutral',
4
5
  'accent',
@@ -134,15 +135,7 @@ export function cssDataClasses() {
134
135
  cursor: not-allowed;
135
136
  }
136
137
 
137
- .sw-table-wrap {
138
- overflow: auto;
139
- width: 100%;
140
- }
141
-
142
- .sw-table-wrap:focus-visible {
143
- outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
144
- outline-offset: calc(-1 * var(--sw-focus-ring-width));
145
- }
138
+ ${scrollRegionRules('.sw-table-wrap')}
146
139
 
147
140
  .sw-table {
148
141
  border-collapse: collapse;
@@ -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 { scrollRegionRules } from './css-scroll-region.js';
4
5
  const caption = typographyVariants.caption;
5
6
  const data = typographyVariants.data;
6
7
  const label = typographyVariants.label;
@@ -20,6 +21,8 @@ export function cssDenominationGridClasses() {
20
21
 
21
22
  ${toneRules()}
22
23
 
24
+ ${scrollRegionRules('.sw-denomination-scroll')}
25
+
23
26
  .sw-denomination-strip {
24
27
  border-collapse: collapse;
25
28
  width: 100%;
@@ -49,20 +52,56 @@ ${toneRules()}
49
52
  }
50
53
 
51
54
  .sw-denomination-label {
52
- align-items: center;
53
55
  border-inline-start: var(--sw-space-1) solid var(--sw-denomination-tone);
54
56
  color: var(--sw-denomination-tone);
55
- display: flex;
56
- flex-wrap: wrap;
57
57
  font-size: ${label.fontSize}px;
58
58
  font-weight: ${label.fontWeight};
59
- gap: var(--sw-space-2);
60
59
  letter-spacing: ${label.letterSpacing}px;
61
60
  line-height: ${label.lineHeight}px;
62
61
  min-width: 0;
63
62
  text-align: start;
64
63
  }
65
64
 
65
+ .sw-denomination-tiles .sw-denomination-label,
66
+ .sw-denomination-label-body {
67
+ align-items: center;
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ gap: var(--sw-space-2);
71
+ }
72
+
73
+ /*
74
+ * The row labels and the header corner above them stay in view while a wide
75
+ * strip scrolls under them, so each column head stays over its counts.
76
+ */
77
+ .sw-denomination-strip .sw-denomination-label,
78
+ .sw-denomination-strip thead .sw-denomination-corner:first-child {
79
+ background: var(--sw-glass-fill);
80
+ backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
81
+ -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
82
+ inset-inline-start: 0;
83
+ position: sticky;
84
+ z-index: 1;
85
+ }
86
+
87
+ /*
88
+ * A collapsed-table border does not travel with a sticky cell in every
89
+ * browser, so the pinned label draws its tone stripe as its own box.
90
+ */
91
+ .sw-denomination-strip .sw-denomination-label {
92
+ border-inline-start: 0;
93
+ padding-inline-start: calc(var(--sw-space-2) + var(--sw-space-1));
94
+ }
95
+
96
+ .sw-denomination-strip .sw-denomination-label::before {
97
+ background: var(--sw-denomination-tone);
98
+ content: '';
99
+ inset-block: 0;
100
+ inset-inline-start: 0;
101
+ position: absolute;
102
+ width: var(--sw-space-1);
103
+ }
104
+
66
105
  .sw-denomination-icon {
67
106
  display: inline-flex;
68
107
  flex: none;
@@ -172,14 +211,16 @@ ${toneRules()}
172
211
  @media ${breakpointQuery('below', 'md')} {
173
212
  .sw-denomination-strip .sw-denomination-total { display: none; }
174
213
  .sw-denomination-total-inline { display: block; }
175
- .sw-denomination-strip .sw-denomination-label { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
214
+ .sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
176
215
  .sw-denomination-strip th,
177
216
  .sw-denomination-strip td { padding: var(--sw-space-1); }
217
+ .sw-denomination-strip .sw-denomination-label { padding-inline-start: calc(var(--sw-space-1) * 2); }
178
218
  .sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
179
219
  }
180
220
 
181
221
  @media (forced-colors: active) {
182
222
  .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
223
+ .sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
183
224
  .sw-denomination-cell-zero { color: GrayText; opacity: 1; }
184
225
  .sw-denomination-cell-negative,
185
226
  .sw-denomination-row-signed .sw-denomination-cell-positive,
@@ -190,6 +231,7 @@ ${toneRules()}
190
231
  export function denominationGridClassCatalog() {
191
232
  return [
192
233
  'sw-denomination-grid',
234
+ 'sw-denomination-scroll',
193
235
  ...badgeTones.map((tone) => `sw-denomination-row-${tone}`),
194
236
  'sw-denomination-strip',
195
237
  'sw-denomination-head',
@@ -197,6 +239,7 @@ export function denominationGridClassCatalog() {
197
239
  'sw-denomination-row',
198
240
  'sw-denomination-row-signed',
199
241
  'sw-denomination-label',
242
+ 'sw-denomination-label-body',
200
243
  'sw-denomination-label-text',
201
244
  'sw-denomination-icon',
202
245
  'sw-denomination-cell',
@@ -1,4 +1,4 @@
1
- import { colorsForPalette, createTheme, paletteHasStylesheet, palettesWithStylesheets, semanticColorKeys, } from '@scalewing/tokens';
1
+ import { colorsForPalette, createTheme, paletteHasStylesheet, palettesWithStylesheets, secondaryActionBorder, semanticColorKeys, } from '@scalewing/tokens';
2
2
  function paletteCustomProperties(id, scheme) {
3
3
  const overlay = colorsForPalette(id, scheme);
4
4
  const theme = createTheme({ colorScheme: scheme, palette: id });
@@ -8,6 +8,10 @@ function paletteCustomProperties(id, scheme) {
8
8
  lines.push(` --sw-color-${key}: ${theme.colors[key]};`);
9
9
  }
10
10
  }
11
+ // The secondary outline derives from these three, so it follows them.
12
+ if (overlay.secondary || overlay.surface || overlay.border) {
13
+ lines.push(` --sw-button-secondary-border: ${secondaryActionBorder(theme.colors)};`);
14
+ }
11
15
  lines.push(` --sw-glass-blur: ${theme.glass.blur}px;`);
12
16
  lines.push(` --sw-glass-saturate: ${theme.glass.saturate};`);
13
17
  lines.push(` --sw-glass-fill: ${theme.glass.fill};`);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A scroll region is a full-width box that scrolls its own content when the
3
+ * content is wider (or taller) than the box, so a wide table never widens the
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.
7
+ */
8
+ export declare function scrollRegionRules(selector: string): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A scroll region is a full-width box that scrolls its own content when the
3
+ * content is wider (or taller) than the box, so a wide table never widens the
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.
7
+ */
8
+ export function scrollRegionRules(selector) {
9
+ return `${selector} {
10
+ overflow: auto;
11
+ width: 100%;
12
+ }
13
+
14
+ ${selector}:focus-visible {
15
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
16
+ outline-offset: calc(-1 * var(--sw-focus-ring-width));
17
+ }`;
18
+ }
@@ -1,8 +1,9 @@
1
- import { controlScale, disabledOpacity, focusRing, quietOpacity, radiusScale, semanticColorKeys, spacingScale, spacingSteps, splitScale, } from '@scalewing/tokens';
1
+ import { controlScale, disabledOpacity, focusRing, quietOpacity, radiusScale, secondaryActionBorder, semanticColorKeys, spacingScale, spacingSteps, splitScale, } from '@scalewing/tokens';
2
2
  function colorDeclarations(theme) {
3
- return semanticColorKeys
4
- .map((key) => ` --sw-color-${key}: ${theme.colors[key]};`)
5
- .join('\n');
3
+ return [
4
+ ...semanticColorKeys.map((key) => ` --sw-color-${key}: ${theme.colors[key]};`),
5
+ ` --sw-button-secondary-border: ${secondaryActionBorder(theme.colors)};`,
6
+ ].join('\n');
6
7
  }
7
8
  function glassDeclarations(theme) {
8
9
  return ` --sw-glass-blur: ${theme.glass.blur}px;
@@ -8,6 +8,7 @@
8
8
  --sw-color-accent: #3E3E24;
9
9
  --sw-color-onAccent: #FFFFFF;
10
10
  --sw-color-border: #C9C2AE;
11
+ --sw-button-secondary-border: #C9C2AE;
11
12
  --sw-glass-blur: 24px;
12
13
  --sw-glass-saturate: 1;
13
14
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -23,6 +24,7 @@
23
24
  --sw-color-accent: #CEA453;
24
25
  --sw-color-onAccent: #101214;
25
26
  --sw-color-border: #3D5238;
27
+ --sw-button-secondary-border: #3D5238;
26
28
  --sw-glass-blur: 24px;
27
29
  --sw-glass-saturate: 1.8;
28
30
  --sw-glass-fill: rgba(60, 52, 40, 0.72);
@@ -8,6 +8,7 @@
8
8
  --sw-color-accent: #473333;
9
9
  --sw-color-onAccent: #FFFFFF;
10
10
  --sw-color-border: #E0CBA8;
11
+ --sw-button-secondary-border: #E0CBA8;
11
12
  --sw-glass-blur: 24px;
12
13
  --sw-glass-saturate: 1;
13
14
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -23,6 +24,7 @@
23
24
  --sw-color-accent: #E8C69F;
24
25
  --sw-color-onAccent: #101214;
25
26
  --sw-color-border: #6B5353;
27
+ --sw-button-secondary-border: #6B5353;
26
28
  --sw-glass-blur: 24px;
27
29
  --sw-glass-saturate: 1.8;
28
30
  --sw-glass-fill: rgba(65, 59, 53, 0.72);
@@ -8,6 +8,7 @@
8
8
  --sw-color-accent: #A33B2B;
9
9
  --sw-color-onAccent: #FFFFFF;
10
10
  --sw-color-border: #D0D0B8;
11
+ --sw-button-secondary-border: #D0D0B8;
11
12
  --sw-glass-blur: 24px;
12
13
  --sw-glass-saturate: 1;
13
14
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -23,6 +24,7 @@
23
24
  --sw-color-accent: #EEAF90;
24
25
  --sw-color-onAccent: #101214;
25
26
  --sw-color-border: #3A4158;
27
+ --sw-button-secondary-border: #3A4158;
26
28
  --sw-glass-blur: 24px;
27
29
  --sw-glass-saturate: 1.8;
28
30
  --sw-glass-fill: rgba(66, 54, 51, 0.72);
@@ -0,0 +1,31 @@
1
+ /* Generated by @scalewing/tokens. Do not edit by hand. */
2
+ :root,
3
+ [data-theme='light'] {
4
+ --sw-color-accent: #0066CC;
5
+ --sw-color-onAccent: #FFFFFF;
6
+ --sw-color-secondary: #1D1D1F;
7
+ --sw-color-onSecondary: #FFFFFF;
8
+ --sw-color-tertiary: #7C5CD6;
9
+ --sw-color-onTertiary: #FFFFFF;
10
+ --sw-button-secondary-border: #1D1D1F;
11
+ --sw-glass-blur: 24px;
12
+ --sw-glass-saturate: 1;
13
+ --sw-glass-fill: rgba(255, 255, 255, 1);
14
+ --sw-glass-border: rgba(210, 210, 215, 1);
15
+ --sw-glass-specular: rgba(255, 255, 255, 0);
16
+ }
17
+
18
+ [data-theme='dark'] {
19
+ --sw-color-accent: #5AC8FA;
20
+ --sw-color-onAccent: #101214;
21
+ --sw-color-secondary: #F5F5F7;
22
+ --sw-color-onSecondary: #101214;
23
+ --sw-color-tertiary: #C4B5FD;
24
+ --sw-color-onTertiary: #101214;
25
+ --sw-button-secondary-border: #F5F5F7;
26
+ --sw-glass-blur: 24px;
27
+ --sw-glass-saturate: 1.8;
28
+ --sw-glass-fill: rgba(39, 59, 70, 0.72);
29
+ --sw-glass-border: rgba(189, 233, 253, 0.22);
30
+ --sw-glass-specular: rgba(222, 244, 254, 0.28);
31
+ }
@@ -8,6 +8,7 @@
8
8
  --sw-color-accent: #A33B20;
9
9
  --sw-color-onAccent: #FFFFFF;
10
10
  --sw-color-border: #E5C48A;
11
+ --sw-button-secondary-border: #E5C48A;
11
12
  --sw-glass-blur: 24px;
12
13
  --sw-glass-saturate: 1;
13
14
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -23,6 +24,7 @@
23
24
  --sw-color-accent: #FAA968;
24
25
  --sw-color-onAccent: #101214;
25
26
  --sw-color-border: #1A5A70;
27
+ --sw-button-secondary-border: #1A5A70;
26
28
  --sw-glass-blur: 24px;
27
29
  --sw-glass-saturate: 1.8;
28
30
  --sw-glass-fill: rgba(68, 53, 43, 0.72);
@@ -18,6 +18,7 @@
18
18
  --sw-color-accent: #FF2A6D;
19
19
  --sw-color-onAccent: #101214;
20
20
  --sw-color-border: #4A2C6A;
21
+ --sw-button-secondary-border: #4A2C6A;
21
22
  --sw-glass-blur: 24px;
22
23
  --sw-glass-saturate: 1.8;
23
24
  --sw-glass-fill: rgba(69, 31, 44, 0.72);
package/dist/styles.css CHANGED
@@ -7,11 +7,17 @@
7
7
  --sw-color-muted: #6E6E73;
8
8
  --sw-color-accent: #5B3DF5;
9
9
  --sw-color-onAccent: #FFFFFF;
10
+ --sw-color-secondary: #FFFFFF;
11
+ --sw-color-onSecondary: #1D1D1F;
12
+ --sw-color-tertiary: #48484A;
13
+ --sw-color-onTertiary: #FFFFFF;
14
+ --sw-color-subtle: #F2F2F5;
10
15
  --sw-color-danger: #B42318;
11
16
  --sw-color-onDanger: #FFFFFF;
12
17
  --sw-color-success: #087443;
13
18
  --sw-color-warning: #B54708;
14
19
  --sw-color-border: #D2D2D7;
20
+ --sw-button-secondary-border: #D2D2D7;
15
21
  --sw-space-0: 0px;
16
22
  --sw-space-1: 4px;
17
23
  --sw-space-2: 8px;
@@ -64,11 +70,17 @@
64
70
  --sw-color-muted: #A1A1A6;
65
71
  --sw-color-accent: #A78BFA;
66
72
  --sw-color-onAccent: #101214;
73
+ --sw-color-secondary: #1C1C1E;
74
+ --sw-color-onSecondary: #F5F5F7;
75
+ --sw-color-tertiary: #D1D1D6;
76
+ --sw-color-onTertiary: #101214;
77
+ --sw-color-subtle: #2C2C2E;
67
78
  --sw-color-danger: #F97066;
68
79
  --sw-color-onDanger: #101214;
69
80
  --sw-color-success: #5BE0A0;
70
81
  --sw-color-warning: #FDB022;
71
82
  --sw-color-border: #3A3A3C;
83
+ --sw-button-secondary-border: #3A3A3C;
72
84
  --sw-space-0: 0px;
73
85
  --sw-space-1: 4px;
74
86
  --sw-space-2: 8px;
@@ -134,6 +146,36 @@
134
146
  --sw-glass-specular: rgba(222, 244, 254, 0.28);
135
147
  }
136
148
 
149
+ [data-theme='light'][data-palette='signal'] {
150
+ --sw-color-accent: #0066CC;
151
+ --sw-color-onAccent: #FFFFFF;
152
+ --sw-color-secondary: #1D1D1F;
153
+ --sw-color-onSecondary: #FFFFFF;
154
+ --sw-color-tertiary: #7C5CD6;
155
+ --sw-color-onTertiary: #FFFFFF;
156
+ --sw-button-secondary-border: #1D1D1F;
157
+ --sw-glass-blur: 24px;
158
+ --sw-glass-saturate: 1;
159
+ --sw-glass-fill: rgba(255, 255, 255, 1);
160
+ --sw-glass-border: rgba(210, 210, 215, 1);
161
+ --sw-glass-specular: rgba(255, 255, 255, 0);
162
+ }
163
+
164
+ [data-theme='dark'][data-palette='signal'] {
165
+ --sw-color-accent: #5AC8FA;
166
+ --sw-color-onAccent: #101214;
167
+ --sw-color-secondary: #F5F5F7;
168
+ --sw-color-onSecondary: #101214;
169
+ --sw-color-tertiary: #C4B5FD;
170
+ --sw-color-onTertiary: #101214;
171
+ --sw-button-secondary-border: #F5F5F7;
172
+ --sw-glass-blur: 24px;
173
+ --sw-glass-saturate: 1.8;
174
+ --sw-glass-fill: rgba(39, 59, 70, 0.72);
175
+ --sw-glass-border: rgba(189, 233, 253, 0.22);
176
+ --sw-glass-specular: rgba(222, 244, 254, 0.28);
177
+ }
178
+
137
179
  [data-theme='light'][data-palette='navy'] {
138
180
  --sw-color-accent: #0A2540;
139
181
  --sw-color-onAccent: #FFFFFF;
@@ -342,6 +384,7 @@
342
384
  --sw-color-accent: #A33B20;
343
385
  --sw-color-onAccent: #FFFFFF;
344
386
  --sw-color-border: #E5C48A;
387
+ --sw-button-secondary-border: #E5C48A;
345
388
  --sw-glass-blur: 24px;
346
389
  --sw-glass-saturate: 1;
347
390
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -357,6 +400,7 @@
357
400
  --sw-color-accent: #FAA968;
358
401
  --sw-color-onAccent: #101214;
359
402
  --sw-color-border: #1A5A70;
403
+ --sw-button-secondary-border: #1A5A70;
360
404
  --sw-glass-blur: 24px;
361
405
  --sw-glass-saturate: 1.8;
362
406
  --sw-glass-fill: rgba(68, 53, 43, 0.72);
@@ -372,6 +416,7 @@
372
416
  --sw-color-accent: #3E3E24;
373
417
  --sw-color-onAccent: #FFFFFF;
374
418
  --sw-color-border: #C9C2AE;
419
+ --sw-button-secondary-border: #C9C2AE;
375
420
  --sw-glass-blur: 24px;
376
421
  --sw-glass-saturate: 1;
377
422
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -387,6 +432,7 @@
387
432
  --sw-color-accent: #CEA453;
388
433
  --sw-color-onAccent: #101214;
389
434
  --sw-color-border: #3D5238;
435
+ --sw-button-secondary-border: #3D5238;
390
436
  --sw-glass-blur: 24px;
391
437
  --sw-glass-saturate: 1.8;
392
438
  --sw-glass-fill: rgba(60, 52, 40, 0.72);
@@ -402,6 +448,7 @@
402
448
  --sw-color-accent: #A33B2B;
403
449
  --sw-color-onAccent: #FFFFFF;
404
450
  --sw-color-border: #D0D0B8;
451
+ --sw-button-secondary-border: #D0D0B8;
405
452
  --sw-glass-blur: 24px;
406
453
  --sw-glass-saturate: 1;
407
454
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -417,6 +464,7 @@
417
464
  --sw-color-accent: #EEAF90;
418
465
  --sw-color-onAccent: #101214;
419
466
  --sw-color-border: #3A4158;
467
+ --sw-button-secondary-border: #3A4158;
420
468
  --sw-glass-blur: 24px;
421
469
  --sw-glass-saturate: 1.8;
422
470
  --sw-glass-fill: rgba(66, 54, 51, 0.72);
@@ -432,6 +480,7 @@
432
480
  --sw-color-accent: #473333;
433
481
  --sw-color-onAccent: #FFFFFF;
434
482
  --sw-color-border: #E0CBA8;
483
+ --sw-button-secondary-border: #E0CBA8;
435
484
  --sw-glass-blur: 24px;
436
485
  --sw-glass-saturate: 1;
437
486
  --sw-glass-fill: rgba(255, 255, 255, 1);
@@ -447,6 +496,7 @@
447
496
  --sw-color-accent: #E8C69F;
448
497
  --sw-color-onAccent: #101214;
449
498
  --sw-color-border: #6B5353;
499
+ --sw-button-secondary-border: #6B5353;
450
500
  --sw-glass-blur: 24px;
451
501
  --sw-glass-saturate: 1.8;
452
502
  --sw-glass-fill: rgba(65, 59, 53, 0.72);
@@ -472,6 +522,7 @@
472
522
  --sw-color-accent: #FF2A6D;
473
523
  --sw-color-onAccent: #101214;
474
524
  --sw-color-border: #4A2C6A;
525
+ --sw-button-secondary-border: #4A2C6A;
475
526
  --sw-glass-blur: 24px;
476
527
  --sw-glass-saturate: 1.8;
477
528
  --sw-glass-fill: rgba(69, 31, 44, 0.72);
@@ -700,23 +751,9 @@ body {
700
751
  box-shadow: var(--sw-elevation-sm);
701
752
  }
702
753
 
703
- @media (prefers-reduced-transparency: reduce) {
704
- .sw-card-glass,
705
- .sw-app-header,
706
- .sw-button-secondary,
707
- .sw-badge-neutral,
708
- .sw-segmented,
709
- .sw-table-sticky thead th,
710
- .sw-bar-chart-track,
711
- .sw-dialog,
712
- .sw-accordion,
713
- .sw-select-list,
714
- .sw-action-menu-list,
715
- .sw-toast {
716
- background: var(--sw-color-surface);
717
- backdrop-filter: none;
718
- -webkit-backdrop-filter: none;
719
- }
754
+ /* Plain information on a quiet fill, apart from rows that press. */
755
+ .sw-card-filled {
756
+ background: var(--sw-color-subtle);
720
757
  }
721
758
 
722
759
  .sw-text-align-start { text-align: start; }
@@ -779,7 +816,8 @@ body {
779
816
  }
780
817
 
781
818
  .sw-button-primary { background: var(--sw-color-accent); border-color: transparent; color: var(--sw-color-onAccent); }
782
- .sw-button-secondary { background: var(--sw-color-surface); border-color: var(--sw-color-border); color: var(--sw-color-text); }
819
+ .sw-button-secondary { background: var(--sw-color-secondary); border-color: var(--sw-button-secondary-border); color: var(--sw-color-onSecondary); }
820
+ .sw-button-tertiary { background: var(--sw-color-tertiary); border-color: transparent; color: var(--sw-color-onTertiary); }
783
821
  .sw-button-ghost { background: transparent; border-color: transparent; color: var(--sw-color-accent); }
784
822
  .sw-button-danger { background: var(--sw-color-danger); border-color: transparent; color: var(--sw-color-onDanger); }
785
823
  .sw-button-xs { min-height: 28px; padding-inline: 10px; }
@@ -2217,6 +2255,16 @@ body {
2217
2255
  .sw-denomination-row-danger { --sw-denomination-tone: var(--sw-color-danger); }
2218
2256
  .sw-denomination-row-warning { --sw-denomination-tone: var(--sw-color-warning); }
2219
2257
 
2258
+ .sw-denomination-scroll {
2259
+ overflow: auto;
2260
+ width: 100%;
2261
+ }
2262
+
2263
+ .sw-denomination-scroll:focus-visible {
2264
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
2265
+ outline-offset: calc(-1 * var(--sw-focus-ring-width));
2266
+ }
2267
+
2220
2268
  .sw-denomination-strip {
2221
2269
  border-collapse: collapse;
2222
2270
  width: 100%;
@@ -2246,20 +2294,56 @@ body {
2246
2294
  }
2247
2295
 
2248
2296
  .sw-denomination-label {
2249
- align-items: center;
2250
2297
  border-inline-start: var(--sw-space-1) solid var(--sw-denomination-tone);
2251
2298
  color: var(--sw-denomination-tone);
2252
- display: flex;
2253
- flex-wrap: wrap;
2254
2299
  font-size: 15px;
2255
2300
  font-weight: 600;
2256
- gap: var(--sw-space-2);
2257
2301
  letter-spacing: -0.2px;
2258
2302
  line-height: 20px;
2259
2303
  min-width: 0;
2260
2304
  text-align: start;
2261
2305
  }
2262
2306
 
2307
+ .sw-denomination-tiles .sw-denomination-label,
2308
+ .sw-denomination-label-body {
2309
+ align-items: center;
2310
+ display: flex;
2311
+ flex-wrap: wrap;
2312
+ gap: var(--sw-space-2);
2313
+ }
2314
+
2315
+ /*
2316
+ * The row labels and the header corner above them stay in view while a wide
2317
+ * strip scrolls under them, so each column head stays over its counts.
2318
+ */
2319
+ .sw-denomination-strip .sw-denomination-label,
2320
+ .sw-denomination-strip thead .sw-denomination-corner:first-child {
2321
+ background: var(--sw-glass-fill);
2322
+ backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
2323
+ -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
2324
+ inset-inline-start: 0;
2325
+ position: sticky;
2326
+ z-index: 1;
2327
+ }
2328
+
2329
+ /*
2330
+ * A collapsed-table border does not travel with a sticky cell in every
2331
+ * browser, so the pinned label draws its tone stripe as its own box.
2332
+ */
2333
+ .sw-denomination-strip .sw-denomination-label {
2334
+ border-inline-start: 0;
2335
+ padding-inline-start: calc(var(--sw-space-2) + var(--sw-space-1));
2336
+ }
2337
+
2338
+ .sw-denomination-strip .sw-denomination-label::before {
2339
+ background: var(--sw-denomination-tone);
2340
+ content: '';
2341
+ inset-block: 0;
2342
+ inset-inline-start: 0;
2343
+ position: absolute;
2344
+ width: var(--sw-space-1);
2345
+ }
2346
+
2263
2347
  .sw-denomination-icon {
2264
2348
  display: inline-flex;
2265
2349
  flex: none;
@@ -2369,14 +2453,16 @@ body {
2369
2453
  @media not all and (min-width: 48rem) {
2370
2454
  .sw-denomination-strip .sw-denomination-total { display: none; }
2371
2455
  .sw-denomination-total-inline { display: block; }
2372
- .sw-denomination-strip .sw-denomination-label { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
2456
+ .sw-denomination-label-body { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
2373
2457
  .sw-denomination-strip th,
2374
2458
  .sw-denomination-strip td { padding: var(--sw-space-1); }
2459
+ .sw-denomination-strip .sw-denomination-label { padding-inline-start: calc(var(--sw-space-1) * 2); }
2375
2460
  .sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2376
2461
  }
2377
2462
 
2378
2463
  @media (forced-colors: active) {
2379
2464
  .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
2465
+ .sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
2380
2466
  .sw-denomination-cell-zero { color: GrayText; opacity: 1; }
2381
2467
  .sw-denomination-cell-negative,
2382
2468
  .sw-denomination-row-signed .sw-denomination-cell-positive,
@@ -2563,3 +2649,23 @@ body {
2563
2649
  @media not all and (min-width: 48rem) {
2564
2650
  .sw-hide-below-md { display: none; }
2565
2651
  }
2652
+
2653
+ @media (prefers-reduced-transparency: reduce) {
2654
+ .sw-card-glass,
2655
+ .sw-app-header,
2656
+ .sw-badge-neutral,
2657
+ .sw-segmented,
2658
+ .sw-table-sticky thead th,
2659
+ .sw-denomination-strip .sw-denomination-label,
2660
+ .sw-denomination-strip thead .sw-denomination-corner:first-child,
2661
+ .sw-bar-chart-track,
2662
+ .sw-dialog,
2663
+ .sw-accordion,
2664
+ .sw-select-list,
2665
+ .sw-action-menu-list,
2666
+ .sw-toast {
2667
+ background: var(--sw-color-surface);
2668
+ backdrop-filter: none;
2669
+ -webkit-backdrop-filter: none;
2670
+ }
2671
+ }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useEffect, useMemo, useState, } from 'react';
3
- import { createTheme, semanticColorKeys, } from '@scalewing/tokens';
3
+ import { createTheme, secondaryActionBorder, semanticColorKeys, } from '@scalewing/tokens';
4
4
  const ThemeContext = createContext(null);
5
5
  function schemeFromMedia() {
6
6
  if (typeof window === 'undefined' ||
@@ -16,6 +16,7 @@ function themeCssVars(theme) {
16
16
  for (const key of semanticColorKeys) {
17
17
  vars[`--sw-color-${key}`] = theme.colors[key];
18
18
  }
19
+ vars['--sw-button-secondary-border'] = secondaryActionBorder(theme.colors);
19
20
  vars['--sw-glass-blur'] = `${theme.glass.blur}px`;
20
21
  vars['--sw-glass-saturate'] = String(theme.glass.saturate);
21
22
  vars['--sw-glass-fill'] = theme.glass.fill;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "README.md"
39
39
  ],
40
40
  "dependencies": {
41
- "@scalewing/tokens": "^1.2.0"
41
+ "@scalewing/tokens": "^1.3.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^19.0.0",