@scalewing/react 1.28.0 → 1.29.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 +19 -0
- package/README.md +39 -1
- package/dist/components/Checkbox.js +5 -4
- package/dist/components/GlyphTile.d.ts +21 -0
- package/dist/components/GlyphTile.js +17 -0
- package/dist/components/MultiSelect.d.ts +10 -0
- package/dist/components/MultiSelect.js +62 -0
- package/dist/components/SegmentedControl.d.ts +8 -0
- package/dist/components/SegmentedControl.js +4 -3
- package/dist/components/checkbox/CheckboxControl.d.ts +9 -0
- package/dist/components/checkbox/CheckboxControl.js +9 -0
- package/dist/components/multi-select/MultiSelectPanel.d.ts +25 -0
- package/dist/components/multi-select/MultiSelectPanel.js +41 -0
- package/dist/components/multi-select/types.d.ts +35 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/multi-select/validate.d.ts +3 -0
- package/dist/components/multi-select/validate.js +25 -0
- package/dist/css/css-components.js +7 -0
- package/dist/css/css-glyph-tile.d.ts +5 -0
- package/dist/css/css-glyph-tile.js +57 -0
- package/dist/css/css-multi-select.d.ts +2 -0
- package/dist/css/css-multi-select.js +156 -0
- package/dist/css/css-segmented.js +58 -0
- package/dist/css/css-variables.js +11 -1
- package/dist/css/stacking.js +5 -2
- package/dist/css/stylesheet.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles.css +223 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 98f6f89: `GlyphTile` (with `GlyphTileProps`, `GlyphTileHue`, `GlyphTileSize` and `glyphTileSizes`): a small rounded square in a categorical colour holding the consumer's glyph in white, the mark of a kind beside its name (`docs/requests/teisoro-glyph-tile.md`, Teisoro F-011 release R3).
|
|
8
|
+
|
|
9
|
+
- `hue` (`green`, `blue`, `orange`, `purple`, `teal`, `rose` or `neutral`, from `@scalewing/tokens` categorical colours), `size` (`sm` 24, `md` 28 by default, `lg` 32), `children` (the glyph, drawn at spacing step 4).
|
|
10
|
+
- Always `aria-hidden`; forced colors draw its edge. An unknown `hue` or `size` throws a `RangeError`.
|
|
11
|
+
- New generated custom properties `--sw-categorical-<colour>` and `--sw-categorical-on` in both schemes, and classes `sw-glyph-tile`, `sw-glyph-tile-<hue>` and `sw-glyph-tile-<size>`. Needs `@scalewing/tokens` 1.5.0. No new dependencies.
|
|
12
|
+
|
|
13
|
+
- b8fe06f: `MultiSelect` (with `MultiSelectProps`, `MultiSelectOption` and `MultiSelectAlign`): several choices from a short list behind one trigger (`docs/requests/teisoro-multi-select.md`, Teisoro F-011 release R3).
|
|
14
|
+
|
|
15
|
+
- `label`, `options` (`{ value, label, icon?, separatorBefore? }`), `value`, `onChange` (checked values in the options' order), `allLabel`, `clearLabel`, `summary` (the trigger's words), `disabled`, `align`.
|
|
16
|
+
- A disclosure: a button with `aria-expanded` showing a group of native checkboxes, with All and Clear, on the popover layer beside the trigger; focus moves to the first checkbox, Escape returns it to the trigger, and focus or a press outside closes the panel.
|
|
17
|
+
- Blank words, no or duplicate options, an unknown value or `align` throw a `RangeError`.
|
|
18
|
+
- New generated `sw-multi-select-*` classes; the panel joins the popup layer in `css/stacking.ts`. No new tokens or dependencies.
|
|
19
|
+
|
|
20
|
+
- 4a648a9: `SegmentedControl` item `icon` (`docs/requests/teisoro-segmented-control-icon.md`, Teisoro F-011 release R3): a consumer mark (such as a `GlyphTile`) before an item's label, `aria-hidden`. A track with icons takes the `md` control height and label type, a subtle track fill and a raised selected item; a track without icons is unchanged. New generated classes `sw-segmented-with-icons` and `sw-segmented-item-icon`. No new tokens or dependencies.
|
|
21
|
+
|
|
3
22
|
## 1.28.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -327,7 +327,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
327
327
|
/>
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
`SegmentedControl` is one exclusive choice shown as a track of segments (`role="radiogroup"`), named by `aria-label` or `aria-labelledby`, with a controlled `value` and `onChange`; `variant="filled"` gives every segment the same width and fills the selection with the accent, and `disabled` keeps the choice visible but inert. `error` puts a message under the track (`sw-field-error`), links it to the group by `aria-describedby` and sets `aria-invalid` and a danger outline on the group (`Mark` in forced colors); as `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. `required` sets `aria-required` on the group; since the control has no label of its own, the element that labels it shows the mark. The track sits in a `.sw-segmented-field` wrapper that holds the message and takes the track's place in the layout, so a `ref` still reaches the `radiogroup`. `error` is for a control in a `Stack` or block layout, where the track keeps its width and the message wraps under it. While it shows a message the field is at least 24ch wide, capped at its container, so a short compact track does not squeeze the message into a column. In an `Inline` the field therefore grows and the rest of the row moves, while the track keeps its labels' width.
|
|
330
|
+
`SegmentedControl` is one exclusive choice shown as a track of segments (`role="radiogroup"`), named by `aria-label` or `aria-labelledby`, with a controlled `value` and `onChange`; `variant="filled"` gives every segment the same width and fills the selection with the accent, and `disabled` keeps the choice visible but inert. `error` puts a message under the track (`sw-field-error`), links it to the group by `aria-describedby` and sets `aria-invalid` and a danger outline on the group (`Mark` in forced colors); as `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. `required` sets `aria-required` on the group; since the control has no label of its own, the element that labels it shows the mark. The track sits in a `.sw-segmented-field` wrapper that holds the message and takes the track's place in the layout, so a `ref` still reaches the `radiogroup`. `error` is for a control in a `Stack` or block layout, where the track keeps its width and the message wraps under it. While it shows a message the field is at least 24ch wide, capped at its container, so a short compact track does not squeeze the message into a column. In an `Inline` the field therefore grows and the rest of the row moves, while the track keeps its labels' width. An item's `icon` (such as a small `GlyphTile`) sits before its label, hidden from assistive technology, so the label names the segment; when any item has one, a compact track takes the `md` control height and label type on a subtle fill, with the selected item raised, and a filled track keeps its look and only gains the marks.
|
|
331
331
|
|
|
332
332
|
```tsx
|
|
333
333
|
<SegmentedControl
|
|
@@ -391,6 +391,44 @@ A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `
|
|
|
391
391
|
|
|
392
392
|
`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.
|
|
393
393
|
|
|
394
|
+
`GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`.
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
<Inline gap={2} align="center">
|
|
398
|
+
<GlyphTile hue="teal">
|
|
399
|
+
<Waves />
|
|
400
|
+
</GlyphTile>
|
|
401
|
+
<Text>Reef</Text>
|
|
402
|
+
</Inline>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`MultiSelect` is several choices from a short list behind one trigger, such as which kinds a list shows. The trigger reads the muted `label` and then `summary` (your words for what is chosen, such as "All entries" or "2 services") with a chevron; it opens a panel named by the label with `allLabel` and `clearLabel` commands over one checkbox per option, each with an optional `icon` (use a small `GlyphTile`, never a bare dot) and a hairline before an option with `separatorBefore`. `onChange` reports the checked values in the options' order. It is a disclosure of native checkboxes, not a listbox: the first checkbox takes focus as it opens, Escape closes it and returns focus to the trigger, focus or a press outside closes it, and disabling it while open closes it. Blank words, no or duplicate options, a value that is not an option, or an unknown `align` throw a `RangeError`.
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
<MultiSelect
|
|
409
|
+
label="Show"
|
|
410
|
+
summary={
|
|
411
|
+
shown.length === options.length ? 'All sightings' : `${shown.length} kinds`
|
|
412
|
+
}
|
|
413
|
+
options={[
|
|
414
|
+
{
|
|
415
|
+
value: 'forest',
|
|
416
|
+
label: 'Forest',
|
|
417
|
+
icon: (
|
|
418
|
+
<GlyphTile hue="green" size="sm">
|
|
419
|
+
<Trees />
|
|
420
|
+
</GlyphTile>
|
|
421
|
+
),
|
|
422
|
+
},
|
|
423
|
+
{ value: 'station', label: 'Station moves', separatorBefore: true },
|
|
424
|
+
]}
|
|
425
|
+
value={shown}
|
|
426
|
+
onChange={setShown}
|
|
427
|
+
allLabel="All"
|
|
428
|
+
clearLabel="Clear"
|
|
429
|
+
/>
|
|
430
|
+
```
|
|
431
|
+
|
|
394
432
|
`Notice` is a tinted note in the page's flow (`tone`: `info`, `success`, `warning` or `danger`). The fill is the tone mixed into the surface (the accent tint for `info`) and the words are the tone moved toward the text color, so they keep 4.5:1 on the fill in every palette (only where a palette's own `danger` is under 4.5:1 on its surface does the note stay at least as readable as that). `title` is a bold lead; a text message (values interpolated in it included) shares its row while it fits and wraps under it otherwise, and a message with any element (a `span`, or a sentence and a `Checkbox` or `Field` in a `Stack`) is stacked under it. Write the message as plain text or a `span`, not a `Text`, which brings its own color. A control inside keeps its own colors; its muted description takes the notice's ink so it stays readable on the fill. `action` is one link or small `Button` at the end of the row; `icon` is a consumer glyph before the text, hidden from assistive technology. `danger` is `role="alert"` and the other tones `role="status"`; `role="none"` is for a note that is part of the page from the start (no live region). Screen readers announce an `alert` that appears, but not reliably a `status` mounted with its words: to have a status heard, keep the Notice mounted and change its words, or move focus to it. It never dismisses itself. Pass `tabIndex={-1}` and a `ref` to move focus to it. `variant="band"` makes it a full-width square strip of page chrome in caption type with its words centered, at least the `xs` control height, such as an environment strip first on the page (`<Notice tone="warning" variant="band" title="Field station">Records here stay out of the census.</Notice>`). It is as wide as its container, so put it outside any padded column, as the page's first child. A glyph or an action stays on the words' row and the words wrap. A band has no live region unless you pass `role`. An unknown `tone`, `role` or `variant`, a blank `title`, or a note with neither a title nor a message throws a `RangeError`.
|
|
395
433
|
|
|
396
434
|
```tsx
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from 'react';
|
|
4
|
+
import { CheckboxControl } from './checkbox/CheckboxControl.js';
|
|
4
5
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
5
6
|
export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, }) {
|
|
6
7
|
const descriptionId = useId();
|
|
@@ -11,8 +12,8 @@ export function Checkbox({ label, checked, onCheckedChange, disabled = false, re
|
|
|
11
12
|
]
|
|
12
13
|
.filter((id) => id !== null)
|
|
13
14
|
.join(' ');
|
|
14
|
-
return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [_jsx(CheckboxControl, { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, disabled: disabled, onChange: (event) => {
|
|
16
|
+
if (!disabled)
|
|
17
|
+
onCheckedChange(event.currentTarget.checked);
|
|
18
|
+
}, required: required }), _jsx("span", { className: "sw-checkbox-text", children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-checkbox-error", id: errorId, message: error })] }));
|
|
18
19
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CategoricalColor } from '@scalewing/tokens';
|
|
2
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
3
|
+
import { type GlyphTileSize, glyphTileSizes } from '../css/css-glyph-tile.js';
|
|
4
|
+
export { glyphTileSizes };
|
|
5
|
+
export type { GlyphTileSize };
|
|
6
|
+
/** A categorical hue (`green`, `blue`, `orange`, `purple`, `teal`, `rose`) or `neutral`. */
|
|
7
|
+
export type GlyphTileHue = CategoricalColor;
|
|
8
|
+
export type GlyphTileProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'aria-hidden'> & {
|
|
9
|
+
/** The categorical fill: a hue that tells a kind apart, or `neutral`. */
|
|
10
|
+
hue: GlyphTileHue;
|
|
11
|
+
/** `sm` 24, `md` 28 (default), `lg` 32. */
|
|
12
|
+
size?: GlyphTileSize;
|
|
13
|
+
/** The consumer's glyph (Lucide), drawn in white at spacing step 4. */
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A small rounded square in a categorical colour holding a white glyph: a
|
|
18
|
+
* kind's mark beside its name, such as a service in a list. Decorative
|
|
19
|
+
* (`aria-hidden`): the visible label beside it carries the meaning.
|
|
20
|
+
*/
|
|
21
|
+
export declare function GlyphTile({ children, className, hue, size, ...rest }: GlyphTileProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { isCategoricalColor } from '@scalewing/tokens';
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { glyphTileSizes } from '../css/css-glyph-tile.js';
|
|
5
|
+
export { glyphTileSizes };
|
|
6
|
+
/**
|
|
7
|
+
* A small rounded square in a categorical colour holding a white glyph: a
|
|
8
|
+
* kind's mark beside its name, such as a service in a list. Decorative
|
|
9
|
+
* (`aria-hidden`): the visible label beside it carries the meaning.
|
|
10
|
+
*/
|
|
11
|
+
export function GlyphTile({ children, className, hue, size = 'md', ...rest }) {
|
|
12
|
+
if (!isCategoricalColor(hue))
|
|
13
|
+
throw new RangeError(`Unknown GlyphTile hue: ${String(hue)}`);
|
|
14
|
+
if (!glyphTileSizes.includes(size))
|
|
15
|
+
throw new RangeError(`Unknown GlyphTile size: ${String(size)}`);
|
|
16
|
+
return (_jsx("span", { ...rest, "aria-hidden": "true", className: cx('sw-glyph-tile', `sw-glyph-tile-${hue}`, `sw-glyph-tile-${size}`, className), children: children }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type MultiSelectProps } from './multi-select/types.js';
|
|
2
|
+
export type { MultiSelectAlign, MultiSelectOption, MultiSelectProps, } from './multi-select/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Several choices from a short list behind one trigger: "Show All entries ▾"
|
|
5
|
+
* opens a panel with All, Clear and a checkbox per option. A disclosure (a
|
|
6
|
+
* button that shows a group of native checkboxes), not a listbox, so each
|
|
7
|
+
* option is an ordinary checkbox to every user. The consumer owns the
|
|
8
|
+
* checked values and the trigger's summary words.
|
|
9
|
+
*/
|
|
10
|
+
export declare function MultiSelect({ align, allLabel, clearLabel, disabled, label, onChange, options, summary, value, }: MultiSelectProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState } from 'react';
|
|
4
|
+
import { MultiSelectPanel } from './multi-select/MultiSelectPanel.js';
|
|
5
|
+
import { assertMultiSelect } from './multi-select/validate.js';
|
|
6
|
+
/**
|
|
7
|
+
* Several choices from a short list behind one trigger: "Show All entries ▾"
|
|
8
|
+
* opens a panel with All, Clear and a checkbox per option. A disclosure (a
|
|
9
|
+
* button that shows a group of native checkboxes), not a listbox, so each
|
|
10
|
+
* option is an ordinary checkbox to every user. The consumer owns the
|
|
11
|
+
* checked values and the trigger's summary words.
|
|
12
|
+
*/
|
|
13
|
+
export function MultiSelect({ align = 'start', allLabel, clearLabel, disabled = false, label, onChange, options, summary, value, }) {
|
|
14
|
+
assertMultiSelect({
|
|
15
|
+
allLabel,
|
|
16
|
+
align,
|
|
17
|
+
clearLabel,
|
|
18
|
+
label,
|
|
19
|
+
options,
|
|
20
|
+
summary,
|
|
21
|
+
value,
|
|
22
|
+
});
|
|
23
|
+
const panelId = useId();
|
|
24
|
+
const triggerRef = useRef(null);
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const checked = new Set(value);
|
|
27
|
+
const shown = open && !disabled;
|
|
28
|
+
// Disabling while open closes the panel for good (state adjusted during
|
|
29
|
+
// render), so enabling it again never reopens it, or pulls focus into it,
|
|
30
|
+
// on its own.
|
|
31
|
+
if (disabled && open)
|
|
32
|
+
setOpen(false);
|
|
33
|
+
function close(restoreFocus) {
|
|
34
|
+
setOpen(false);
|
|
35
|
+
if (restoreFocus)
|
|
36
|
+
triggerRef.current?.focus();
|
|
37
|
+
}
|
|
38
|
+
function report(next) {
|
|
39
|
+
onChange(options
|
|
40
|
+
.filter((option) => next.has(option.value))
|
|
41
|
+
.map((option) => option.value));
|
|
42
|
+
}
|
|
43
|
+
function onToggle(changed, isChecked) {
|
|
44
|
+
const next = new Set(checked);
|
|
45
|
+
if (isChecked)
|
|
46
|
+
next.add(changed);
|
|
47
|
+
else
|
|
48
|
+
next.delete(changed);
|
|
49
|
+
report(next);
|
|
50
|
+
}
|
|
51
|
+
function onTriggerKeyDown(event) {
|
|
52
|
+
if (event.key === 'ArrowDown' && !shown) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
setOpen(true);
|
|
55
|
+
}
|
|
56
|
+
else if (event.key === 'Escape' && shown) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
close(true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs("div", { className: "sw-multi-select", children: [_jsxs("button", { "aria-controls": shown ? panelId : undefined, "aria-expanded": shown, className: "sw-multi-select-trigger", disabled: disabled, onClick: () => (shown ? close(true) : setOpen(true)), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: [_jsx("span", { className: "sw-multi-select-label", children: label }), ' ', _jsx("span", { className: "sw-multi-select-summary", children: summary })] }), shown ? (_jsx(MultiSelectPanel, { align: align, allLabel: allLabel, checked: checked, clearLabel: clearLabel, id: panelId, label: label, onAll: () => report(new Set(options.map((option) => option.value))), onClear: () => report(new Set()), onClose: close, onToggle: onToggle, options: options, triggerRef: triggerRef })) : null] }));
|
|
62
|
+
}
|
|
@@ -2,6 +2,14 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
export type SegmentedItem = {
|
|
3
3
|
id: string;
|
|
4
4
|
label: ReactNode;
|
|
5
|
+
/**
|
|
6
|
+
* A consumer mark before the label, such as a `GlyphTile` or a Lucide
|
|
7
|
+
* glyph, hidden from assistive technology (the label names the item).
|
|
8
|
+
* When any item has one, every item of a compact track takes the `md`
|
|
9
|
+
* control height and label type on a subtle track; a filled track keeps
|
|
10
|
+
* its look and gains the marks. A falsy value (`0`, `''`) is no icon.
|
|
11
|
+
*/
|
|
12
|
+
icon?: ReactNode;
|
|
5
13
|
};
|
|
6
14
|
type SegmentedLabel = {
|
|
7
15
|
'aria-label': string;
|
|
@@ -5,6 +5,7 @@ import { cx } from '../class-names.js';
|
|
|
5
5
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
6
|
export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
|
|
7
7
|
const errorId = useId();
|
|
8
|
+
const withIcons = items.some((item) => item.icon);
|
|
8
9
|
function onKeyDown(event) {
|
|
9
10
|
if (disabled || (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft')) {
|
|
10
11
|
return;
|
|
@@ -22,12 +23,12 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
|
|
|
22
23
|
}
|
|
23
24
|
// The wrapper is always rendered, so an error that appears later swaps
|
|
24
25
|
// text into a live region that already exists and the group keeps focus.
|
|
25
|
-
return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
|
|
26
|
+
return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
|
|
26
27
|
const selected = item.id === value;
|
|
27
|
-
return (
|
|
28
|
+
return (_jsxs("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
|
|
28
29
|
if (!selected) {
|
|
29
30
|
onChange?.(item.id);
|
|
30
31
|
}
|
|
31
|
-
}, role: "radio", type: "button", children: item.label }, item.id));
|
|
32
|
+
}, role: "radio", type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, item.label] }, item.id));
|
|
32
33
|
}) }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
|
|
33
34
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'>;
|
|
3
|
+
/**
|
|
4
|
+
* The native checkbox and its drawn mark, the one markup Checkbox's
|
|
5
|
+
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
6
|
+
* and MultiSelect's rows so the two cannot drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export declare function CheckboxControl(props: CheckboxControlProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The native checkbox and its drawn mark, the one markup Checkbox's
|
|
4
|
+
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
5
|
+
* and MultiSelect's rows so the two cannot drift apart.
|
|
6
|
+
*/
|
|
7
|
+
export function CheckboxControl(props) {
|
|
8
|
+
return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type MultiSelectAlign, type MultiSelectOption } from './types.js';
|
|
3
|
+
type MultiSelectPanelProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
align: MultiSelectAlign;
|
|
7
|
+
options: readonly MultiSelectOption[];
|
|
8
|
+
checked: ReadonlySet<string>;
|
|
9
|
+
allLabel: string;
|
|
10
|
+
clearLabel: string;
|
|
11
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
12
|
+
onToggle: (value: string, checked: boolean) => void;
|
|
13
|
+
onAll: () => void;
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
/** Closes the panel, returning focus to the trigger when asked. */
|
|
16
|
+
onClose: (restoreFocus: boolean) => void;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The open panel: a group named by the label on the popover layer beside
|
|
20
|
+
* the trigger, with All and Clear over one checkbox per option. The first
|
|
21
|
+
* checkbox takes focus as it opens; Escape closes it and returns focus to
|
|
22
|
+
* the trigger; focus or a press moving outside it closes it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function MultiSelectPanel({ align, allLabel, checked, clearLabel, id, label, onAll, onClear, onClose, onToggle, options, triggerRef, }: MultiSelectPanelProps): import("react").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef, } from 'react';
|
|
4
|
+
import { Button } from '../Button.js';
|
|
5
|
+
import { CheckboxControl } from '../checkbox/CheckboxControl.js';
|
|
6
|
+
import { useAnchoredPopover } from '../use-anchored-popover.js';
|
|
7
|
+
/**
|
|
8
|
+
* The open panel: a group named by the label on the popover layer beside
|
|
9
|
+
* the trigger, with All and Clear over one checkbox per option. The first
|
|
10
|
+
* checkbox takes focus as it opens; Escape closes it and returns focus to
|
|
11
|
+
* the trigger; focus or a press moving outside it closes it.
|
|
12
|
+
*/
|
|
13
|
+
export function MultiSelectPanel({ align, allLabel, checked, clearLabel, id, label, onAll, onClear, onClose, onToggle, options, triggerRef, }) {
|
|
14
|
+
const panelRef = useRef(null);
|
|
15
|
+
useAnchoredPopover(panelRef, triggerRef, triggerRef, () => onClose(false), {
|
|
16
|
+
align,
|
|
17
|
+
flipInline: true,
|
|
18
|
+
});
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
panelRef.current
|
|
21
|
+
?.querySelector('.sw-multi-select-option input')
|
|
22
|
+
?.focus();
|
|
23
|
+
}, []);
|
|
24
|
+
function onKeyDown(event) {
|
|
25
|
+
if (event.key !== 'Escape')
|
|
26
|
+
return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
onClose(true);
|
|
29
|
+
}
|
|
30
|
+
function onBlur(event) {
|
|
31
|
+
const next = event.relatedTarget;
|
|
32
|
+
if (!(next instanceof Node))
|
|
33
|
+
return;
|
|
34
|
+
if (panelRef.current?.contains(next) || triggerRef.current?.contains(next))
|
|
35
|
+
return;
|
|
36
|
+
onClose(false);
|
|
37
|
+
}
|
|
38
|
+
return (_jsxs("div", { "aria-label": label, className: "sw-multi-select-panel", id: id, onBlur: onBlur, onKeyDown: onKeyDown, ref: panelRef, role: "group", children: [_jsxs("div", { className: "sw-multi-select-commands", children: [_jsx(Button, { onPress: onAll, size: "sm", variant: "ghost", children: allLabel }), _jsx(Button, { onPress: onClear, size: "sm", variant: "ghost", children: clearLabel })] }), options.map((option) => (_jsx("div", { className: option.separatorBefore
|
|
39
|
+
? 'sw-multi-select-row sw-multi-select-row-separated'
|
|
40
|
+
: 'sw-multi-select-row', children: _jsxs("label", { className: "sw-checkbox-label sw-multi-select-option", children: [_jsx(CheckboxControl, { checked: checked.has(option.value), onChange: (event) => onToggle(option.value, event.currentTarget.checked) }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-multi-select-icon", children: option.icon })) : null, _jsx("span", { className: "sw-multi-select-text", children: option.label })] }) }, option.value)))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type InlineAlign } from '../../anchored-position.js';
|
|
3
|
+
/** One choice in a MultiSelect. */
|
|
4
|
+
export type MultiSelectOption = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** A consumer mark before the label, such as a `GlyphTile`; hidden from assistive technology. */
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
/** Draws a hairline before this option, to set a group of options apart. */
|
|
10
|
+
separatorBefore?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type MultiSelectAlign = InlineAlign;
|
|
13
|
+
export type MultiSelectProps = {
|
|
14
|
+
/**
|
|
15
|
+
* What is chosen, such as "Show": the muted lead of the trigger and the
|
|
16
|
+
* name of the panel of checkboxes.
|
|
17
|
+
*/
|
|
18
|
+
label: string;
|
|
19
|
+
options: readonly MultiSelectOption[];
|
|
20
|
+
/** The checked values; `onChange` reports them in the options' order. */
|
|
21
|
+
value: readonly string[];
|
|
22
|
+
onChange: (value: string[]) => void;
|
|
23
|
+
/** The command that checks every option, such as "All". */
|
|
24
|
+
allLabel: string;
|
|
25
|
+
/** The command that clears every option, such as "Clear". */
|
|
26
|
+
clearLabel: string;
|
|
27
|
+
/**
|
|
28
|
+
* The trigger's words after the label, naming what is chosen, such as
|
|
29
|
+
* "All entries" or "2 services". The consumer writes it.
|
|
30
|
+
*/
|
|
31
|
+
summary: string;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** The trigger edge the panel lines up with (default start). */
|
|
34
|
+
align?: MultiSelectAlign;
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type MultiSelectProps } from './types.js';
|
|
2
|
+
/** Fails closed on words or options a MultiSelect cannot show. */
|
|
3
|
+
export declare function assertMultiSelect({ allLabel, align, clearLabel, label, options, summary, value, }: Pick<MultiSelectProps, 'allLabel' | 'align' | 'clearLabel' | 'label' | 'options' | 'summary' | 'value'>): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function isBlank(text) {
|
|
2
|
+
return typeof text !== 'string' || !text.trim();
|
|
3
|
+
}
|
|
4
|
+
/** Fails closed on words or options a MultiSelect cannot show. */
|
|
5
|
+
export function assertMultiSelect({ allLabel, align, clearLabel, label, options, summary, value, }) {
|
|
6
|
+
if (isBlank(label) || isBlank(summary))
|
|
7
|
+
throw new RangeError('MultiSelect needs a label and a summary');
|
|
8
|
+
if (isBlank(allLabel) || isBlank(clearLabel))
|
|
9
|
+
throw new RangeError('MultiSelect needs an allLabel and a clearLabel');
|
|
10
|
+
if (options.length === 0)
|
|
11
|
+
throw new RangeError('MultiSelect needs at least one option');
|
|
12
|
+
const values = new Set();
|
|
13
|
+
for (const option of options) {
|
|
14
|
+
if (isBlank(option.value) || isBlank(option.label))
|
|
15
|
+
throw new RangeError('MultiSelect options need a value and a label');
|
|
16
|
+
if (values.has(option.value))
|
|
17
|
+
throw new RangeError(`Duplicate MultiSelect value: ${option.value}`);
|
|
18
|
+
values.add(option.value);
|
|
19
|
+
}
|
|
20
|
+
for (const checked of value)
|
|
21
|
+
if (!values.has(checked))
|
|
22
|
+
throw new RangeError(`Unknown MultiSelect value: ${checked}`);
|
|
23
|
+
if (align !== undefined && align !== 'start' && align !== 'end')
|
|
24
|
+
throw new RangeError(`Unknown MultiSelect align: ${String(align)}`);
|
|
25
|
+
}
|
|
@@ -15,12 +15,14 @@ import { cssDateFieldClasses } from './css-date-field.js';
|
|
|
15
15
|
import { cssDialogClasses } from './css-dialog.js';
|
|
16
16
|
import { cssDialogFrameClasses } from './css-dialog-frame.js';
|
|
17
17
|
import { cssSelectClasses } from './css-select.js';
|
|
18
|
+
import { cssMultiSelectClasses } from './css-multi-select.js';
|
|
18
19
|
import { cssSplitClasses } from './css-split.js';
|
|
19
20
|
import { cssSwitchClasses } from './css-switch.js';
|
|
20
21
|
import { cssTextClasses } from './css-text.js';
|
|
21
22
|
import { cssSpinnerClasses } from './css-spinner.js';
|
|
22
23
|
import { cssProgressClasses } from './css-progress.js';
|
|
23
24
|
import { cssNoticeClasses } from './css-notice.js';
|
|
25
|
+
import { cssGlyphTileClasses } from './css-glyph-tile.js';
|
|
24
26
|
import { cssTooltipClasses } from './css-tooltip.js';
|
|
25
27
|
import { cssInfoTipClasses } from './css-info-tip.js';
|
|
26
28
|
import { cssSeparatorClasses } from './css-separator.js';
|
|
@@ -60,6 +62,7 @@ function reducedTransparencyRules() {
|
|
|
60
62
|
.sw-accordion,
|
|
61
63
|
.sw-select-list,
|
|
62
64
|
.sw-action-menu-list,
|
|
65
|
+
.sw-multi-select-panel,
|
|
63
66
|
.sw-date-field-calendar,
|
|
64
67
|
.sw-toast {
|
|
65
68
|
background: var(--sw-color-surface);
|
|
@@ -144,6 +147,8 @@ ${cssSelectClasses()}
|
|
|
144
147
|
|
|
145
148
|
${cssActionMenuClasses()}
|
|
146
149
|
|
|
150
|
+
${cssMultiSelectClasses()}
|
|
151
|
+
|
|
147
152
|
${cssActionBarClasses()}
|
|
148
153
|
|
|
149
154
|
${cssSwitchClasses()}
|
|
@@ -154,6 +159,8 @@ ${cssProgressClasses()}
|
|
|
154
159
|
|
|
155
160
|
${cssNoticeClasses()}
|
|
156
161
|
|
|
162
|
+
${cssGlyphTileClasses()}
|
|
163
|
+
|
|
157
164
|
${cssTooltipClasses()}
|
|
158
165
|
|
|
159
166
|
${cssInfoTipClasses()}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const glyphTileSizes: readonly ["sm", "md", "lg"];
|
|
2
|
+
/** `sm` 24, `md` 28 (the xs control height), `lg` 32. */
|
|
3
|
+
export type GlyphTileSize = (typeof glyphTileSizes)[number];
|
|
4
|
+
export declare function cssGlyphTileClasses(): string;
|
|
5
|
+
export declare function glyphTileClassCatalog(): string[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { categoricalColorKeys } from '@scalewing/tokens';
|
|
2
|
+
export const glyphTileSizes = ['sm', 'md', 'lg'];
|
|
3
|
+
const tileSide = {
|
|
4
|
+
sm: 'var(--sw-space-5)',
|
|
5
|
+
md: 'var(--sw-control-xs-min-height)',
|
|
6
|
+
lg: 'var(--sw-space-6)',
|
|
7
|
+
};
|
|
8
|
+
function hueRules() {
|
|
9
|
+
return categoricalColorKeys
|
|
10
|
+
.map((key) => `.sw-glyph-tile-${key} { --sw-glyph-tile-fill: var(--sw-categorical-${key}); }`)
|
|
11
|
+
.join('\n');
|
|
12
|
+
}
|
|
13
|
+
function sizeRules() {
|
|
14
|
+
return Object.entries(tileSide)
|
|
15
|
+
.map(([size, side]) => `.sw-glyph-tile-${size} { height: ${side}; width: ${side}; }`)
|
|
16
|
+
.join('\n');
|
|
17
|
+
}
|
|
18
|
+
/*
|
|
19
|
+
* A rounded square in a categorical fill with the consumer's glyph centred
|
|
20
|
+
* in white, sized to spacing step 4 whatever the glyph's own size. It is
|
|
21
|
+
* decoration: the words beside it carry the meaning. Forced colors drop the
|
|
22
|
+
* fill, so a border keeps the tile's shape and the glyph takes the text
|
|
23
|
+
* color.
|
|
24
|
+
*/
|
|
25
|
+
export function cssGlyphTileClasses() {
|
|
26
|
+
return `.sw-glyph-tile {
|
|
27
|
+
align-items: center;
|
|
28
|
+
background: var(--sw-glyph-tile-fill);
|
|
29
|
+
border-radius: var(--sw-radius-sm);
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
color: var(--sw-categorical-on);
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
flex: none;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sw-glyph-tile > svg {
|
|
38
|
+
flex: none;
|
|
39
|
+
height: var(--sw-space-4);
|
|
40
|
+
width: var(--sw-space-4);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
${hueRules()}
|
|
44
|
+
|
|
45
|
+
${sizeRules()}
|
|
46
|
+
|
|
47
|
+
@media (forced-colors: active) {
|
|
48
|
+
.sw-glyph-tile { border: 1px solid CanvasText; color: CanvasText; }
|
|
49
|
+
}`;
|
|
50
|
+
}
|
|
51
|
+
export function glyphTileClassCatalog() {
|
|
52
|
+
return [
|
|
53
|
+
'sw-glyph-tile',
|
|
54
|
+
...categoricalColorKeys.map((key) => `sw-glyph-tile-${key}`),
|
|
55
|
+
...glyphTileSizes.map((size) => `sw-glyph-tile-${size}`),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { chevronStroke } from './chevron.js';
|
|
3
|
+
import { zIndex } from './stacking.js';
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
const body = typographyVariants.body;
|
|
6
|
+
/*
|
|
7
|
+
* The trigger: a glass control at the md height with the muted label, the
|
|
8
|
+
* summary and Select's chevron. The panel: a glass popover beside it (as
|
|
9
|
+
* ActionMenu's list), All and Clear over a hairline, then one checkbox row
|
|
10
|
+
* per option at the md height, the whole row its label; a separated row
|
|
11
|
+
* starts after a hairline. Checkboxes reuse Checkbox's generated control.
|
|
12
|
+
*/
|
|
13
|
+
export function cssMultiSelectClasses() {
|
|
14
|
+
return `.sw-multi-select {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
max-width: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.sw-multi-select-trigger {
|
|
20
|
+
align-items: center;
|
|
21
|
+
appearance: none;
|
|
22
|
+
background-color: var(--sw-glass-fill);
|
|
23
|
+
border: 1px solid var(--sw-color-border);
|
|
24
|
+
border-radius: var(--sw-radius-sm);
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
color: var(--sw-color-text);
|
|
27
|
+
column-gap: var(--sw-space-2);
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
font-family: var(--sw-font-sans);
|
|
31
|
+
font-size: ${label.fontSize}px;
|
|
32
|
+
font-weight: ${label.fontWeight};
|
|
33
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
34
|
+
line-height: ${label.lineHeight}px;
|
|
35
|
+
margin: 0;
|
|
36
|
+
max-width: 100%;
|
|
37
|
+
min-height: var(--sw-control-md-min-height);
|
|
38
|
+
padding-inline: var(--sw-control-md-padding-inline);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sw-multi-select-trigger::after {
|
|
42
|
+
${chevronStroke}
|
|
43
|
+
content: '';
|
|
44
|
+
flex: none;
|
|
45
|
+
margin-inline-start: var(--sw-space-2);
|
|
46
|
+
transform: translateY(-25%) rotate(45deg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sw-multi-select-trigger:focus-visible {
|
|
50
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
51
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sw-multi-select-trigger:disabled {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
opacity: var(--sw-disabled-opacity);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.sw-multi-select-label {
|
|
60
|
+
color: var(--sw-color-muted);
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sw-multi-select-summary {
|
|
65
|
+
min-width: 0;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
text-overflow: ellipsis;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sw-multi-select-panel {
|
|
72
|
+
background: var(--sw-glass-fill);
|
|
73
|
+
border: 1px solid var(--sw-glass-border);
|
|
74
|
+
border-radius: var(--sw-radius-sm);
|
|
75
|
+
box-shadow: var(--sw-elevation-sm), inset 0 1px 0 var(--sw-glass-specular);
|
|
76
|
+
backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
77
|
+
-webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
color: var(--sw-color-text);
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
inset: auto;
|
|
83
|
+
margin: 0;
|
|
84
|
+
/* The viewport, not --sw-select-max: the list is short and should show
|
|
85
|
+
whole rather than scroll inside a 22rem cap. */
|
|
86
|
+
max-height: calc(100% - var(--sw-space-2) - var(--sw-space-2));
|
|
87
|
+
max-width: calc(100% - var(--sw-space-2) - var(--sw-space-2));
|
|
88
|
+
min-width: var(--sw-select-max);
|
|
89
|
+
overflow: auto;
|
|
90
|
+
padding: var(--sw-space-1);
|
|
91
|
+
position: fixed;
|
|
92
|
+
width: max-content;
|
|
93
|
+
${zIndex('popup')}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.sw-multi-select-commands {
|
|
97
|
+
border-bottom: 1px solid var(--sw-color-border);
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
margin-bottom: var(--sw-space-1);
|
|
101
|
+
padding-bottom: var(--sw-space-1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.sw-multi-select-row-separated {
|
|
105
|
+
border-top: 1px solid var(--sw-color-border);
|
|
106
|
+
margin-top: var(--sw-space-1);
|
|
107
|
+
padding-top: var(--sw-space-1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sw-checkbox-label.sw-multi-select-option {
|
|
111
|
+
border-radius: var(--sw-radius-sm);
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
font-family: var(--sw-font-sans);
|
|
114
|
+
font-size: ${label.fontSize}px;
|
|
115
|
+
font-weight: ${body.fontWeight};
|
|
116
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
117
|
+
line-height: ${label.lineHeight}px;
|
|
118
|
+
padding-inline: var(--sw-space-2);
|
|
119
|
+
width: 100%;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.sw-multi-select-option:hover {
|
|
123
|
+
background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sw-multi-select-icon {
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
flex: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sw-multi-select-text {
|
|
132
|
+
min-width: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (forced-colors: active) {
|
|
136
|
+
.sw-multi-select-trigger::after { border-color: CanvasText; }
|
|
137
|
+
.sw-multi-select-panel { border-color: CanvasText; }
|
|
138
|
+
.sw-multi-select-commands,
|
|
139
|
+
.sw-multi-select-row-separated { border-color: CanvasText; }
|
|
140
|
+
}`;
|
|
141
|
+
}
|
|
142
|
+
export function multiSelectClassCatalog() {
|
|
143
|
+
return [
|
|
144
|
+
'sw-multi-select',
|
|
145
|
+
'sw-multi-select-trigger',
|
|
146
|
+
'sw-multi-select-label',
|
|
147
|
+
'sw-multi-select-summary',
|
|
148
|
+
'sw-multi-select-panel',
|
|
149
|
+
'sw-multi-select-commands',
|
|
150
|
+
'sw-multi-select-row',
|
|
151
|
+
'sw-multi-select-row-separated',
|
|
152
|
+
'sw-multi-select-option',
|
|
153
|
+
'sw-multi-select-icon',
|
|
154
|
+
'sw-multi-select-text',
|
|
155
|
+
];
|
|
156
|
+
}
|
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
import { trackInset, typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
const body = typographyVariants.body;
|
|
3
3
|
const data = typographyVariants.data;
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
/*
|
|
6
|
+
* Items with a mark (a glyph tile before the label) centre the mark and
|
|
7
|
+
* the label a space-2 gap apart; the mark keeps its size. On the compact
|
|
8
|
+
* track every item also takes the md control height and label type, so
|
|
9
|
+
* the row stays even; the track is the subtle fill (two classes, so
|
|
10
|
+
* reduced transparency keeps it) and the selected item is the surface
|
|
11
|
+
* raised by the small elevation, so the choice shows on a white canvas
|
|
12
|
+
* too. A filled track keeps its own look and only gains the marks. The
|
|
13
|
+
* error outline is restated after these rules so an icon track keeps it.
|
|
14
|
+
* In forced colours the selected item is outlined in Highlight; a focused
|
|
15
|
+
* selected item keeps that outline, offset by the focus ring's offset.
|
|
16
|
+
*/
|
|
17
|
+
function iconRules() {
|
|
18
|
+
const compact = '.sw-segmented-with-icons:not(.sw-segmented-filled)';
|
|
19
|
+
return `.sw-segmented${compact} {
|
|
20
|
+
background: var(--sw-color-subtle);
|
|
21
|
+
border-color: transparent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
${compact} .sw-segmented-item-selected {
|
|
25
|
+
box-shadow: var(--sw-elevation-sm);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sw-segmented-with-icons .sw-segmented-item {
|
|
29
|
+
align-items: center;
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
gap: var(--sw-space-2);
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
${compact} .sw-segmented-item {
|
|
36
|
+
font-size: ${label.fontSize}px;
|
|
37
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
38
|
+
line-height: ${label.lineHeight}px;
|
|
39
|
+
min-height: var(--sw-control-md-min-height);
|
|
40
|
+
padding-inline: var(--sw-space-2) var(--sw-space-3);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.sw-segmented-item-icon {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
flex: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sw-segmented.sw-segmented-with-icons[aria-invalid='true'] {
|
|
49
|
+
border-color: var(--sw-color-danger);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@media (forced-colors: active) {
|
|
53
|
+
.sw-segmented${compact} { border-color: CanvasText; }
|
|
54
|
+
${compact} .sw-segmented-item-selected { outline: 2px solid Highlight; }
|
|
55
|
+
.sw-segmented.sw-segmented-with-icons[aria-invalid='true'] { border-color: Mark; }
|
|
56
|
+
}`;
|
|
57
|
+
}
|
|
4
58
|
/*
|
|
5
59
|
* The control and its error message. The wrapper takes the track's place in
|
|
6
60
|
* the layout: inline beside text and stretched in a Stack (compact), block
|
|
@@ -106,6 +160,8 @@ export function cssSegmentedClasses() {
|
|
|
106
160
|
color: var(--sw-color-onAccent);
|
|
107
161
|
}
|
|
108
162
|
|
|
163
|
+
${iconRules()}
|
|
164
|
+
|
|
109
165
|
.sw-segmented-disabled {
|
|
110
166
|
opacity: var(--sw-disabled-opacity);
|
|
111
167
|
}
|
|
@@ -122,6 +178,8 @@ export function segmentedClassCatalog() {
|
|
|
122
178
|
'sw-segmented-item',
|
|
123
179
|
'sw-segmented-item-selected',
|
|
124
180
|
'sw-segmented-filled',
|
|
181
|
+
'sw-segmented-with-icons',
|
|
182
|
+
'sw-segmented-item-icon',
|
|
125
183
|
'sw-segmented-disabled',
|
|
126
184
|
];
|
|
127
185
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { controlScale, disabledOpacity, focusRing, quietOpacity, radiusScale, secondaryActionBorder, semanticColorKeys, spacingScale, spacingSteps, splitScale, } from '@scalewing/tokens';
|
|
1
|
+
import { categoricalColorKeys, categoricalColors, onCategorical, controlScale, disabledOpacity, focusRing, quietOpacity, radiusScale, secondaryActionBorder, semanticColorKeys, spacingScale, spacingSteps, splitScale, } from '@scalewing/tokens';
|
|
2
2
|
function colorDeclarations(theme) {
|
|
3
3
|
return [
|
|
4
4
|
...semanticColorKeys.map((key) => ` --sw-color-${key}: ${theme.colors[key]};`),
|
|
5
5
|
` --sw-button-secondary-border: ${secondaryActionBorder(theme.colors)};`,
|
|
6
6
|
].join('\n');
|
|
7
7
|
}
|
|
8
|
+
/* The categorical fills of the theme's scheme and the glyph drawn on them. */
|
|
9
|
+
function categoricalDeclarations(theme) {
|
|
10
|
+
const fills = categoricalColors[theme.colorScheme];
|
|
11
|
+
return [
|
|
12
|
+
...categoricalColorKeys.map((key) => ` --sw-categorical-${key}: ${fills[key]};`),
|
|
13
|
+
` --sw-categorical-on: ${onCategorical};`,
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
8
16
|
function glassDeclarations(theme) {
|
|
9
17
|
return ` --sw-glass-blur: ${theme.glass.blur}px;
|
|
10
18
|
--sw-glass-saturate: ${theme.glass.saturate};
|
|
@@ -54,11 +62,13 @@ export function cssVariables(light, dark) {
|
|
|
54
62
|
return `:root,
|
|
55
63
|
[data-theme='light'] {
|
|
56
64
|
${colorDeclarations(light)}
|
|
65
|
+
${categoricalDeclarations(light)}
|
|
57
66
|
${sharedDeclarations(light)}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
[data-theme='dark'] {
|
|
61
70
|
${colorDeclarations(dark)}
|
|
71
|
+
${categoricalDeclarations(dark)}
|
|
62
72
|
${sharedDeclarations(dark)}
|
|
63
73
|
}`;
|
|
64
74
|
}
|
package/dist/css/stacking.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* surface that scrolls up under it (the header is the page's way back, the
|
|
15
15
|
* strip its way between sections, and a popup opens downward).
|
|
16
16
|
* 5. `popup`: a popup outside any lifted surface (Select list, ActionMenu
|
|
17
|
-
* list, Tooltip, the DateField and CalendarButton
|
|
18
|
-
* everything in the page.
|
|
17
|
+
* list, MultiSelect panel, Tooltip, the DateField and CalendarButton
|
|
18
|
+
* calendar) over everything in the page.
|
|
19
19
|
*
|
|
20
20
|
* Dialog and Toast sit on the browser's top layer, above all of these.
|
|
21
21
|
*/
|
|
@@ -33,6 +33,9 @@ const openPopups = [
|
|
|
33
33
|
'.sw-select-list',
|
|
34
34
|
// Rendered only while open; on the top layer where the popover API exists.
|
|
35
35
|
'.sw-action-menu-list',
|
|
36
|
+
// MultiSelect's panel. Rendered only while open; on the top layer where
|
|
37
|
+
// the popover API exists.
|
|
38
|
+
'.sw-multi-select-panel',
|
|
36
39
|
'.sw-tooltip:not([hidden])',
|
|
37
40
|
// DateField and CalendarButton share it. Rendered only while open; on the
|
|
38
41
|
// top layer where the popover API exists.
|
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 { noticeClassCatalog } from './css-notice.js';
|
|
13
|
+
import { glyphTileClassCatalog } from './css-glyph-tile.js';
|
|
13
14
|
import { tooltipClassCatalog } from './css-tooltip.js';
|
|
14
15
|
import { infoTipClassCatalog } from './css-info-tip.js';
|
|
15
16
|
import { separatorClassCatalog } from './css-separator.js';
|
|
@@ -37,6 +38,7 @@ import { sectionNavClassCatalog } from './css-section-nav.js';
|
|
|
37
38
|
import { dialogClassCatalog } from './css-dialog.js';
|
|
38
39
|
import { dialogFrameClassCatalog } from './css-dialog-frame.js';
|
|
39
40
|
import { selectClassCatalog } from './css-select.js';
|
|
41
|
+
import { multiSelectClassCatalog } from './css-multi-select.js';
|
|
40
42
|
import { splitClassCatalog } from './css-split.js';
|
|
41
43
|
import { toastClassCatalog } from './css-toast.js';
|
|
42
44
|
import { toastCardClassCatalog } from './css-toast-card.js';
|
|
@@ -71,6 +73,7 @@ export function utilityClassCatalog() {
|
|
|
71
73
|
...accordionClassCatalog(),
|
|
72
74
|
...selectClassCatalog(),
|
|
73
75
|
...actionMenuClassCatalog(),
|
|
76
|
+
...multiSelectClassCatalog(),
|
|
74
77
|
...actionBarClassCatalog(),
|
|
75
78
|
...switchClassCatalog(),
|
|
76
79
|
...dateFieldClassCatalog(),
|
|
@@ -80,6 +83,7 @@ export function utilityClassCatalog() {
|
|
|
80
83
|
...spinnerClassCatalog(),
|
|
81
84
|
...progressClassCatalog(),
|
|
82
85
|
...noticeClassCatalog(),
|
|
86
|
+
...glyphTileClassCatalog(),
|
|
83
87
|
...tooltipClassCatalog(),
|
|
84
88
|
...infoTipClassCatalog(),
|
|
85
89
|
...separatorClassCatalog(),
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { DateField, type DateFieldLabels, type DateFieldProps, type WeekStart, }
|
|
|
7
7
|
export { CalendarButton, type CalendarButtonLabels, type CalendarButtonProps, type CalendarButtonRange, } from './components/CalendarButton.js';
|
|
8
8
|
export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
|
|
9
9
|
export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
|
|
10
|
+
export { GlyphTile, glyphTileSizes, type GlyphTileHue, type GlyphTileProps, type GlyphTileSize, } from './components/GlyphTile.js';
|
|
10
11
|
export { Notice, noticeTones, noticeVariants, type NoticeProps, type NoticeRole, type NoticeTone, type NoticeVariant, } from './components/Notice.js';
|
|
11
12
|
export { Progress, progressTones, type ProgressProps, type ProgressTone, } from './components/Progress.js';
|
|
12
13
|
export { Tooltip, type TooltipProps, type TooltipRelationship, } from './components/Tooltip.js';
|
|
@@ -35,6 +36,7 @@ export { SectionNav, type SectionNavItem, type SectionNavProps, } from './compon
|
|
|
35
36
|
export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, type SegmentedItem, } from './components/SegmentedControl.js';
|
|
36
37
|
export { Select } from './components/Select.js';
|
|
37
38
|
export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
|
|
39
|
+
export { MultiSelect, type MultiSelectAlign, type MultiSelectOption, type MultiSelectProps, } from './components/MultiSelect.js';
|
|
38
40
|
export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
|
|
39
41
|
export { Table, TableBody, TableCell, TableHeader, TableRow, type TableBodyProps, type TableCellProps, type TableColumnWidth, type TableDensity, type TableHeaderProps, type TableLayout, type TableProps, type TableRowProps, type TableSort, type TableVerticalAlign, } from './components/Table.js';
|
|
40
42
|
export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { DateField, } from './components/DateField.js';
|
|
|
6
6
|
export { CalendarButton, } from './components/CalendarButton.js';
|
|
7
7
|
export { Checkbox } from './components/Checkbox.js';
|
|
8
8
|
export { Spinner, } from './components/Spinner.js';
|
|
9
|
+
export { GlyphTile, glyphTileSizes, } from './components/GlyphTile.js';
|
|
9
10
|
export { Notice, noticeTones, noticeVariants, } from './components/Notice.js';
|
|
10
11
|
export { Progress, progressTones, } from './components/Progress.js';
|
|
11
12
|
export { Tooltip, } from './components/Tooltip.js';
|
|
@@ -31,6 +32,7 @@ export { Nav } from './components/Nav.js';
|
|
|
31
32
|
export { SectionNav, } from './components/SectionNav.js';
|
|
32
33
|
export { SegmentedControl, } from './components/SegmentedControl.js';
|
|
33
34
|
export { Select } from './components/Select.js';
|
|
35
|
+
export { MultiSelect, } from './components/MultiSelect.js';
|
|
34
36
|
export { Stack, } from './components/Stack.js';
|
|
35
37
|
export { Table, TableBody, TableCell, TableHeader, TableRow, } from './components/Table.js';
|
|
36
38
|
export { Text, } from './components/Text.js';
|
package/dist/styles.css
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
--sw-color-warning: #B54708;
|
|
20
20
|
--sw-color-border: #D2D2D7;
|
|
21
21
|
--sw-button-secondary-border: #D2D2D7;
|
|
22
|
+
--sw-categorical-green: #2E8F47;
|
|
23
|
+
--sw-categorical-blue: #0A7AFF;
|
|
24
|
+
--sw-categorical-orange: #D57F00;
|
|
25
|
+
--sw-categorical-purple: #8E5CD9;
|
|
26
|
+
--sw-categorical-teal: #1A96AD;
|
|
27
|
+
--sw-categorical-rose: #C2415D;
|
|
28
|
+
--sw-categorical-neutral: #8E8E93;
|
|
29
|
+
--sw-categorical-on: #FFFFFF;
|
|
22
30
|
--sw-space-0: 0px;
|
|
23
31
|
--sw-space-1: 4px;
|
|
24
32
|
--sw-space-2: 8px;
|
|
@@ -84,6 +92,14 @@
|
|
|
84
92
|
--sw-color-warning: #FDB022;
|
|
85
93
|
--sw-color-border: #3A3A3C;
|
|
86
94
|
--sw-button-secondary-border: #3A3A3C;
|
|
95
|
+
--sw-categorical-green: #2E8F47;
|
|
96
|
+
--sw-categorical-blue: #0A7AFF;
|
|
97
|
+
--sw-categorical-orange: #D57F00;
|
|
98
|
+
--sw-categorical-purple: #8E5CD9;
|
|
99
|
+
--sw-categorical-teal: #1A96AD;
|
|
100
|
+
--sw-categorical-rose: #C2415D;
|
|
101
|
+
--sw-categorical-neutral: #8E8E93;
|
|
102
|
+
--sw-categorical-on: #FFFFFF;
|
|
87
103
|
--sw-space-0: 0px;
|
|
88
104
|
--sw-space-1: 4px;
|
|
89
105
|
--sw-space-2: 8px;
|
|
@@ -812,8 +828,8 @@ body {
|
|
|
812
828
|
}
|
|
813
829
|
|
|
814
830
|
/* Backdrop blur makes a glass surface its own stacking context, so it lifts while it holds an open popup (see stacking.ts). */
|
|
815
|
-
.sw-card-glass:has(.sw-select-list, .sw-action-menu-list, .sw-tooltip:not([hidden]), .sw-date-field-calendar),
|
|
816
|
-
.sw-accordion:has(.sw-select-list, .sw-action-menu-list, .sw-tooltip:not([hidden]), .sw-date-field-calendar) {
|
|
831
|
+
.sw-card-glass:has(.sw-select-list, .sw-action-menu-list, .sw-multi-select-panel, .sw-tooltip:not([hidden]), .sw-date-field-calendar),
|
|
832
|
+
.sw-accordion:has(.sw-select-list, .sw-action-menu-list, .sw-multi-select-panel, .sw-tooltip:not([hidden]), .sw-date-field-calendar) {
|
|
817
833
|
position: relative;
|
|
818
834
|
z-index: 3;
|
|
819
835
|
}
|
|
@@ -1348,6 +1364,45 @@ body {
|
|
|
1348
1364
|
color: var(--sw-color-onAccent);
|
|
1349
1365
|
}
|
|
1350
1366
|
|
|
1367
|
+
.sw-segmented.sw-segmented-with-icons:not(.sw-segmented-filled) {
|
|
1368
|
+
background: var(--sw-color-subtle);
|
|
1369
|
+
border-color: transparent;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
.sw-segmented-with-icons:not(.sw-segmented-filled) .sw-segmented-item-selected {
|
|
1373
|
+
box-shadow: var(--sw-elevation-sm);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.sw-segmented-with-icons .sw-segmented-item {
|
|
1377
|
+
align-items: center;
|
|
1378
|
+
display: inline-flex;
|
|
1379
|
+
gap: var(--sw-space-2);
|
|
1380
|
+
justify-content: center;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.sw-segmented-with-icons:not(.sw-segmented-filled) .sw-segmented-item {
|
|
1384
|
+
font-size: 15px;
|
|
1385
|
+
letter-spacing: -0.2px;
|
|
1386
|
+
line-height: 20px;
|
|
1387
|
+
min-height: var(--sw-control-md-min-height);
|
|
1388
|
+
padding-inline: var(--sw-space-2) var(--sw-space-3);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.sw-segmented-item-icon {
|
|
1392
|
+
display: inline-flex;
|
|
1393
|
+
flex: none;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.sw-segmented.sw-segmented-with-icons[aria-invalid='true'] {
|
|
1397
|
+
border-color: var(--sw-color-danger);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
@media (forced-colors: active) {
|
|
1401
|
+
.sw-segmented.sw-segmented-with-icons:not(.sw-segmented-filled) { border-color: CanvasText; }
|
|
1402
|
+
.sw-segmented-with-icons:not(.sw-segmented-filled) .sw-segmented-item-selected { outline: 2px solid Highlight; }
|
|
1403
|
+
.sw-segmented.sw-segmented-with-icons[aria-invalid='true'] { border-color: Mark; }
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1351
1406
|
.sw-segmented-disabled {
|
|
1352
1407
|
opacity: var(--sw-disabled-opacity);
|
|
1353
1408
|
}
|
|
@@ -2717,6 +2772,138 @@ body {
|
|
|
2717
2772
|
}
|
|
2718
2773
|
}
|
|
2719
2774
|
|
|
2775
|
+
.sw-multi-select {
|
|
2776
|
+
display: inline-flex;
|
|
2777
|
+
max-width: 100%;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
.sw-multi-select-trigger {
|
|
2781
|
+
align-items: center;
|
|
2782
|
+
appearance: none;
|
|
2783
|
+
background-color: var(--sw-glass-fill);
|
|
2784
|
+
border: 1px solid var(--sw-color-border);
|
|
2785
|
+
border-radius: var(--sw-radius-sm);
|
|
2786
|
+
box-sizing: border-box;
|
|
2787
|
+
color: var(--sw-color-text);
|
|
2788
|
+
column-gap: var(--sw-space-2);
|
|
2789
|
+
cursor: pointer;
|
|
2790
|
+
display: inline-flex;
|
|
2791
|
+
font-family: var(--sw-font-sans);
|
|
2792
|
+
font-size: 15px;
|
|
2793
|
+
font-weight: 600;
|
|
2794
|
+
letter-spacing: -0.2px;
|
|
2795
|
+
line-height: 20px;
|
|
2796
|
+
margin: 0;
|
|
2797
|
+
max-width: 100%;
|
|
2798
|
+
min-height: var(--sw-control-md-min-height);
|
|
2799
|
+
padding-inline: var(--sw-control-md-padding-inline);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
.sw-multi-select-trigger::after {
|
|
2803
|
+
border-bottom: 2px solid var(--sw-color-muted);
|
|
2804
|
+
border-right: 2px solid var(--sw-color-muted);
|
|
2805
|
+
box-sizing: border-box;
|
|
2806
|
+
height: var(--sw-space-2);
|
|
2807
|
+
width: var(--sw-space-2);
|
|
2808
|
+
content: '';
|
|
2809
|
+
flex: none;
|
|
2810
|
+
margin-inline-start: var(--sw-space-2);
|
|
2811
|
+
transform: translateY(-25%) rotate(45deg);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
.sw-multi-select-trigger:focus-visible {
|
|
2815
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
2816
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
.sw-multi-select-trigger:disabled {
|
|
2820
|
+
cursor: not-allowed;
|
|
2821
|
+
opacity: var(--sw-disabled-opacity);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
.sw-multi-select-label {
|
|
2825
|
+
color: var(--sw-color-muted);
|
|
2826
|
+
font-weight: 500;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
.sw-multi-select-summary {
|
|
2830
|
+
min-width: 0;
|
|
2831
|
+
overflow: hidden;
|
|
2832
|
+
text-overflow: ellipsis;
|
|
2833
|
+
white-space: nowrap;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
.sw-multi-select-panel {
|
|
2837
|
+
background: var(--sw-glass-fill);
|
|
2838
|
+
border: 1px solid var(--sw-glass-border);
|
|
2839
|
+
border-radius: var(--sw-radius-sm);
|
|
2840
|
+
box-shadow: var(--sw-elevation-sm), inset 0 1px 0 var(--sw-glass-specular);
|
|
2841
|
+
backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
2842
|
+
-webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
2843
|
+
box-sizing: border-box;
|
|
2844
|
+
color: var(--sw-color-text);
|
|
2845
|
+
display: flex;
|
|
2846
|
+
flex-direction: column;
|
|
2847
|
+
inset: auto;
|
|
2848
|
+
margin: 0;
|
|
2849
|
+
/* The viewport, not --sw-select-max: the list is short and should show
|
|
2850
|
+
whole rather than scroll inside a 22rem cap. */
|
|
2851
|
+
max-height: calc(100% - var(--sw-space-2) - var(--sw-space-2));
|
|
2852
|
+
max-width: calc(100% - var(--sw-space-2) - var(--sw-space-2));
|
|
2853
|
+
min-width: var(--sw-select-max);
|
|
2854
|
+
overflow: auto;
|
|
2855
|
+
padding: var(--sw-space-1);
|
|
2856
|
+
position: fixed;
|
|
2857
|
+
width: max-content;
|
|
2858
|
+
z-index: 10;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
.sw-multi-select-commands {
|
|
2862
|
+
border-bottom: 1px solid var(--sw-color-border);
|
|
2863
|
+
display: flex;
|
|
2864
|
+
justify-content: space-between;
|
|
2865
|
+
margin-bottom: var(--sw-space-1);
|
|
2866
|
+
padding-bottom: var(--sw-space-1);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
.sw-multi-select-row-separated {
|
|
2870
|
+
border-top: 1px solid var(--sw-color-border);
|
|
2871
|
+
margin-top: var(--sw-space-1);
|
|
2872
|
+
padding-top: var(--sw-space-1);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
.sw-checkbox-label.sw-multi-select-option {
|
|
2876
|
+
border-radius: var(--sw-radius-sm);
|
|
2877
|
+
box-sizing: border-box;
|
|
2878
|
+
font-family: var(--sw-font-sans);
|
|
2879
|
+
font-size: 15px;
|
|
2880
|
+
font-weight: 400;
|
|
2881
|
+
letter-spacing: -0.2px;
|
|
2882
|
+
line-height: 20px;
|
|
2883
|
+
padding-inline: var(--sw-space-2);
|
|
2884
|
+
width: 100%;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
.sw-multi-select-option:hover {
|
|
2888
|
+
background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
.sw-multi-select-icon {
|
|
2892
|
+
display: inline-flex;
|
|
2893
|
+
flex: none;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
.sw-multi-select-text {
|
|
2897
|
+
min-width: 0;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
@media (forced-colors: active) {
|
|
2901
|
+
.sw-multi-select-trigger::after { border-color: CanvasText; }
|
|
2902
|
+
.sw-multi-select-panel { border-color: CanvasText; }
|
|
2903
|
+
.sw-multi-select-commands,
|
|
2904
|
+
.sw-multi-select-row-separated { border-color: CanvasText; }
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2720
2907
|
.sw-action-bar {
|
|
2721
2908
|
align-items: center;
|
|
2722
2909
|
background: var(--sw-glass-fill);
|
|
@@ -3126,6 +3313,39 @@ body {
|
|
|
3126
3313
|
.sw-notice-band { border-width: 0 0 1px; }
|
|
3127
3314
|
}
|
|
3128
3315
|
|
|
3316
|
+
.sw-glyph-tile {
|
|
3317
|
+
align-items: center;
|
|
3318
|
+
background: var(--sw-glyph-tile-fill);
|
|
3319
|
+
border-radius: var(--sw-radius-sm);
|
|
3320
|
+
box-sizing: border-box;
|
|
3321
|
+
color: var(--sw-categorical-on);
|
|
3322
|
+
display: inline-flex;
|
|
3323
|
+
flex: none;
|
|
3324
|
+
justify-content: center;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
.sw-glyph-tile > svg {
|
|
3328
|
+
flex: none;
|
|
3329
|
+
height: var(--sw-space-4);
|
|
3330
|
+
width: var(--sw-space-4);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
.sw-glyph-tile-green { --sw-glyph-tile-fill: var(--sw-categorical-green); }
|
|
3334
|
+
.sw-glyph-tile-blue { --sw-glyph-tile-fill: var(--sw-categorical-blue); }
|
|
3335
|
+
.sw-glyph-tile-orange { --sw-glyph-tile-fill: var(--sw-categorical-orange); }
|
|
3336
|
+
.sw-glyph-tile-purple { --sw-glyph-tile-fill: var(--sw-categorical-purple); }
|
|
3337
|
+
.sw-glyph-tile-teal { --sw-glyph-tile-fill: var(--sw-categorical-teal); }
|
|
3338
|
+
.sw-glyph-tile-rose { --sw-glyph-tile-fill: var(--sw-categorical-rose); }
|
|
3339
|
+
.sw-glyph-tile-neutral { --sw-glyph-tile-fill: var(--sw-categorical-neutral); }
|
|
3340
|
+
|
|
3341
|
+
.sw-glyph-tile-sm { height: var(--sw-space-5); width: var(--sw-space-5); }
|
|
3342
|
+
.sw-glyph-tile-md { height: var(--sw-control-xs-min-height); width: var(--sw-control-xs-min-height); }
|
|
3343
|
+
.sw-glyph-tile-lg { height: var(--sw-space-6); width: var(--sw-space-6); }
|
|
3344
|
+
|
|
3345
|
+
@media (forced-colors: active) {
|
|
3346
|
+
.sw-glyph-tile { border: 1px solid CanvasText; color: CanvasText; }
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3129
3349
|
.sw-tooltip-anchor {
|
|
3130
3350
|
display: inline-flex;
|
|
3131
3351
|
position: relative;
|
|
@@ -4624,6 +4844,7 @@ body {
|
|
|
4624
4844
|
.sw-accordion,
|
|
4625
4845
|
.sw-select-list,
|
|
4626
4846
|
.sw-action-menu-list,
|
|
4847
|
+
.sw-multi-select-panel,
|
|
4627
4848
|
.sw-date-field-calendar,
|
|
4628
4849
|
.sw-toast {
|
|
4629
4850
|
background: var(--sw-color-surface);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalewing/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Scalewing DOM components and package-owned CSS utilities.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@scalewing/tokens": "^1.
|
|
41
|
+
"@scalewing/tokens": "^1.5.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^19.0.0",
|