@scalewing/react 1.23.0 → 1.24.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,13 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e6ed0d6: `Field` `changed` now also tints the control's fill (`docs/requests/teisoro-field-validation.md`, 2026-10-09 follow-up, Teisoro F-006-S11 task 1890), so a changed field no longer reads as the focused one: beside the accent border it keeps, the native control or the adorned frame takes the accent at 12 % as a background image over its own fill, which focus never draws. The value keeps 4.5:1 or more on the tint in every palette and scheme (6.74:1 at the lowest); the tint is on the text controls and select only (not a range, color or file input); while a placeholder shows the fill stays plain; on the tint an adorned field's prefix and suffix take the text color; a select keeps its chevron above it, placed for its size (`xs` included) by a private `--sw-select-chevron-inset` the select rule sets; and forced colors drop it and keep the `Highlight` border. No API change: same prop, same `sw-field-changed` class, no new tokens or dependencies. A consumer that layered its own background image on a changed control (none should: Scalewing owns the canvas) would now see it replaced.
8
+
9
+ `BarChartItem` takes an optional `tone` (`docs/requests/teisoro-bar-chart.md`, 2026-10-09 follow-up, Teisoro F-006-S11 task 1890): `'accent'`, `'success'`, `'warning'` or `'danger'` (new exported type `BarChartTone` and list `barChartTones`) colors a bar by what it means in place of its sign's default, such as a diverging chart's overage in warning and shortage in danger. New generated classes `sw-bar-chart-fill-accent`, `sw-bar-chart-fill-success`, `sw-bar-chart-fill-warning` and `sw-bar-chart-fill-danger`; a toned negative bar keeps `sw-bar-chart-fill-negative` and its diverging shape. Without a tone a chart is unchanged. An unknown tone throws a `RangeError`. Forced colors now draw every bar in `CanvasText`: a token fill was painted as the canvas there, so bars were invisible on their tracks. No new tokens and no new dependencies.
10
+
3
11
  ## 1.23.0
4
12
 
5
13
  ### Minor Changes
package/README.md CHANGED
@@ -53,7 +53,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
53
53
 
54
54
  `Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby` and appends a stable ID for the hint, or for the error, which replaces the hint on screen while it is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The error sits in a polite live region (`aria-live="polite"`) that is always rendered, empty while there is no error, so an error that appears while the person types, or after a submit, is announced once, politely, when its text is swapped in; it is never `role="alert"`, so a submit that finds several errors does not fire several assertive alerts at once. Empty, the region takes no room in the layout. The consumer owns validation and localized messages, and after a refused submit it moves focus to the first invalid control (or to one form-level summary). `Checkbox`, `RadioGroup` and `DateField` expose their `error` the same way. `invalid` marks the control invalid the same way (`aria-invalid` and the danger border, `Mark` in forced colors) without a message of its own, for fields whose one error is shown elsewhere, such as a count that adds up to nothing under a group of count fields: point each control's `aria-describedby` at that message so it is read with the field. The field's own polite region stays empty and its `description` stays on screen; `error` implies `invalid`. Passing `invalid`, even `invalid={false}` but not `invalid={undefined}`, needs one native control child and throws a `TypeError` on the first render otherwise, so a form learns it while the fields are valid, not on its first failed submit. `error` keeps its rule: it needs the native control only while it holds a message. `invalid` is `Field`'s only: `DateField`, `Select` and the other controls with `error` do not have it yet.
55
55
 
56
- `Field` `changed` marks a value the person changed from a saved one, such as a field in a correction: the control's border, or an adorned field's frame, takes the accent color a hairline thicker (an inset shadow, so nothing moves; `Highlight` in forced colors). It is a cue beside the words, so say what the value was in `description`; it sets no ARIA state, and `invalid` and `error` win over it. Like `invalid`, passing it, even `false` but not `undefined`, needs one native control child from the first render (a `TypeError` otherwise).
56
+ `Field` `changed` marks a value the person changed from a saved one, such as a field in a correction: the control's border, or an adorned field's frame, takes the accent color a hairline thicker (an inset shadow, so nothing moves; `Highlight` in forced colors), and its fill takes a 12 % accent tint over its own background, so a changed field reads apart from the focused one, whose ring never tints. The value keeps 4.5:1 or more on the tint in every palette; the tint is on text controls and selects (not a range, color or file input), a select's chevron stays where its size puts it, while a placeholder shows the fill stays plain, on the tint an adorned field's prefix and suffix take the text color, and forced colors drop the tint. It is a cue beside the words, so say what the value was in `description`; it sets no ARIA state, and `invalid` and `error` win over it. Like `invalid`, passing it, even `false` but not `undefined`, needs one native control child from the first render (a `TypeError` otherwise).
57
57
 
58
58
  ```tsx
59
59
  <Field invalid={countIsZero} label="$1 bills">
@@ -285,7 +285,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
285
285
 
286
286
  A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `'none'`) and `onSort` is its column's sort control: its children become a button in the header's own style with a chevron (before the name in a `numeric` or end-aligned column), and the cell carries `aria-sort` only while it is the sorted column. `sort` on a data cell, or one of the two without the other, throws a `TypeError`. `Table` `layout="fixed"` (default `auto`) sizes the columns from the header row alone, so a filter that hides a row moves no column, and `verticalAlign="top"` (default `middle`) starts every cell of a tall row on its first line. `TableCell` `width` is `min` (an auto table's column takes the least room its content allows, on one line; set it on each cell of the column) or a size, `xs` to `xl` (4, 6, 8, 12 and 16 rem); in a fixed table a column without a width shares what is left, so leave one column, such as a description, unsized. The HTML `width` attribute is not accepted. Unknown values throw a `RangeError`.
287
287
 
288
- `BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start.
288
+ `BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start. An item's `tone` (`BarChartTone`: `'accent'`, `'success'`, `'warning'` or `'danger'`, listed in the exported `barChartTones`) colors its bar by what it means in place of its sign's default (the accent, or danger for a negative value), such as a diverging chart's overage in `'warning'` and shortage in `'danger'`; a toned bar keeps its sign's place, every tone keeps 3:1 against the track, and an unknown tone throws a `RangeError`. Forced colors draw every bar in the system text color, so the value and its side of zero carry the meaning there.
289
289
 
290
290
  `Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
291
291
 
@@ -1,8 +1,19 @@
1
1
  import { type HTMLAttributes } from 'react';
2
+ import { type BarChartTone, barChartTones } from '../css/css-chart.js';
3
+ export type { BarChartTone };
4
+ export { barChartTones };
2
5
  export type BarChartItem = {
3
6
  label: string;
4
7
  value: number;
5
8
  valueLabel?: string;
9
+ /**
10
+ * Colors the bar by what it means, in place of its sign's default (the
11
+ * accent, or danger for a negative value): such as `'warning'` for an
12
+ * overage and `'danger'` for a shortage. The color is a cue beside the
13
+ * value and its label, never the only sign of the meaning. An unknown tone
14
+ * throws a `RangeError`.
15
+ */
16
+ tone?: BarChartTone;
6
17
  };
7
18
  export type BarChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
8
19
  'aria-label': string;
@@ -2,12 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { barChartFillRatio, barChartScaleMax, formatBarChartValue, } from '@scalewing/tokens';
3
3
  import { forwardRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
+ import { barChartTones } from '../css/css-chart.js';
6
+ export { barChartTones };
7
+ /** The fill's classes: the sign's (which also shapes a diverging bar) and the tone's. */
8
+ function barChartFillClassName(item) {
9
+ if (item.tone !== undefined && !barChartTones.includes(item.tone))
10
+ throw new RangeError(`BarChart item tone must be one of ${barChartTones.join(', ')}; received ${String(item.tone)}`);
11
+ return cx('sw-bar-chart-fill', item.value < 0 && 'sw-bar-chart-fill-negative', item.tone && `sw-bar-chart-fill-${item.tone}`);
12
+ }
5
13
  export const BarChart = forwardRef(function BarChart({ 'aria-label': ariaLabel, className, diverging = false, formatValue = formatBarChartValue, items, max, style, ...rest }, ref) {
6
14
  const scaleMax = barChartScaleMax(items.map((item) => item.value), max);
7
15
  const axis = diverging ? [-scaleMax, 0, scaleMax] : [0, scaleMax];
8
16
  return (_jsxs("div", { ref: ref, className: cx('sw-bar-chart', diverging && 'sw-bar-chart-diverging', className), style: style, ...rest, children: [_jsx("ul", { "aria-label": ariaLabel, className: "sw-bar-chart-plot", children: items.map((item, index) => {
9
17
  const ratio = barChartFillRatio(item.value, scaleMax);
10
- return (_jsxs("li", { className: "sw-bar-chart-row", children: [_jsx("span", { className: "sw-bar-chart-label", children: item.label }), _jsx("span", { className: "sw-bar-chart-track", children: _jsx("span", { className: cx('sw-bar-chart-fill', item.value < 0 && 'sw-bar-chart-fill-negative'), style: {
18
+ return (_jsxs("li", { className: "sw-bar-chart-row", children: [_jsx("span", { className: "sw-bar-chart-label", children: item.label }), _jsx("span", { className: "sw-bar-chart-track", children: _jsx("span", { className: barChartFillClassName(item), style: {
11
19
  '--sw-bar-fill': ratio,
12
20
  } }) }), _jsx("span", { className: "sw-bar-chart-value", children: item.valueLabel ?? formatValue(item.value) })] }, `${item.label}-${index}`));
13
21
  }) }), _jsxs("div", { "aria-hidden": "true", className: "sw-bar-chart-axis", children: [_jsx("span", {}), _jsx("span", { className: "sw-bar-chart-axis-track", children: axis.map((value) => (_jsx("span", { children: formatValue(value) }, value))) }), _jsx("span", {})] })] }));
@@ -21,7 +21,10 @@ export type FieldProps = {
21
21
  /**
22
22
  * Marks a value the person changed from a saved one, such as a field in a
23
23
  * correction: the control's border (or an adorned frame's) in the accent
24
- * color, thicker than the plain hairline. It is a cue, not the record:
24
+ * color, thicker than the plain hairline, and its fill tinted with 12 % of
25
+ * the accent, so a changed field reads apart from the focused one (focus
26
+ * draws a ring and never tints). The value keeps 4.5:1 on the tint; while
27
+ * a placeholder shows, the fill stays plain. It is a cue, not the record:
25
28
  * say what it was in `description` ("Was 25"). `invalid` and `error`
26
29
  * win. Like `invalid`, passing it, even `false` but not `undefined`,
27
30
  * requires one native control child from the first render.
@@ -1,2 +1,9 @@
1
+ /**
2
+ * The meanings a bar's fill can take in place of its sign's default (the
3
+ * accent, or danger for a negative value), such as a diverging chart whose
4
+ * overage is a warning and whose shortage is danger.
5
+ */
6
+ export declare const barChartTones: readonly ["accent", "success", "warning", "danger"];
7
+ export type BarChartTone = (typeof barChartTones)[number];
1
8
  export declare function cssChartClasses(): string;
2
9
  export declare function chartClassCatalog(): string[];
@@ -1,6 +1,30 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  const caption = typographyVariants.caption;
3
3
  const data = typographyVariants.data;
4
+ /**
5
+ * The meanings a bar's fill can take in place of its sign's default (the
6
+ * accent, or danger for a negative value), such as a diverging chart whose
7
+ * overage is a warning and whose shortage is danger.
8
+ */
9
+ export const barChartTones = [
10
+ 'accent',
11
+ 'success',
12
+ 'warning',
13
+ 'danger',
14
+ ];
15
+ /*
16
+ * A tone's rule follows the negative fill's, at the same specificity, so a
17
+ * toned negative bar keeps its diverging geometry and takes the tone's
18
+ * color. Each tone keeps 3:1 against the track in every palette and scheme
19
+ * (`chart.test.tsx`). Forced colors would paint a token fill as the
20
+ * canvas, so every fill, toned or not, is drawn in the system text color
21
+ * after the tones: the value and its sign carry the meaning there.
22
+ */
23
+ function toneRules() {
24
+ return barChartTones
25
+ .map((tone) => `.sw-bar-chart-fill-${tone} { background: var(--sw-color-${tone}); }`)
26
+ .join('\n');
27
+ }
4
28
  /*
5
29
  * A diverging chart puts zero in the middle of each track, marked by a
6
30
  * hairline: a positive bar starts there and grows toward the inline end, a
@@ -98,6 +122,8 @@ export function cssChartClasses() {
98
122
  background: var(--sw-color-danger);
99
123
  }
100
124
 
125
+ ${toneRules()}
126
+
101
127
  .sw-bar-chart-value {
102
128
  color: var(--sw-color-text);
103
129
  font-family: var(--sw-font-sans);
@@ -123,7 +149,11 @@ export function cssChartClasses() {
123
149
  justify-content: space-between;
124
150
  }
125
151
 
126
- ${divergingRules()}`;
152
+ ${divergingRules()}
153
+
154
+ @media (forced-colors: active) {
155
+ .sw-bar-chart-fill { background: CanvasText; }
156
+ }`;
127
157
  }
128
158
  export function chartClassCatalog() {
129
159
  return [
@@ -135,6 +165,7 @@ export function chartClassCatalog() {
135
165
  'sw-bar-chart-track',
136
166
  'sw-bar-chart-fill',
137
167
  'sw-bar-chart-fill-negative',
168
+ ...barChartTones.map((tone) => `sw-bar-chart-fill-${tone}`),
138
169
  'sw-bar-chart-value',
139
170
  'sw-bar-chart-axis-track',
140
171
  'sw-bar-chart-diverging',
@@ -57,10 +57,9 @@ export function cssChromeClasses() {
57
57
  padding-inline: var(--sw-control-xs-padding-inline);
58
58
  }
59
59
 
60
+ /* The chevron's layers place themselves from this inset (selectChevronLayers). */
60
61
  .sw-field-xs select {
61
- background-position:
62
- calc(100% - var(--sw-space-3)) calc(50% - 1px),
63
- calc(100% - calc(var(--sw-space-3) - var(--sw-space-1))) calc(50% - 1px);
62
+ --sw-select-chevron-inset: var(--sw-space-3);
64
63
  height: calc(var(--sw-control-xs-min-height) - 1px - 1px);
65
64
  min-height: 0;
66
65
  padding-inline-end: calc(
@@ -1,7 +1,21 @@
1
+ /** The native controls that take typed text: the canvas's text controls. */
2
+ export declare const typedInputs: string;
1
3
  /** The frame every native text control draws: fill, hairline border, radius. */
2
4
  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
5
  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
6
  export declare const disabledControlForcedColors = "border-color: GrayText;\n color: GrayText;\n -webkit-text-fill-color: GrayText;";
7
+ /**
8
+ * A select's chevron: two muted triangles drawn as background layers, so a
9
+ * rule that adds a layer (a changed field's tint) lists them with it. Their
10
+ * distance from the end is `--sw-select-chevron-inset`, which the select
11
+ * rule sets per size (space 4, and space 3 in an `xs` field), so every rule
12
+ * that lists the layers places them for the size it is in.
13
+ */
14
+ export declare const selectChevronLayers: {
15
+ readonly image: "linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),\n linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%)";
16
+ readonly position: "calc(100% - var(--sw-select-chevron-inset)) calc(50% - 1px),\n calc(100% - calc(var(--sw-select-chevron-inset) - var(--sw-space-1))) calc(50% - 1px)";
17
+ readonly size: "var(--sw-space-1) var(--sw-space-1),\n var(--sw-space-1) var(--sw-space-1)";
18
+ };
5
19
  /** A script-only focus target in the canvas, at zero specificity. */
6
20
  export declare const focusTargetSelector = ":where([data-theme] [tabindex='-1']:focus-visible)";
7
21
  export declare function cssDocumentCanvas(): string;
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  const body = typographyVariants.body;
3
- const typedInputs = [
3
+ /** The native controls that take typed text: the canvas's text controls. */
4
+ export const typedInputs = [
4
5
  "input[type='text']",
5
6
  "input[type='email']",
6
7
  "input[type='number']",
@@ -34,6 +35,21 @@ export const disabledControlSurface = `background-color: var(--sw-color-subtle);
34
35
  export const disabledControlForcedColors = `border-color: GrayText;
35
36
  color: GrayText;
36
37
  -webkit-text-fill-color: GrayText;`;
38
+ /**
39
+ * A select's chevron: two muted triangles drawn as background layers, so a
40
+ * rule that adds a layer (a changed field's tint) lists them with it. Their
41
+ * distance from the end is `--sw-select-chevron-inset`, which the select
42
+ * rule sets per size (space 4, and space 3 in an `xs` field), so every rule
43
+ * that lists the layers places them for the size it is in.
44
+ */
45
+ export const selectChevronLayers = {
46
+ image: `linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
47
+ linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%)`,
48
+ position: `calc(100% - var(--sw-select-chevron-inset)) calc(50% - 1px),
49
+ calc(100% - calc(var(--sw-select-chevron-inset) - var(--sw-space-1))) calc(50% - 1px)`,
50
+ size: `var(--sw-space-1) var(--sw-space-1),
51
+ var(--sw-space-1) var(--sw-space-1)`,
52
+ };
37
53
  /** A script-only focus target in the canvas, at zero specificity. */
38
54
  export const focusTargetSelector = ":where([data-theme] [tabindex='-1']:focus-visible)";
39
55
  export function cssDocumentCanvas() {
@@ -103,16 +119,14 @@ ${focusTargetSelector} {
103
119
 
104
120
  [data-theme] select {
105
121
  ${controlSurface}
122
+ --sw-select-chevron-inset: var(--sw-space-4);
106
123
  appearance: none;
107
124
  background-image:
108
- linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
109
- linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%);
125
+ ${selectChevronLayers.image};
110
126
  background-position:
111
- calc(100% - var(--sw-space-4)) calc(50% - 1px),
112
- calc(100% - calc(var(--sw-space-4) - var(--sw-space-1))) calc(50% - 1px);
127
+ ${selectChevronLayers.position};
113
128
  background-repeat: no-repeat;
114
- background-size: var(--sw-space-1) var(--sw-space-1),
115
- var(--sw-space-1) var(--sw-space-1);
129
+ background-size: ${selectChevronLayers.size};
116
130
  box-sizing: content-box;
117
131
  height: calc(var(--sw-control-md-min-height) - 1px - 1px);
118
132
  min-height: 0;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * How much accent a changed control's fill takes: the share of the accent
3
+ * layered over the control's own background (the glass fill, or the subtle
4
+ * fill of a disabled control). The value keeps 4.5:1 or more on it in every
5
+ * palette and scheme (`field-changed.test.tsx`).
6
+ */
7
+ export declare const changedTintStrength = 0.12;
8
+ /**
9
+ * The tint as one background image, so it lies over the control's background
10
+ * color instead of replacing it.
11
+ */
12
+ export declare const changedTintLayer = "linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent))";
13
+ export declare function cssFieldChangedClasses(): string;
@@ -0,0 +1,64 @@
1
+ import { selectChevronLayers, typedInputs } from './css-document.js';
2
+ /**
3
+ * How much accent a changed control's fill takes: the share of the accent
4
+ * layered over the control's own background (the glass fill, or the subtle
5
+ * fill of a disabled control). The value keeps 4.5:1 or more on it in every
6
+ * palette and scheme (`field-changed.test.tsx`).
7
+ */
8
+ export const changedTintStrength = 0.12;
9
+ const tintColor = `color-mix(in srgb, var(--sw-color-accent) ${changedTintStrength * 100}%, transparent)`;
10
+ /**
11
+ * The tint as one background image, so it lies over the control's background
12
+ * color instead of replacing it.
13
+ */
14
+ export const changedTintLayer = `linear-gradient(${tintColor}, ${tintColor})`;
15
+ /** An adorned frame that holds a value (no placeholder showing). */
16
+ const tintedFrame = '.sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown))';
17
+ /*
18
+ * A changed value: the accent border, a hairline thicker through an inset
19
+ * shadow so nothing moves, and the accent tint over the fill, on the control
20
+ * or on an adorned frame (whose input is transparent and draws no border, so
21
+ * the tint shows once). Focus draws a ring and never tints, so "changed" and
22
+ * "where I am" read apart. While a placeholder shows there is no value to
23
+ * mark and the fill stays plain, so the muted placeholder keeps its contrast;
24
+ * on the tint, an adorned frame's prefix and suffix take the text color. The
25
+ * tint is for the text controls and select (not a range, color or file
26
+ * input). A select lists its chevron above the tint, placed for its size by
27
+ * `--sw-select-chevron-inset`. Forced colors drop the tint and the shadow
28
+ * and keep the Highlight border.
29
+ */
30
+ export function cssFieldChangedClasses() {
31
+ return `[data-theme] .sw-field-changed > :is(input, select, textarea),
32
+ .sw-field-changed .sw-field-adorned {
33
+ border-color: var(--sw-color-accent);
34
+ box-shadow: inset 0 0 0 1px var(--sw-color-accent);
35
+ }
36
+
37
+ [data-theme] .sw-field-changed > :is(${typedInputs}):not(:placeholder-shown),
38
+ ${tintedFrame} {
39
+ background-image: ${changedTintLayer};
40
+ }
41
+
42
+ [data-theme] .sw-field-changed > select {
43
+ background-image:
44
+ ${selectChevronLayers.image},
45
+ ${changedTintLayer};
46
+ background-position:
47
+ ${selectChevronLayers.position},
48
+ 0 0;
49
+ background-size: ${selectChevronLayers.size},
50
+ 100% 100%;
51
+ }
52
+
53
+ ${tintedFrame} > :is(.sw-field-prefix, .sw-field-suffix) {
54
+ color: var(--sw-color-text);
55
+ }
56
+
57
+ @media (forced-colors: active) {
58
+ [data-theme] .sw-field-changed > :is(input, select, textarea),
59
+ .sw-field-changed .sw-field-adorned { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
60
+ [data-theme] .sw-field-changed > :is(${typedInputs}):not(:placeholder-shown),
61
+ ${tintedFrame} { background-image: none; }
62
+ [data-theme] .sw-field-changed > select { background-image: ${selectChevronLayers.image}; }
63
+ }`;
64
+ }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { controlSurface, disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
+ import { cssFieldChangedClasses } from './css-field-changed.js';
3
4
  const caption = typographyVariants.caption;
4
5
  /*
5
6
  * A prefix or suffix sits inside the control frame: the wrapper draws the
@@ -80,24 +81,13 @@ export function cssFieldClasses() {
80
81
  border-color: var(--sw-color-danger);
81
82
  }
82
83
 
83
- /*
84
- * A changed value: the accent border, a hairline thicker through an inset
85
- * shadow so nothing moves, on the control or on an adorned frame (whose
86
- * input draws no border of its own).
87
- */
88
- [data-theme] .sw-field-changed > :is(input, select, textarea),
89
- .sw-field-changed .sw-field-adorned {
90
- border-color: var(--sw-color-accent);
91
- box-shadow: inset 0 0 0 1px var(--sw-color-accent);
92
- }
93
-
94
84
  ${adornmentRules()}
95
85
 
86
+ ${cssFieldChangedClasses()}
87
+
96
88
  @media (forced-colors: active) {
97
89
  [data-theme] .sw-field-invalid :is(input, select, textarea),
98
90
  .sw-field-invalid .sw-field-adorned { border-color: Mark; }
99
- [data-theme] .sw-field-changed > :is(input, select, textarea),
100
- .sw-field-changed .sw-field-adorned { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
101
91
  .sw-field-prefix, .sw-field-suffix { color: CanvasText; }
102
92
  .sw-field-adorned:has(> input:disabled) {
103
93
  ${disabledControlForcedColors}
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { Tooltip, type TooltipProps, type TooltipRelationship, } from './compone
12
12
  export { RadioGroup, type RadioGroupOption, type RadioGroupProps, } from './components/RadioGroup.js';
13
13
  export { AppHeader, type AppHeaderProps } from './components/AppHeader.js';
14
14
  export { Badge, type BadgeProps, type BadgeSize, type BadgeTone, badgeSizes, badgeTones, } from './components/Badge.js';
15
- export { BarChart, type BarChartItem, type BarChartProps, } from './components/BarChart.js';
15
+ export { BarChart, type BarChartItem, type BarChartProps, type BarChartTone, barChartTones, } from './components/BarChart.js';
16
16
  export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, } from './components/Button.js';
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';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { Tooltip, } from './components/Tooltip.js';
11
11
  export { RadioGroup, } from './components/RadioGroup.js';
12
12
  export { AppHeader } from './components/AppHeader.js';
13
13
  export { Badge, badgeSizes, badgeTones, } from './components/Badge.js';
14
- export { BarChart, } from './components/BarChart.js';
14
+ export { BarChart, barChartTones, } from './components/BarChart.js';
15
15
  export { Button, } from './components/Button.js';
16
16
  export { Box, } from './components/Box.js';
17
17
  export { ButtonGroup, } from './components/ButtonGroup.js';
package/dist/styles.css CHANGED
@@ -644,13 +644,14 @@ body {
644
644
  font-family: inherit;
645
645
  font-size: inherit;
646
646
  padding-inline: var(--sw-control-md-padding-inline);
647
+ --sw-select-chevron-inset: var(--sw-space-4);
647
648
  appearance: none;
648
649
  background-image:
649
650
  linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
650
651
  linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%);
651
652
  background-position:
652
- calc(100% - var(--sw-space-4)) calc(50% - 1px),
653
- calc(100% - calc(var(--sw-space-4) - var(--sw-space-1))) calc(50% - 1px);
653
+ calc(100% - var(--sw-select-chevron-inset)) calc(50% - 1px),
654
+ calc(100% - calc(var(--sw-select-chevron-inset) - var(--sw-space-1))) calc(50% - 1px);
654
655
  background-repeat: no-repeat;
655
656
  background-size: var(--sw-space-1) var(--sw-space-1),
656
657
  var(--sw-space-1) var(--sw-space-1);
@@ -1951,6 +1952,11 @@ body {
1951
1952
  background: var(--sw-color-danger);
1952
1953
  }
1953
1954
 
1955
+ .sw-bar-chart-fill-accent { background: var(--sw-color-accent); }
1956
+ .sw-bar-chart-fill-success { background: var(--sw-color-success); }
1957
+ .sw-bar-chart-fill-warning { background: var(--sw-color-warning); }
1958
+ .sw-bar-chart-fill-danger { background: var(--sw-color-danger); }
1959
+
1954
1960
  .sw-bar-chart-value {
1955
1961
  color: var(--sw-color-text);
1956
1962
  font-family: var(--sw-font-sans);
@@ -2008,6 +2014,10 @@ body {
2008
2014
  .sw-bar-chart-diverging .sw-bar-chart-track::before { background: CanvasText; }
2009
2015
  }
2010
2016
 
2017
+ @media (forced-colors: active) {
2018
+ .sw-bar-chart-fill { background: CanvasText; }
2019
+ }
2020
+
2011
2021
  .sw-app-header {
2012
2022
  background: var(--sw-glass-fill);
2013
2023
  border: 1px solid var(--sw-glass-border);
@@ -2061,10 +2071,9 @@ body {
2061
2071
  padding-inline: var(--sw-control-xs-padding-inline);
2062
2072
  }
2063
2073
 
2074
+ /* The chevron's layers place themselves from this inset (selectChevronLayers). */
2064
2075
  .sw-field-xs select {
2065
- background-position:
2066
- calc(100% - var(--sw-space-3)) calc(50% - 1px),
2067
- calc(100% - calc(var(--sw-space-3) - var(--sw-space-1))) calc(50% - 1px);
2076
+ --sw-select-chevron-inset: var(--sw-space-3);
2068
2077
  height: calc(var(--sw-control-xs-min-height) - 1px - 1px);
2069
2078
  min-height: 0;
2070
2079
  padding-inline-end: calc(
@@ -3611,17 +3620,6 @@ body {
3611
3620
  border-color: var(--sw-color-danger);
3612
3621
  }
3613
3622
 
3614
- /*
3615
- * A changed value: the accent border, a hairline thicker through an inset
3616
- * shadow so nothing moves, on the control or on an adorned frame (whose
3617
- * input draws no border of its own).
3618
- */
3619
- [data-theme] .sw-field-changed > :is(input, select, textarea),
3620
- .sw-field-changed .sw-field-adorned {
3621
- border-color: var(--sw-color-accent);
3622
- box-shadow: inset 0 0 0 1px var(--sw-color-accent);
3623
- }
3624
-
3625
3623
  .sw-field-adorned {
3626
3624
  background-color: var(--sw-glass-fill);
3627
3625
  border: 1px solid var(--sw-color-border);
@@ -3685,11 +3683,47 @@ body {
3685
3683
 
3686
3684
  [data-theme] .sw-field-adorned > input:disabled { background: transparent; }
3687
3685
 
3686
+ [data-theme] .sw-field-changed > :is(input, select, textarea),
3687
+ .sw-field-changed .sw-field-adorned {
3688
+ border-color: var(--sw-color-accent);
3689
+ box-shadow: inset 0 0 0 1px var(--sw-color-accent);
3690
+ }
3691
+
3692
+ [data-theme] .sw-field-changed > :is(input[type='text'], input[type='email'], input[type='number'], input[type='search'], input[type='url'], input[type='password'], input:not([type]), textarea):not(:placeholder-shown),
3693
+ .sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) {
3694
+ background-image: linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent));
3695
+ }
3696
+
3697
+ [data-theme] .sw-field-changed > select {
3698
+ background-image:
3699
+ linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
3700
+ linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%),
3701
+ linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent));
3702
+ background-position:
3703
+ calc(100% - var(--sw-select-chevron-inset)) calc(50% - 1px),
3704
+ calc(100% - calc(var(--sw-select-chevron-inset) - var(--sw-space-1))) calc(50% - 1px),
3705
+ 0 0;
3706
+ background-size: var(--sw-space-1) var(--sw-space-1),
3707
+ var(--sw-space-1) var(--sw-space-1),
3708
+ 100% 100%;
3709
+ }
3710
+
3711
+ .sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) > :is(.sw-field-prefix, .sw-field-suffix) {
3712
+ color: var(--sw-color-text);
3713
+ }
3714
+
3688
3715
  @media (forced-colors: active) {
3689
- [data-theme] .sw-field-invalid :is(input, select, textarea),
3690
- .sw-field-invalid .sw-field-adorned { border-color: Mark; }
3691
3716
  [data-theme] .sw-field-changed > :is(input, select, textarea),
3692
3717
  .sw-field-changed .sw-field-adorned { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
3718
+ [data-theme] .sw-field-changed > :is(input[type='text'], input[type='email'], input[type='number'], input[type='search'], input[type='url'], input[type='password'], input:not([type]), textarea):not(:placeholder-shown),
3719
+ .sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) { background-image: none; }
3720
+ [data-theme] .sw-field-changed > select { background-image: linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
3721
+ linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%); }
3722
+ }
3723
+
3724
+ @media (forced-colors: active) {
3725
+ [data-theme] .sw-field-invalid :is(input, select, textarea),
3726
+ .sw-field-invalid .sw-field-adorned { border-color: Mark; }
3693
3727
  .sw-field-prefix, .sw-field-suffix { color: CanvasText; }
3694
3728
  .sw-field-adorned:has(> input:disabled) {
3695
3729
  border-color: GrayText;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",