@scalewing/react 1.22.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,20 @@
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
+
11
+ ## 1.23.0
12
+
13
+ ### Minor Changes
14
+
15
+ - f51d196: A `Button` inside `Nav` is at least 44 × 44 px on a coarse pointer (`docs/requests/teisoro-nav.md`, Teisoro F-006-S11, UX-16), whatever its `size`, as `SectionNav` links, `CalendarButton` and the `ActionMenu` trigger already are; a fine pointer is unchanged. Only the minimum height and width grow, so a label beside its glyph keeps its line. No new classes, tokens or dependencies.
16
+ - f51d196: `hideBelow` and `hideFrom` take `lg` (64rem) (`docs/requests/teisoro-responsive-visibility.md`, 2026-10-09 follow-up, Teisoro F-006-S11, SET-3 and UX-16), for content that fits only from a laptop up, such as a row of labelled destinations a tablet shows as glyphs. New generated classes `sw-hide-below-lg` and `sw-hide-from-lg`; new exported types `VisibilityBreakpoint` (`'md' | 'lg'`) and `BreakpointDirection` (`'from' | 'below'`). The layout props keep the one layout breakpoint, `md`. `breakpointQuery(direction, breakpoint)` is now exported: the media query a hide class uses, so an app's `matchMedia` follows the same width; an unknown direction or breakpoint throws a `RangeError`. An unknown `hideBelow` or `hideFrom` now throws a `RangeError` (it produced a class with no rule). No new tokens and no new dependencies.
17
+
3
18
  ## 1.22.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -49,11 +49,11 @@ Import the CSS once at the application entry. Do not copy it into your source tr
49
49
 
50
50
  `Box` `border` draws a hairline in the `border` color token: `border` (or `border={true}`) is solid, and `border="dashed"` is dashed (the generated `sw-border` and `sw-border-dashed` classes; a `style` you pass still wins), for a space to fill in by hand such as a blank on a printed form. It does not make the box look or act like an input. Every `Box`-based component takes it.
51
51
 
52
- `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Select` is a labeled listbox menu when the open list must match the canvas.
52
+ `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Select` is a labeled listbox menu when the open list must match the canvas.
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">
@@ -244,6 +244,8 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
244
244
 
245
245
  `Tabs` is the strip for the sections of one page (`role="tablist"`, one `role="tab"` button per item; arrow keys, Home and End move and select), and `TabPanel` is each section's content, hidden while another tab is current. `sticky` (default `false`) keeps the strip at the top of the viewport, under `env(safe-area-inset-top)`, while a long panel scrolls under it, on the same layer as a sticky `AppHeader` (so a page uses one or the other at the top edge). The stuck strip is a full-bleed band of the page canvas, not a glass card: the canvas color at nine parts in ten over the glass blur, with the strip's hairline under it, solid canvas under Reduce Transparency and in forced colors. A sticky element only sticks within its parent: make the strip a direct child of the long page container that also holds the panels, not of a padded `Box` round the strip alone, and keep `overflow` visible on its ancestors. Put the page's side gutter on the title and the panels instead: each tab's inline padding is the md control's 16 px (spacing step 4), so the first label lines up with a `space-4` gutter when the strip runs edge to edge. A strip whose labels do not fit scrolls sideways, and while labels are scrolled out past an inline edge that edge draws the same inset shade as a wide `Table` (none in forced colors), so a phone shows there are more tabs.
246
246
 
247
+ `Nav` is a labelled `nav` of a workspace's destinations, links or buttons one token gap apart. A `Button` inside it is at least 44 × 44 px on a coarse pointer, whatever its `size`, so a compact row of destinations is still a touch target on a phone or a tablet.
248
+
247
249
  `SectionNav` is the navigation between the sections of one area, such as a portal's pages: a labelled `nav` (`aria-label` or `aria-labelledby`) of links from `items` (`{ id, label, href, current?, icon? }`), quieter than a workspace's `Nav`. `current` is `'page'`, `'location'` (a page inside that section, such as a detail page under its list) or a boolean, written as `aria-current`; the current item is in the text color over an accent underline, or, from `verticalFrom` (a breakpoint) up, where the items stack as a side list, a bar at its start. `icon` is a decorative glyph before the label. `onNavigate` takes a plain press after preventing the browser's navigation, for a client router; a press with a modifier key or the middle button keeps the browser's behavior, and without `onNavigate` the links carry no handler, so a server component can render the nav. A coarse pointer gets 44 px targets. Empty or duplicate ids, empty labels and an unknown `verticalFrom` throw a `RangeError`. Use `Tabs` for panels on one page.
248
250
 
249
251
  ```tsx
@@ -283,7 +285,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
283
285
 
284
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`.
285
287
 
286
- `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.
287
289
 
288
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.
289
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,8 +1,29 @@
1
+ /** The layout breakpoint: a phone below it, a wider screen from it. */
1
2
  export declare const breakpointScale: {
2
3
  readonly md: 48;
3
4
  };
4
5
  export type Breakpoint = keyof typeof breakpointScale;
5
6
  export declare const breakpoints: Breakpoint[];
6
- export type HideDirection = 'from' | 'below';
7
- export declare function hideClass(direction: HideDirection, breakpoint: Breakpoint): string;
8
- export declare function breakpointQuery(direction: HideDirection, breakpoint: Breakpoint): string;
7
+ /**
8
+ * The breakpoints `hideBelow` and `hideFrom` take: `md`, and `lg` (64rem)
9
+ * for content that fits only from a laptop up, such as a row of labelled
10
+ * destinations a tablet shows as glyphs. The layout props (`columnsBelow`,
11
+ * `stickyBelow`, `verticalFrom`, …) keep the one layout breakpoint.
12
+ */
13
+ export declare const visibilityBreakpointScale: {
14
+ readonly lg: 64;
15
+ readonly md: 48;
16
+ };
17
+ export type VisibilityBreakpoint = keyof typeof visibilityBreakpointScale;
18
+ export declare const visibilityBreakpoints: VisibilityBreakpoint[];
19
+ /** Which side of a breakpoint: `from` it and wider, or `below` it. */
20
+ export type BreakpointDirection = 'from' | 'below';
21
+ export declare const breakpointDirections: readonly BreakpointDirection[];
22
+ export declare function hideClass(direction: BreakpointDirection, breakpoint: VisibilityBreakpoint): string;
23
+ /**
24
+ * The media query `hideBelow` (`'below'`) or `hideFrom` (`'from'`) uses at a
25
+ * breakpoint, such as `not all and (min-width: 64rem)` below `lg`, for an app
26
+ * that must follow the same width in script (a tooltip that names a glyph
27
+ * only while its label is hidden).
28
+ */
29
+ export declare function breakpointQuery(direction: BreakpointDirection, breakpoint: VisibilityBreakpoint): string;
@@ -1,11 +1,37 @@
1
+ /** The layout breakpoint: a phone below it, a wider screen from it. */
1
2
  export const breakpointScale = {
2
3
  md: 48,
3
4
  };
4
5
  export const breakpoints = Object.keys(breakpointScale);
6
+ /**
7
+ * The breakpoints `hideBelow` and `hideFrom` take: `md`, and `lg` (64rem)
8
+ * for content that fits only from a laptop up, such as a row of labelled
9
+ * destinations a tablet shows as glyphs. The layout props (`columnsBelow`,
10
+ * `stickyBelow`, `verticalFrom`, …) keep the one layout breakpoint.
11
+ */
12
+ export const visibilityBreakpointScale = {
13
+ ...breakpointScale,
14
+ lg: 64,
15
+ };
16
+ export const visibilityBreakpoints = Object.keys(visibilityBreakpointScale);
17
+ export const breakpointDirections = [
18
+ 'from',
19
+ 'below',
20
+ ];
5
21
  export function hideClass(direction, breakpoint) {
6
22
  return `sw-hide-${direction}-${breakpoint}`;
7
23
  }
24
+ /**
25
+ * The media query `hideBelow` (`'below'`) or `hideFrom` (`'from'`) uses at a
26
+ * breakpoint, such as `not all and (min-width: 64rem)` below `lg`, for an app
27
+ * that must follow the same width in script (a tooltip that names a glyph
28
+ * only while its label is hidden).
29
+ */
8
30
  export function breakpointQuery(direction, breakpoint) {
9
- const minWidth = `(min-width: ${breakpointScale[breakpoint]}rem)`;
31
+ if (!breakpointDirections.includes(direction))
32
+ throw new RangeError(`direction must be one of ${breakpointDirections.join(', ')}`);
33
+ if (!visibilityBreakpoints.includes(breakpoint))
34
+ throw new RangeError(`breakpoint must be one of ${visibilityBreakpoints.join(', ')}`);
35
+ const minWidth = `(min-width: ${visibilityBreakpointScale[breakpoint]}rem)`;
10
36
  return direction === 'from' ? minWidth : `not all and ${minWidth}`;
11
37
  }
@@ -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',
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { zIndex } from './stacking.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
4
  const caption = typographyVariants.caption;
4
5
  const label = typographyVariants.label;
5
6
  export function cssChromeClasses() {
@@ -36,6 +37,18 @@ export function cssChromeClasses() {
36
37
  color: var(--sw-color-text);
37
38
  }
38
39
 
40
+ /*
41
+ * A button in a navigation is a destination: on a coarse pointer it grows to
42
+ * the 44 px touch target both ways, whatever its size, as a section link
43
+ * and a calendar button do. A label beside its glyph keeps its line.
44
+ */
45
+ @media ${coarsePointerQuery} {
46
+ .sw-nav .sw-button {
47
+ min-height: ${touchTarget};
48
+ min-width: ${touchTarget};
49
+ }
50
+ }
51
+
39
52
  .sw-field-xs :is(select, input, textarea) {
40
53
  font-size: ${caption.fontSize}px;
41
54
  letter-spacing: ${caption.letterSpacing}px;
@@ -44,10 +57,9 @@ export function cssChromeClasses() {
44
57
  padding-inline: var(--sw-control-xs-padding-inline);
45
58
  }
46
59
 
60
+ /* The chevron's layers place themselves from this inset (selectChevronLayers). */
47
61
  .sw-field-xs select {
48
- background-position:
49
- calc(100% - var(--sw-space-3)) calc(50% - 1px),
50
- calc(100% - calc(var(--sw-space-3) - var(--sw-space-1))) calc(50% - 1px);
62
+ --sw-select-chevron-inset: var(--sw-space-3);
51
63
  height: calc(var(--sw-control-xs-min-height) - 1px - 1px);
52
64
  min-height: 0;
53
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}
@@ -1,12 +1,11 @@
1
- import { breakpointQuery, breakpoints, hideClass, } from './breakpoints.js';
2
- const directions = ['from', 'below'];
1
+ import { breakpointDirections, breakpointQuery, hideClass, visibilityBreakpoints, } from './breakpoints.js';
3
2
  export function cssResponsiveClasses() {
4
- return breakpoints
5
- .flatMap((breakpoint) => directions.map((direction) => `@media ${breakpointQuery(direction, breakpoint)} {
3
+ return visibilityBreakpoints
4
+ .flatMap((breakpoint) => breakpointDirections.map((direction) => `@media ${breakpointQuery(direction, breakpoint)} {
6
5
  .${hideClass(direction, breakpoint)} { display: none; }
7
6
  }`))
8
7
  .join('\n\n');
9
8
  }
10
9
  export function responsiveClassCatalog() {
11
- return breakpoints.flatMap((breakpoint) => directions.map((direction) => hideClass(direction, breakpoint)));
10
+ return visibilityBreakpoints.flatMap((breakpoint) => breakpointDirections.map((direction) => hideClass(direction, breakpoint)));
12
11
  }
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';
@@ -37,7 +37,8 @@ export { Text, type TextAlign, type TextElement, type TextProps, } from './compo
37
37
  export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
38
38
  export { utilityClassCatalog } from './css/stylesheet.js';
39
39
  export type { VisibilityProps } from './visibility-classes.js';
40
- export type { Breakpoint } from './css/breakpoints.js';
40
+ export type { Breakpoint, BreakpointDirection, VisibilityBreakpoint, } from './css/breakpoints.js';
41
+ export { breakpointQuery } from './css/breakpoints.js';
41
42
  export { Separator } from './components/Separator.js';
42
43
  export type { SeparatorOrientation, SeparatorProps, } from './components/Separator.js';
43
44
  export { Grid } from './components/Grid.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';
@@ -32,6 +32,7 @@ export { Table, TableBody, TableCell, TableHeader, TableRow, } from './component
32
32
  export { Text, } from './components/Text.js';
33
33
  export { ThemeProvider, useTheme, } from './theme/ThemeProvider.js';
34
34
  export { utilityClassCatalog } from './css/stylesheet.js';
35
+ export { breakpointQuery } from './css/breakpoints.js';
35
36
  export { Separator } from './components/Separator.js';
36
37
  export { Grid } from './components/Grid.js';
37
38
  export { DenominationGrid } from './components/DenominationGrid.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);
@@ -2041,6 +2051,18 @@ body {
2041
2051
  color: var(--sw-color-text);
2042
2052
  }
2043
2053
 
2054
+ /*
2055
+ * A button in a navigation is a destination: on a coarse pointer it grows to
2056
+ * the 44 px touch target both ways, whatever its size, as a section link
2057
+ * and a calendar button do. A label beside its glyph keeps its line.
2058
+ */
2059
+ @media (pointer: coarse) {
2060
+ .sw-nav .sw-button {
2061
+ min-height: var(--sw-control-md-min-height);
2062
+ min-width: var(--sw-control-md-min-height);
2063
+ }
2064
+ }
2065
+
2044
2066
  .sw-field-xs :is(select, input, textarea) {
2045
2067
  font-size: 13px;
2046
2068
  letter-spacing: -0.08px;
@@ -2049,10 +2071,9 @@ body {
2049
2071
  padding-inline: var(--sw-control-xs-padding-inline);
2050
2072
  }
2051
2073
 
2074
+ /* The chevron's layers place themselves from this inset (selectChevronLayers). */
2052
2075
  .sw-field-xs select {
2053
- background-position:
2054
- calc(100% - var(--sw-space-3)) calc(50% - 1px),
2055
- calc(100% - calc(var(--sw-space-3) - var(--sw-space-1))) calc(50% - 1px);
2076
+ --sw-select-chevron-inset: var(--sw-space-3);
2056
2077
  height: calc(var(--sw-control-xs-min-height) - 1px - 1px);
2057
2078
  min-height: 0;
2058
2079
  padding-inline-end: calc(
@@ -3599,17 +3620,6 @@ body {
3599
3620
  border-color: var(--sw-color-danger);
3600
3621
  }
3601
3622
 
3602
- /*
3603
- * A changed value: the accent border, a hairline thicker through an inset
3604
- * shadow so nothing moves, on the control or on an adorned frame (whose
3605
- * input draws no border of its own).
3606
- */
3607
- [data-theme] .sw-field-changed > :is(input, select, textarea),
3608
- .sw-field-changed .sw-field-adorned {
3609
- border-color: var(--sw-color-accent);
3610
- box-shadow: inset 0 0 0 1px var(--sw-color-accent);
3611
- }
3612
-
3613
3623
  .sw-field-adorned {
3614
3624
  background-color: var(--sw-glass-fill);
3615
3625
  border: 1px solid var(--sw-color-border);
@@ -3673,11 +3683,47 @@ body {
3673
3683
 
3674
3684
  [data-theme] .sw-field-adorned > input:disabled { background: transparent; }
3675
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
+
3676
3715
  @media (forced-colors: active) {
3677
- [data-theme] .sw-field-invalid :is(input, select, textarea),
3678
- .sw-field-invalid .sw-field-adorned { border-color: Mark; }
3679
3716
  [data-theme] .sw-field-changed > :is(input, select, textarea),
3680
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; }
3681
3727
  .sw-field-prefix, .sw-field-suffix { color: CanvasText; }
3682
3728
  .sw-field-adorned:has(> input:disabled) {
3683
3729
  border-color: GrayText;
@@ -3873,6 +3919,14 @@ body {
3873
3919
  .sw-hide-below-md { display: none; }
3874
3920
  }
3875
3921
 
3922
+ @media (min-width: 64rem) {
3923
+ .sw-hide-from-lg { display: none; }
3924
+ }
3925
+
3926
+ @media not all and (min-width: 64rem) {
3927
+ .sw-hide-below-lg { display: none; }
3928
+ }
3929
+
3876
3930
  .sw-border { border: 1px solid var(--sw-color-border); }
3877
3931
  .sw-border-dashed { border: 1px dashed var(--sw-color-border); }
3878
3932
 
@@ -1,6 +1,6 @@
1
- import { type Breakpoint } from './css/breakpoints.js';
1
+ import { type VisibilityBreakpoint } from './css/breakpoints.js';
2
2
  export type VisibilityProps = {
3
- hideBelow?: Breakpoint;
4
- hideFrom?: Breakpoint;
3
+ hideBelow?: VisibilityBreakpoint;
4
+ hideFrom?: VisibilityBreakpoint;
5
5
  };
6
6
  export declare function visibilityClassNames({ hideBelow, hideFrom, }: VisibilityProps): string[];
@@ -1,5 +1,11 @@
1
- import { hideClass } from './css/breakpoints.js';
1
+ import { hideClass, visibilityBreakpoints, } from './css/breakpoints.js';
2
+ function assertBreakpoint(prop, breakpoint) {
3
+ if (breakpoint && !visibilityBreakpoints.includes(breakpoint))
4
+ throw new RangeError(`${prop} must be one of ${visibilityBreakpoints.join(', ')}`);
5
+ }
2
6
  export function visibilityClassNames({ hideBelow, hideFrom, }) {
7
+ assertBreakpoint('hideBelow', hideBelow);
8
+ assertBreakpoint('hideFrom', hideFrom);
3
9
  return [
4
10
  ...(hideBelow ? [hideClass('below', hideBelow)] : []),
5
11
  ...(hideFrom ? [hideClass('from', hideFrom)] : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.22.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",