@scalewing/react 1.23.0 → 1.25.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 +22 -0
- package/README.md +18 -3
- package/dist/components/BarChart.d.ts +11 -0
- package/dist/components/BarChart.js +9 -1
- package/dist/components/Field.d.ts +4 -1
- package/dist/components/InfoTip.d.ts +26 -0
- package/dist/components/InfoTip.js +19 -0
- package/dist/components/Tooltip.d.ts +5 -0
- package/dist/components/Tooltip.js +8 -59
- package/dist/components/tooltip/TooltipAnchor.d.ts +19 -0
- package/dist/components/tooltip/TooltipAnchor.js +27 -0
- package/dist/components/tooltip/use-tooltip-open.d.ts +33 -0
- package/dist/components/tooltip/use-tooltip-open.js +90 -0
- package/dist/css/css-calendar-button.js +2 -22
- package/dist/css/css-chart.d.ts +7 -0
- package/dist/css/css-chart.js +32 -1
- package/dist/css/css-chrome.js +2 -3
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-document.d.ts +14 -0
- package/dist/css/css-document.js +21 -7
- package/dist/css/css-field-changed.d.ts +13 -0
- package/dist/css/css-field-changed.js +64 -0
- package/dist/css/css-field.js +3 -13
- package/dist/css/css-icon-button.d.ts +1 -0
- package/dist/css/css-icon-button.js +26 -0
- package/dist/css/css-info-tip.d.ts +2 -0
- package/dist/css/css-info-tip.js +8 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/styles.css +73 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.25.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 34131f3: `InfoTip` and `InfoTipProps`: a toggletip, a ghost icon-only `Button` whose only job is to show its tip (`docs/requests/teisoro-tooltip.md`, 2026-10-09 follow-up, Teisoro F-006-S11). Since 1.22.0 a tap on a `Button` presses it and never opens its `Tooltip`, so an ⓘ beside a figure could not show its help on a touch screen.
|
|
8
|
+
|
|
9
|
+
- `label` (the button's name), `content` (the tip, plain text), `children` (the decorative glyph), `size` (a Button size; `md` by default, and 44 px on a coarse pointer at every size).
|
|
10
|
+
- Hover and a visible focus show the tip as `Tooltip` does. A press (a tap, a click, Enter or Space) shows it and keeps it shown when the pointer leaves; the next press, Escape, blur or a press outside hides it.
|
|
11
|
+
- The tip is the button's description (`aria-describedby`). A press also puts it in a visually hidden polite live region (`role="status"`), so a screen reader user who presses the button hears it. Every InfoTip renders that `role="status"` element, empty until a press, and while a press holds the tip open its text is in the page twice (the bubble and the region): a test that finds a `status` or the tip's text should scope its query.
|
|
12
|
+
- `type="button"`: it never submits a form. An empty `label` or `content` throws a `RangeError`.
|
|
13
|
+
- New generated class `sw-info-tip` (the square icon button, shared with `sw-calendar-button`, whose rules are equivalent: same declarations and specificity, one block reordered).
|
|
14
|
+
|
|
15
|
+
`Tooltip`'s behavior is unchanged for every other trigger: a tap on a control still does what the control does and leaves its tooltip closed. No new tokens or dependencies.
|
|
16
|
+
|
|
17
|
+
## 1.24.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
`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.
|
|
24
|
+
|
|
3
25
|
## 1.23.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
Spinner,
|
|
21
21
|
Progress,
|
|
22
22
|
Tooltip,
|
|
23
|
+
InfoTip,
|
|
23
24
|
Separator,
|
|
24
25
|
DenominationGrid,
|
|
25
26
|
FilterChips,
|
|
@@ -53,7 +54,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
53
54
|
|
|
54
55
|
`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
56
|
|
|
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).
|
|
57
|
+
`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
58
|
|
|
58
59
|
```tsx
|
|
59
60
|
<Field invalid={countIsZero} label="$1 bills">
|
|
@@ -285,13 +286,27 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
285
286
|
|
|
286
287
|
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
288
|
|
|
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.
|
|
289
|
+
`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
290
|
|
|
290
291
|
`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
292
|
|
|
292
293
|
`Progress` shows a known value between zero and a positive maximum. It uses native progressbar semantics and displays the value and maximum beside the localized label. Invalid bounds throw instead of silently clamping. Optional `tone` is `accent`, `success`, or `danger`. `showCount={false}` hides the visible `value / max` count when the page shows its own count caption, so the count appears once; the progress bar still exposes its value and maximum.
|
|
293
294
|
|
|
294
|
-
`Tooltip` adds supplementary plain-text help to one labelled, focusable trigger, or names an icon-only one (`relationship="label"`, below). Supply localized `content` and an existing trigger element; by default it has its own accessible name. It opens on hover or on a visible focus (`:focus-visible`: the keyboard, not the focus a click or a tap gives), closes on pointer leave, blur, Escape, or outside touch, and a tap
|
|
295
|
+
`Tooltip` adds supplementary plain-text help to one labelled, focusable trigger, or names an icon-only one (`relationship="label"`, below). Supply localized `content` and an existing trigger element; by default it has its own accessible name. It opens on hover or on a visible focus (`:focus-visible`: the keyboard, not the focus a click or a tap gives), closes on pointer leave, blur, Escape, or outside touch, and a tap on a control (a button, a link, a form control or its label, an editable region, an element with a widget role, or anything inside one) does what the control does and leaves the tooltip closed, so on a touch screen a `Tooltip` on a `Button` is never shown. For an icon-only hint whose button does nothing but show it, such as an ⓘ beside a figure, use `InfoTip` (below), which a tap, a click, Enter or Space opens; do not make a non-control focusable to get a tap. A tap still toggles a `Tooltip` whose trigger does nothing else on a tap, such as an existing focusable badge. The bubble is placed beside its trigger on the top layer while it shows and stays a `space-2` inset inside the screen. Keep required instructions visible outside the tooltip. `disabled` turns the tooltip off, not its trigger: the trigger stays mounted, enabled and focusable, and there is no tooltip, no `aria-describedby` from it, and Escape is left to the page. Use it when what the tooltip says is visible at some widths, such as text shown only from `md` up, instead of rendering the trigger with and without a `Tooltip`, which mounts a new trigger and drops its focus. Focus, hover and touch are still followed, so a trigger that still has focus, the pointer or an open touch toggle shows the tooltip as soon as it is enabled again (Escape pressed while disabled reaches the page and does not stop that). `disabled` must be the same in the server render and the first client render, or hydration mismatches; read a breakpoint through a hydration-safe store, such as `useSyncExternalStore` with a server snapshot. `relationship="label"` makes the tooltip its trigger's name instead of its description, for an icon-only control whose tooltip says what it is: the trigger gets `aria-labelledby` (after its own, if any) and no `aria-describedby` from the tooltip, so the text is read once. The tooltip stays in the page, hidden, so it names the trigger while it is not shown, and it wins over an `aria-label`. While `disabled` there is no tooltip to name the trigger, so give the trigger its own name for that state: visible text, such as a label shown only from `md` up, or an `aria-label`. Do not use `relationship="label"` while the trigger shows visible text that differs from `content`: the tooltip would replace that visible label as the name (WCAG 2.5.3, label in name). The default, `relationship="description"`, is the supplementary help above. `relationship` must also be the same in the server render and the first client render.
|
|
296
|
+
|
|
297
|
+
`InfoTip` is a toggletip: a ghost icon-only `Button` whose only job is to show its tip, built on `Tooltip`'s bubble and placement. `label` is the button's accessible name and `content` its tip, plain text as `Tooltip`'s, both localized by the consumer; `children` is the decorative glyph (a Lucide `Info` with `aria-hidden`); `size` is a Button size, and the default `md` and every size on a coarse pointer are a square 44 px target. Hover and a visible focus show the tip as `Tooltip` does. A press of the button (a tap, a click, Enter or Space) shows it and keeps it shown when the pointer leaves, and the next press hides it; Escape (taken only while the tip shows), blur and a press outside also hide it. The tip is the button's description (`aria-describedby`), read once on focus; a press also puts it in a visually hidden polite live region (`role="status"`, empty until a press opens the tip and emptied when it closes), so a screen reader user who presses the button hears it even with descriptions or hints turned off. The button is `type="button"` and never submits a form, and the ref reaches it. An empty `label` or `content` throws a `RangeError`. There is no `disabled`: where the tip is shown as text instead, render no `InfoTip`.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
<Inline gap={1}>
|
|
301
|
+
<Text>Expected waders: 128</Text>
|
|
302
|
+
<InfoTip
|
|
303
|
+
content="Counted at the last high tide, before the hides opened"
|
|
304
|
+
label="About the expected waders"
|
|
305
|
+
>
|
|
306
|
+
<Info aria-hidden size={16} />
|
|
307
|
+
</InfoTip>
|
|
308
|
+
</Inline>
|
|
309
|
+
```
|
|
295
310
|
|
|
296
311
|
`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.
|
|
297
312
|
|
|
@@ -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:
|
|
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
|
|
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.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ButtonSize } from './Button.js';
|
|
3
|
+
export type InfoTipProps = {
|
|
4
|
+
/**
|
|
5
|
+
* The button's accessible name, such as "About the expected balance".
|
|
6
|
+
* Localized by the consumer.
|
|
7
|
+
*/
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* The tip: plain text, as `Tooltip`'s `content`. It is the button's
|
|
11
|
+
* description, and a press also says it once in a polite live region.
|
|
12
|
+
*/
|
|
13
|
+
content: string;
|
|
14
|
+
/** The glyph, such as a Lucide `Info`; decorative, the label names it. */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/** A Button size; md (default) is 44 px, and a coarse pointer always gets 44 px. */
|
|
17
|
+
size?: ButtonSize;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A toggletip: a ghost icon-only button whose only job is to show its tip.
|
|
21
|
+
* Hover and a visible focus show it as `Tooltip` does; a press (a tap, a
|
|
22
|
+
* click, Enter or Space) shows it and keeps it shown until the next press,
|
|
23
|
+
* Escape, blur or a press outside. The button never submits a form. The ref
|
|
24
|
+
* reaches the `<button>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const InfoTip: import("react").ForwardRefExoticComponent<InfoTipProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { Button } from './Button.js';
|
|
5
|
+
import { TooltipAnchor } from './tooltip/TooltipAnchor.js';
|
|
6
|
+
/** The press is the anchor's: it toggles the tip on the button's click. */
|
|
7
|
+
function pressShowsTheTip() { }
|
|
8
|
+
/**
|
|
9
|
+
* A toggletip: a ghost icon-only button whose only job is to show its tip.
|
|
10
|
+
* Hover and a visible focus show it as `Tooltip` does; a press (a tap, a
|
|
11
|
+
* click, Enter or Space) shows it and keeps it shown until the next press,
|
|
12
|
+
* Escape, blur or a press outside. The button never submits a form. The ref
|
|
13
|
+
* reaches the `<button>`.
|
|
14
|
+
*/
|
|
15
|
+
export const InfoTip = forwardRef(function InfoTip({ children, content, label, size = 'md' }, ref) {
|
|
16
|
+
if (typeof label !== 'string' || label.trim() === '')
|
|
17
|
+
throw new RangeError('label must be non-empty text');
|
|
18
|
+
return (_jsx(TooltipAnchor, { content: content, pressToggles: true, trigger: _jsx(Button, { "aria-label": label, className: "sw-info-tip", onPress: pressShowsTheTip, ref: ref, size: size, type: "button", variant: "ghost", children: children }) }));
|
|
19
|
+
});
|
|
@@ -28,4 +28,9 @@ export type TooltipProps = {
|
|
|
28
28
|
relationship?: TooltipRelationship;
|
|
29
29
|
trigger: ReactElement<TooltipTriggerAria>;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Supplementary help for a trigger that has its own name, or the name of an
|
|
33
|
+
* icon-only trigger (`relationship="label"`). For a glyph whose only job is
|
|
34
|
+
* to show its help, use `InfoTip`, which a press opens.
|
|
35
|
+
*/
|
|
31
36
|
export declare function Tooltip({ content, disabled, relationship, trigger, }: TooltipProps): import("react").JSX.Element;
|
|
@@ -1,62 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { TooltipAnchor } from './tooltip/TooltipAnchor.js';
|
|
4
|
+
/**
|
|
5
|
+
* Supplementary help for a trigger that has its own name, or the name of an
|
|
6
|
+
* icon-only trigger (`relationship="label"`). For a glyph whose only job is
|
|
7
|
+
* to show its help, use `InfoTip`, which a press opens.
|
|
8
|
+
*/
|
|
7
9
|
export function Tooltip({ content, disabled = false, relationship = 'description', trigger, }) {
|
|
8
|
-
|
|
9
|
-
throw new RangeError('content must not be empty');
|
|
10
|
-
// `open` follows focus, hover and touch even while disabled; `shown` is
|
|
11
|
-
// what the page gets.
|
|
12
|
-
const [open, setOpen] = useState(false);
|
|
13
|
-
const shown = open && !disabled;
|
|
14
|
-
const id = useId();
|
|
15
|
-
const wrapperRef = useRef(null);
|
|
16
|
-
const tooltipRef = useRef(null);
|
|
17
|
-
useTooltipPlacement(shown, tooltipRef, wrapperRef);
|
|
18
|
-
const triggerAria = tooltipTriggerAria(trigger.props, disabled ? undefined : id, relationship);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!open)
|
|
21
|
-
return;
|
|
22
|
-
function dismissOutside(event) {
|
|
23
|
-
if (event.target instanceof Node &&
|
|
24
|
-
!wrapperRef.current?.contains(event.target))
|
|
25
|
-
setOpen(false);
|
|
26
|
-
}
|
|
27
|
-
document.addEventListener('pointerdown', dismissOutside);
|
|
28
|
-
return () => document.removeEventListener('pointerdown', dismissOutside);
|
|
29
|
-
}, [open]);
|
|
30
|
-
return (_jsxs("span", { className: "sw-tooltip-anchor", onBlurCapture: (event) => {
|
|
31
|
-
if (!event.currentTarget.contains(event.relatedTarget))
|
|
32
|
-
setOpen(false);
|
|
33
|
-
}, onFocusCapture: (event) => {
|
|
34
|
-
// A click or a tap focuses the trigger too; only a visible focus
|
|
35
|
-
// (the keyboard) opens the tooltip, so a clicked control does not
|
|
36
|
-
// keep its help shown until the focus moves.
|
|
37
|
-
if (focusIsVisible(event.target))
|
|
38
|
-
setOpen(true);
|
|
39
|
-
}, onKeyDownCapture: (event) => {
|
|
40
|
-
// Escape hides a visible tooltip and nothing else, so a surrounding
|
|
41
|
-
// Dialog does not also close. A hidden tooltip leaves Escape alone.
|
|
42
|
-
if (event.key !== 'Escape' || !shown)
|
|
43
|
-
return;
|
|
44
|
-
event.preventDefault();
|
|
45
|
-
setOpen(false);
|
|
46
|
-
}, onPointerEnter: (event) => {
|
|
47
|
-
if (event.pointerType === 'mouse' || event.pointerType === 'pen')
|
|
48
|
-
setOpen(true);
|
|
49
|
-
}, onPointerLeave: (event) => {
|
|
50
|
-
if (event.pointerType === 'mouse' || event.pointerType === 'pen')
|
|
51
|
-
setOpen(false);
|
|
52
|
-
}, onPointerDownCapture: (event) => {
|
|
53
|
-
if (event.pointerType !== 'touch')
|
|
54
|
-
return;
|
|
55
|
-
// A tap on a control does what the control does; only a trigger
|
|
56
|
-
// that does nothing else on a tap toggles its help.
|
|
57
|
-
if (tapActs(event.target, event.currentTarget))
|
|
58
|
-
setOpen(false);
|
|
59
|
-
else
|
|
60
|
-
setOpen((current) => !current);
|
|
61
|
-
}, ref: wrapperRef, children: [cloneElement(trigger, triggerAria), disabled ? null : (_jsx("span", { className: "sw-tooltip", hidden: !shown, id: id, ref: tooltipRef, role: "tooltip", children: content }))] }));
|
|
10
|
+
return (_jsx(TooltipAnchor, { content: content, disabled: disabled, relationship: relationship, trigger: trigger }));
|
|
62
11
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type TooltipRelationship, type TooltipTriggerAria } from '../tooltip-trigger-aria.js';
|
|
3
|
+
export type TooltipAnchorProps = {
|
|
4
|
+
content: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Internal, for `InfoTip`: a press of the trigger toggles the tooltip, and
|
|
8
|
+
* the tooltip a press opens is also said once in a polite live region.
|
|
9
|
+
*/
|
|
10
|
+
pressToggles?: boolean;
|
|
11
|
+
relationship?: TooltipRelationship;
|
|
12
|
+
trigger: ReactElement<TooltipTriggerAria>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The anchor `span` around a trigger and its tooltip: the bubble, its
|
|
16
|
+
* reference from the trigger, and what opens and closes it. `Tooltip` and
|
|
17
|
+
* `InfoTip` are this anchor; only `InfoTip` turns `pressToggles` on.
|
|
18
|
+
*/
|
|
19
|
+
export declare function TooltipAnchor({ content, disabled, pressToggles, relationship, trigger, }: TooltipAnchorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, useId, useRef } from 'react';
|
|
4
|
+
import { tooltipTriggerAria, } from '../tooltip-trigger-aria.js';
|
|
5
|
+
import { useTooltipOpen } from './use-tooltip-open.js';
|
|
6
|
+
import { useTooltipPlacement } from './use-tooltip-placement.js';
|
|
7
|
+
/**
|
|
8
|
+
* The anchor `span` around a trigger and its tooltip: the bubble, its
|
|
9
|
+
* reference from the trigger, and what opens and closes it. `Tooltip` and
|
|
10
|
+
* `InfoTip` are this anchor; only `InfoTip` turns `pressToggles` on.
|
|
11
|
+
*/
|
|
12
|
+
export function TooltipAnchor({ content, disabled = false, pressToggles = false, relationship = 'description', trigger, }) {
|
|
13
|
+
if (!content.trim())
|
|
14
|
+
throw new RangeError('content must not be empty');
|
|
15
|
+
const id = useId();
|
|
16
|
+
const wrapperRef = useRef(null);
|
|
17
|
+
const tooltipRef = useRef(null);
|
|
18
|
+
const { handlers, pressed, shown } = useTooltipOpen({
|
|
19
|
+
disabled,
|
|
20
|
+
pressToggles,
|
|
21
|
+
tooltipRef,
|
|
22
|
+
wrapperRef,
|
|
23
|
+
});
|
|
24
|
+
useTooltipPlacement(shown, tooltipRef, wrapperRef);
|
|
25
|
+
const triggerAria = tooltipTriggerAria(trigger.props, disabled ? undefined : id, relationship);
|
|
26
|
+
return (_jsxs("span", { className: "sw-tooltip-anchor", ref: wrapperRef, ...handlers, children: [cloneElement(trigger, triggerAria), disabled ? null : (_jsx("span", { className: "sw-tooltip", hidden: !shown, id: id, ref: tooltipRef, role: "tooltip", children: content })), pressToggles ? (_jsx("span", { className: "sw-sr-only", role: "status", children: pressed && shown ? content : null })) : null] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type FocusEvent, type KeyboardEvent, type MouseEvent, type PointerEvent as ReactPointerEvent, type RefObject } from 'react';
|
|
2
|
+
export type TooltipOpenOptions = {
|
|
3
|
+
/** Whether the tooltip is enabled: only a shown tooltip takes Escape. */
|
|
4
|
+
disabled: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The toggletip: a press of the trigger (a tap, a click, Enter or Space)
|
|
7
|
+
* shows the tooltip and keeps it shown, and the next press hides it.
|
|
8
|
+
* Without it, a tap toggles only a trigger that does nothing else.
|
|
9
|
+
*/
|
|
10
|
+
pressToggles: boolean;
|
|
11
|
+
tooltipRef: RefObject<HTMLElement | null>;
|
|
12
|
+
wrapperRef: RefObject<HTMLElement | null>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Whether a tooltip is open, and the anchor's handlers that open and close
|
|
16
|
+
* it. `open` follows focus, hover, touch and presses even while disabled;
|
|
17
|
+
* `shown` is what the page gets. `pressed` is a tooltip a press opened: it
|
|
18
|
+
* stays open when the pointer leaves, until the next press, Escape, blur or
|
|
19
|
+
* a press outside the anchor.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useTooltipOpen({ disabled, pressToggles, tooltipRef, wrapperRef, }: TooltipOpenOptions): {
|
|
22
|
+
handlers: {
|
|
23
|
+
onBlurCapture(event: FocusEvent<HTMLElement>): void;
|
|
24
|
+
onFocusCapture(event: FocusEvent<HTMLElement>): void;
|
|
25
|
+
onKeyDownCapture(event: KeyboardEvent<HTMLElement>): void;
|
|
26
|
+
onPointerEnter(event: ReactPointerEvent<HTMLElement>): void;
|
|
27
|
+
onPointerLeave(event: ReactPointerEvent<HTMLElement>): void;
|
|
28
|
+
onPointerDownCapture(event: ReactPointerEvent<HTMLElement>): void;
|
|
29
|
+
onClick: ((event: MouseEvent<HTMLElement>) => void) | undefined;
|
|
30
|
+
};
|
|
31
|
+
pressed: boolean;
|
|
32
|
+
shown: boolean;
|
|
33
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState, } from 'react';
|
|
3
|
+
import { focusIsVisible, tapActs } from './tooltip-open.js';
|
|
4
|
+
function isHover(event) {
|
|
5
|
+
return event.pointerType === 'mouse' || event.pointerType === 'pen';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether a tooltip is open, and the anchor's handlers that open and close
|
|
9
|
+
* it. `open` follows focus, hover, touch and presses even while disabled;
|
|
10
|
+
* `shown` is what the page gets. `pressed` is a tooltip a press opened: it
|
|
11
|
+
* stays open when the pointer leaves, until the next press, Escape, blur or
|
|
12
|
+
* a press outside the anchor.
|
|
13
|
+
*/
|
|
14
|
+
export function useTooltipOpen({ disabled, pressToggles, tooltipRef, wrapperRef, }) {
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const [pressed, setPressed] = useState(false);
|
|
17
|
+
const shown = open && !disabled;
|
|
18
|
+
function close() {
|
|
19
|
+
setOpen(false);
|
|
20
|
+
setPressed(false);
|
|
21
|
+
}
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open)
|
|
24
|
+
return;
|
|
25
|
+
function dismissOutside(event) {
|
|
26
|
+
if (event.target instanceof Node &&
|
|
27
|
+
!wrapperRef.current?.contains(event.target)) {
|
|
28
|
+
setOpen(false);
|
|
29
|
+
setPressed(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
document.addEventListener('pointerdown', dismissOutside);
|
|
33
|
+
return () => document.removeEventListener('pointerdown', dismissOutside);
|
|
34
|
+
}, [open, wrapperRef]);
|
|
35
|
+
const handlers = {
|
|
36
|
+
onBlurCapture(event) {
|
|
37
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
38
|
+
close();
|
|
39
|
+
},
|
|
40
|
+
onFocusCapture(event) {
|
|
41
|
+
// A click or a tap focuses the trigger too; only a visible focus
|
|
42
|
+
// (the keyboard) opens the tooltip, so a clicked control does not
|
|
43
|
+
// keep its help shown until the focus moves.
|
|
44
|
+
if (focusIsVisible(event.target))
|
|
45
|
+
setOpen(true);
|
|
46
|
+
},
|
|
47
|
+
onKeyDownCapture(event) {
|
|
48
|
+
// Escape hides a visible tooltip and nothing else, so a surrounding
|
|
49
|
+
// Dialog does not also close. A hidden tooltip leaves Escape alone.
|
|
50
|
+
if (event.key !== 'Escape' || !shown)
|
|
51
|
+
return;
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
close();
|
|
54
|
+
},
|
|
55
|
+
onPointerEnter(event) {
|
|
56
|
+
if (isHover(event))
|
|
57
|
+
setOpen(true);
|
|
58
|
+
},
|
|
59
|
+
onPointerLeave(event) {
|
|
60
|
+
if (isHover(event) && !pressed)
|
|
61
|
+
setOpen(false);
|
|
62
|
+
},
|
|
63
|
+
onPointerDownCapture(event) {
|
|
64
|
+
// A toggletip's tap is a press, which its click handles.
|
|
65
|
+
if (event.pointerType !== 'touch' || pressToggles)
|
|
66
|
+
return;
|
|
67
|
+
// A tap on a control does what the control does; only a trigger
|
|
68
|
+
// that does nothing else on a tap toggles its help.
|
|
69
|
+
if (tapActs(event.target, event.currentTarget))
|
|
70
|
+
setOpen(false);
|
|
71
|
+
else
|
|
72
|
+
setOpen((current) => !current);
|
|
73
|
+
},
|
|
74
|
+
onClick: pressToggles
|
|
75
|
+
? (event) => {
|
|
76
|
+
// A press inside the bubble is not a press of the trigger.
|
|
77
|
+
if (event.target instanceof Node &&
|
|
78
|
+
tooltipRef.current?.contains(event.target))
|
|
79
|
+
return;
|
|
80
|
+
if (pressed)
|
|
81
|
+
close();
|
|
82
|
+
else {
|
|
83
|
+
setOpen(true);
|
|
84
|
+
setPressed(true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
};
|
|
89
|
+
return { handlers, pressed, shown };
|
|
90
|
+
}
|
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
3
|
-
/* Square at every Button size: as wide as the size's control height. */
|
|
4
|
-
function squareRules() {
|
|
5
|
-
return buttonSizes
|
|
6
|
-
.map((size) => `.sw-button-${size}.sw-calendar-button { min-width: var(--sw-control-${size}-min-height); }`)
|
|
7
|
-
.join('\n');
|
|
8
|
-
}
|
|
1
|
+
import { iconButtonRules } from './css-icon-button.js';
|
|
9
2
|
export function cssCalendarButtonClasses() {
|
|
10
|
-
return
|
|
11
|
-
flex: none;
|
|
12
|
-
padding-inline: 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
${squareRules()}
|
|
3
|
+
return `${iconButtonRules('sw-calendar-button')}
|
|
16
4
|
|
|
17
5
|
.sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
|
|
18
6
|
height: var(--sw-space-4);
|
|
19
7
|
width: var(--sw-space-4);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* A coarse pointer gets the full touch target at the smaller sizes too. */
|
|
23
|
-
@media ${coarsePointerQuery} {
|
|
24
|
-
.sw-button.sw-calendar-button {
|
|
25
|
-
min-height: ${touchTarget};
|
|
26
|
-
min-width: ${touchTarget};
|
|
27
|
-
}
|
|
28
8
|
}`;
|
|
29
9
|
}
|
|
30
10
|
export function calendarButtonClassCatalog() {
|
package/dist/css/css-chart.d.ts
CHANGED
|
@@ -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[];
|
package/dist/css/css-chart.js
CHANGED
|
@@ -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',
|
package/dist/css/css-chrome.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
@@ -20,6 +20,7 @@ import { cssTextClasses } from './css-text.js';
|
|
|
20
20
|
import { cssSpinnerClasses } from './css-spinner.js';
|
|
21
21
|
import { cssProgressClasses } from './css-progress.js';
|
|
22
22
|
import { cssTooltipClasses } from './css-tooltip.js';
|
|
23
|
+
import { cssInfoTipClasses } from './css-info-tip.js';
|
|
23
24
|
import { cssSeparatorClasses } from './css-separator.js';
|
|
24
25
|
import { cssStatTileClasses } from './css-stat-tile.js';
|
|
25
26
|
import { cssTabsClasses } from './css-tabs.js';
|
|
@@ -144,6 +145,8 @@ ${cssProgressClasses()}
|
|
|
144
145
|
|
|
145
146
|
${cssTooltipClasses()}
|
|
146
147
|
|
|
148
|
+
${cssInfoTipClasses()}
|
|
149
|
+
|
|
147
150
|
${cssSeparatorClasses()}
|
|
148
151
|
|
|
149
152
|
${cssStatTileClasses()}
|
|
@@ -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;
|
package/dist/css/css-document.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
const body = typographyVariants.body;
|
|
3
|
-
|
|
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
|
-
|
|
109
|
-
linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%);
|
|
125
|
+
${selectChevronLayers.image};
|
|
110
126
|
background-position:
|
|
111
|
-
|
|
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:
|
|
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
|
+
}
|
package/dist/css/css-field.js
CHANGED
|
@@ -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}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function iconButtonRules(className: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buttonSizes } from '@scalewing/tokens';
|
|
2
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
3
|
+
/*
|
|
4
|
+
* An icon-only Button drawn by `className` (CalendarButton, InfoTip): square
|
|
5
|
+
* at every Button size, as wide as the size's control height, and the full
|
|
6
|
+
* touch target on a coarse pointer at the smaller sizes too. The coarse
|
|
7
|
+
* pointer's rule follows the squares, which it outranks only by order.
|
|
8
|
+
*/
|
|
9
|
+
export function iconButtonRules(className) {
|
|
10
|
+
const squares = buttonSizes
|
|
11
|
+
.map((size) => `.sw-button-${size}.${className} { min-width: var(--sw-control-${size}-min-height); }`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
return `.sw-button.${className} {
|
|
14
|
+
flex: none;
|
|
15
|
+
padding-inline: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
${squares}
|
|
19
|
+
|
|
20
|
+
@media ${coarsePointerQuery} {
|
|
21
|
+
.sw-button.${className} {
|
|
22
|
+
min-height: ${touchTarget};
|
|
23
|
+
min-width: ${touchTarget};
|
|
24
|
+
}
|
|
25
|
+
}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { iconButtonRules } from './css-icon-button.js';
|
|
2
|
+
/* InfoTip's glyph button; its bubble is the generated `.sw-tooltip`. */
|
|
3
|
+
export function cssInfoTipClasses() {
|
|
4
|
+
return iconButtonRules('sw-info-tip');
|
|
5
|
+
}
|
|
6
|
+
export function infoTipClassCatalog() {
|
|
7
|
+
return ['sw-info-tip'];
|
|
8
|
+
}
|
package/dist/css/stylesheet.js
CHANGED
|
@@ -10,6 +10,7 @@ import { radioGroupClassCatalog } from './css-radio-group.js';
|
|
|
10
10
|
import { spinnerClassCatalog } from './css-spinner.js';
|
|
11
11
|
import { progressClassCatalog } from './css-progress.js';
|
|
12
12
|
import { tooltipClassCatalog } from './css-tooltip.js';
|
|
13
|
+
import { infoTipClassCatalog } from './css-info-tip.js';
|
|
13
14
|
import { separatorClassCatalog } from './css-separator.js';
|
|
14
15
|
import { statTileClassCatalog } from './css-stat-tile.js';
|
|
15
16
|
import { tabsClassCatalog } from './css-tabs.js';
|
|
@@ -72,6 +73,7 @@ export function utilityClassCatalog() {
|
|
|
72
73
|
...spinnerClassCatalog(),
|
|
73
74
|
...progressClassCatalog(),
|
|
74
75
|
...tooltipClassCatalog(),
|
|
76
|
+
...infoTipClassCatalog(),
|
|
75
77
|
...separatorClassCatalog(),
|
|
76
78
|
...statTileClassCatalog(),
|
|
77
79
|
...tabsClassCatalog(),
|
package/dist/index.d.ts
CHANGED
|
@@ -9,10 +9,11 @@ export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
|
|
|
9
9
|
export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
|
|
10
10
|
export { Progress, type ProgressProps, type ProgressTone, } from './components/Progress.js';
|
|
11
11
|
export { Tooltip, type TooltipProps, type TooltipRelationship, } from './components/Tooltip.js';
|
|
12
|
+
export { InfoTip, type InfoTipProps } from './components/InfoTip.js';
|
|
12
13
|
export { RadioGroup, type RadioGroupOption, type RadioGroupProps, } from './components/RadioGroup.js';
|
|
13
14
|
export { AppHeader, type AppHeaderProps } from './components/AppHeader.js';
|
|
14
15
|
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';
|
|
16
|
+
export { BarChart, type BarChartItem, type BarChartProps, type BarChartTone, barChartTones, } from './components/BarChart.js';
|
|
16
17
|
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, } from './components/Button.js';
|
|
17
18
|
export { Box, type BoxBorder, type BoxElement, type BoxProps, } from './components/Box.js';
|
|
18
19
|
export { ButtonGroup, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,11 @@ export { Checkbox } from './components/Checkbox.js';
|
|
|
8
8
|
export { Spinner, } from './components/Spinner.js';
|
|
9
9
|
export { Progress, } from './components/Progress.js';
|
|
10
10
|
export { Tooltip, } from './components/Tooltip.js';
|
|
11
|
+
export { InfoTip } from './components/InfoTip.js';
|
|
11
12
|
export { RadioGroup, } from './components/RadioGroup.js';
|
|
12
13
|
export { AppHeader } from './components/AppHeader.js';
|
|
13
14
|
export { Badge, badgeSizes, badgeTones, } from './components/Badge.js';
|
|
14
|
-
export { BarChart, } from './components/BarChart.js';
|
|
15
|
+
export { BarChart, barChartTones, } from './components/BarChart.js';
|
|
15
16
|
export { Button, } from './components/Button.js';
|
|
16
17
|
export { Box, } from './components/Box.js';
|
|
17
18
|
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-
|
|
653
|
-
calc(100% - calc(var(--sw-
|
|
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);
|
|
@@ -1883,12 +1884,6 @@ body {
|
|
|
1883
1884
|
.sw-button-sm.sw-calendar-button { min-width: var(--sw-control-sm-min-height); }
|
|
1884
1885
|
.sw-button-md.sw-calendar-button { min-width: var(--sw-control-md-min-height); }
|
|
1885
1886
|
|
|
1886
|
-
.sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
|
|
1887
|
-
height: var(--sw-space-4);
|
|
1888
|
-
width: var(--sw-space-4);
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
/* A coarse pointer gets the full touch target at the smaller sizes too. */
|
|
1892
1887
|
@media (pointer: coarse) {
|
|
1893
1888
|
.sw-button.sw-calendar-button {
|
|
1894
1889
|
min-height: var(--sw-control-md-min-height);
|
|
@@ -1896,6 +1891,11 @@ body {
|
|
|
1896
1891
|
}
|
|
1897
1892
|
}
|
|
1898
1893
|
|
|
1894
|
+
.sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
|
|
1895
|
+
height: var(--sw-space-4);
|
|
1896
|
+
width: var(--sw-space-4);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
1899
|
.sw-bar-chart {
|
|
1900
1900
|
display: grid;
|
|
1901
1901
|
gap: var(--sw-space-2);
|
|
@@ -1951,6 +1951,11 @@ body {
|
|
|
1951
1951
|
background: var(--sw-color-danger);
|
|
1952
1952
|
}
|
|
1953
1953
|
|
|
1954
|
+
.sw-bar-chart-fill-accent { background: var(--sw-color-accent); }
|
|
1955
|
+
.sw-bar-chart-fill-success { background: var(--sw-color-success); }
|
|
1956
|
+
.sw-bar-chart-fill-warning { background: var(--sw-color-warning); }
|
|
1957
|
+
.sw-bar-chart-fill-danger { background: var(--sw-color-danger); }
|
|
1958
|
+
|
|
1954
1959
|
.sw-bar-chart-value {
|
|
1955
1960
|
color: var(--sw-color-text);
|
|
1956
1961
|
font-family: var(--sw-font-sans);
|
|
@@ -2008,6 +2013,10 @@ body {
|
|
|
2008
2013
|
.sw-bar-chart-diverging .sw-bar-chart-track::before { background: CanvasText; }
|
|
2009
2014
|
}
|
|
2010
2015
|
|
|
2016
|
+
@media (forced-colors: active) {
|
|
2017
|
+
.sw-bar-chart-fill { background: CanvasText; }
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2011
2020
|
.sw-app-header {
|
|
2012
2021
|
background: var(--sw-glass-fill);
|
|
2013
2022
|
border: 1px solid var(--sw-glass-border);
|
|
@@ -2061,10 +2070,9 @@ body {
|
|
|
2061
2070
|
padding-inline: var(--sw-control-xs-padding-inline);
|
|
2062
2071
|
}
|
|
2063
2072
|
|
|
2073
|
+
/* The chevron's layers place themselves from this inset (selectChevronLayers). */
|
|
2064
2074
|
.sw-field-xs select {
|
|
2065
|
-
|
|
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);
|
|
2075
|
+
--sw-select-chevron-inset: var(--sw-space-3);
|
|
2068
2076
|
height: calc(var(--sw-control-xs-min-height) - 1px - 1px);
|
|
2069
2077
|
min-height: 0;
|
|
2070
2078
|
padding-inline-end: calc(
|
|
@@ -2940,6 +2948,22 @@ body {
|
|
|
2940
2948
|
.sw-tooltip { border-color: CanvasText; box-shadow: none; }
|
|
2941
2949
|
}
|
|
2942
2950
|
|
|
2951
|
+
.sw-button.sw-info-tip {
|
|
2952
|
+
flex: none;
|
|
2953
|
+
padding-inline: 0;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
.sw-button-xs.sw-info-tip { min-width: var(--sw-control-xs-min-height); }
|
|
2957
|
+
.sw-button-sm.sw-info-tip { min-width: var(--sw-control-sm-min-height); }
|
|
2958
|
+
.sw-button-md.sw-info-tip { min-width: var(--sw-control-md-min-height); }
|
|
2959
|
+
|
|
2960
|
+
@media (pointer: coarse) {
|
|
2961
|
+
.sw-button.sw-info-tip {
|
|
2962
|
+
min-height: var(--sw-control-md-min-height);
|
|
2963
|
+
min-width: var(--sw-control-md-min-height);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2943
2967
|
.sw-separator {
|
|
2944
2968
|
background: var(--sw-color-border);
|
|
2945
2969
|
border: 0;
|
|
@@ -3611,17 +3635,6 @@ body {
|
|
|
3611
3635
|
border-color: var(--sw-color-danger);
|
|
3612
3636
|
}
|
|
3613
3637
|
|
|
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
3638
|
.sw-field-adorned {
|
|
3626
3639
|
background-color: var(--sw-glass-fill);
|
|
3627
3640
|
border: 1px solid var(--sw-color-border);
|
|
@@ -3685,11 +3698,47 @@ body {
|
|
|
3685
3698
|
|
|
3686
3699
|
[data-theme] .sw-field-adorned > input:disabled { background: transparent; }
|
|
3687
3700
|
|
|
3701
|
+
[data-theme] .sw-field-changed > :is(input, select, textarea),
|
|
3702
|
+
.sw-field-changed .sw-field-adorned {
|
|
3703
|
+
border-color: var(--sw-color-accent);
|
|
3704
|
+
box-shadow: inset 0 0 0 1px var(--sw-color-accent);
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
[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),
|
|
3708
|
+
.sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) {
|
|
3709
|
+
background-image: linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent));
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
[data-theme] .sw-field-changed > select {
|
|
3713
|
+
background-image:
|
|
3714
|
+
linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
|
|
3715
|
+
linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%),
|
|
3716
|
+
linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent));
|
|
3717
|
+
background-position:
|
|
3718
|
+
calc(100% - var(--sw-select-chevron-inset)) calc(50% - 1px),
|
|
3719
|
+
calc(100% - calc(var(--sw-select-chevron-inset) - var(--sw-space-1))) calc(50% - 1px),
|
|
3720
|
+
0 0;
|
|
3721
|
+
background-size: var(--sw-space-1) var(--sw-space-1),
|
|
3722
|
+
var(--sw-space-1) var(--sw-space-1),
|
|
3723
|
+
100% 100%;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
.sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) > :is(.sw-field-prefix, .sw-field-suffix) {
|
|
3727
|
+
color: var(--sw-color-text);
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3688
3730
|
@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
3731
|
[data-theme] .sw-field-changed > :is(input, select, textarea),
|
|
3692
3732
|
.sw-field-changed .sw-field-adorned { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
|
|
3733
|
+
[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),
|
|
3734
|
+
.sw-field-changed .sw-field-adorned:not(:has(> input:placeholder-shown)) { background-image: none; }
|
|
3735
|
+
[data-theme] .sw-field-changed > select { background-image: linear-gradient(45deg, transparent 50%, var(--sw-color-muted) 50%),
|
|
3736
|
+
linear-gradient(135deg, var(--sw-color-muted) 50%, transparent 50%); }
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
@media (forced-colors: active) {
|
|
3740
|
+
[data-theme] .sw-field-invalid :is(input, select, textarea),
|
|
3741
|
+
.sw-field-invalid .sw-field-adorned { border-color: Mark; }
|
|
3693
3742
|
.sw-field-prefix, .sw-field-suffix { color: CanvasText; }
|
|
3694
3743
|
.sw-field-adorned:has(> input:disabled) {
|
|
3695
3744
|
border-color: GrayText;
|