@scalewing/react 1.13.0 → 1.15.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 +12 -0
- package/README.md +36 -0
- package/dist/components/RadioGroup.d.ts +2 -12
- package/dist/components/RadioGroup.js +5 -6
- package/dist/components/Select.d.ts +13 -1
- package/dist/components/Select.js +2 -2
- package/dist/components/radio-group/RadioGroupOptionLabel.d.ts +39 -0
- package/dist/components/radio-group/RadioGroupOptionLabel.js +26 -0
- package/dist/css/css-radio-group.js +56 -0
- package/dist/css/css-select.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +70 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c0a1c95: `RadioGroup` options take a `description` (`docs/requests/teisoro-radio-group.md`, 2026-09-29 follow-up, Teisoro F-007-S05 task 1365). `RadioGroupOption.description?: ReactNode` is secondary text for that option: a muted caption at the row's inline end while it fits beside the label, and a second line under the label text when it does not or below `md` (new generated classes `sw-radio-group-body` and `sw-radio-group-option-description`). It is the radio's accessible description (`aria-describedby`), the radio's name stays its `label` text (`aria-labelledby` on the label text), and a press on it chooses the option. It may hold phrasing content such as a `Badge`, nothing interactive; a taller description grows the row downward and the label stays on the radio's line. `undefined`, `null`, booleans and `''` are no description; `0` and a component that renders nothing are one. An option with a description fills the group's width. Without `description` nothing changes. No new dependencies.
|
|
8
|
+
|
|
9
|
+
## 1.14.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 84d5fd9: `Select` takes `width?: 'content' | 'full'` (`docs/requests/teisoro-select.md`, 2026-09-29 follow-up). `'content'`, the default, is today's field, as wide as its longest option; nothing changes for existing Selects. `'full'` adds the generated `sw-select-full` class: the field and its trigger fill the container's inline size in a `Stack`, a `Grid` cell or a narrow column, and in an `Inline` row they take the space the siblings leave (`.sw-inline > .sw-select-full { flex: 1 1 0; min-width: 0 }`), so a `Button` beside it keeps its label on one line. The value still ellipsizes, the chevron stays at the inline end (left to right and right to left), and the open list is exactly the trigger's width, so a long option wraps inside it, even a single long word (`overflow-wrap: anywhere`), instead of running past a phone's edge or scrolling sideways. New exported type `SelectWidth`. No new dependencies.
|
|
14
|
+
|
|
3
15
|
## 1.13.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -78,6 +78,18 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
78
78
|
/>
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
`Select` `width` is `'content'` (the default: the field is as wide as its longest option, as above) or `'full'`: the field and its trigger fill the container's inline size in a `Stack`, a `Grid` cell or any narrow column, such as a filter above full-width cards on a phone. In an `Inline` row it takes the space its siblings leave, so a `Button` beside it keeps its label on one line. The value still takes the free space and ellipsizes past it, the chevron stays at the inline end (the left, right to left), and the open list is exactly the trigger's width, so it never runs past the screen's edge; a long option wraps inside it, even a single long word.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Select
|
|
85
|
+
label="Show"
|
|
86
|
+
onChange={setFilter}
|
|
87
|
+
options={filters}
|
|
88
|
+
value={filter}
|
|
89
|
+
width="full"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
81
93
|
`Accordion` is a native `<details>` disclosure controlled by `open` and `onOpenChange`. A token chevron at the end of the header replaces the browser triangle and turns when it opens (it holds still under reduced motion). `subtitle` adds one muted caption line under the title, such as a summary of what the section holds; it wraps rather than truncating and is read after the title. `size="sm"` is a quieter disclosure nested inside other content: a label-size title, tighter padding, smaller corners and a header that is an `sm` control (at least 32px).
|
|
82
94
|
|
|
83
95
|
```tsx
|
|
@@ -140,6 +152,30 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
140
152
|
/>
|
|
141
153
|
```
|
|
142
154
|
|
|
155
|
+
An option's optional `description` (a `ReactNode`) is secondary text for that option alone, such as its history. It is a muted caption after the label, at the row's inline end, while the two fit on one line; when they do not, and always below the `md` breakpoint, it wraps to a second line that starts under the label text, not under the radio. The label keeps the first line. An option with a description fills the group's width. The description is the radio's accessible description (`aria-describedby` on that option's input), while its name stays the `label` text, and a press on it chooses the option as the label does. A disabled option fades its description with the rest of the option. It may hold phrasing content such as a small `Badge`, but nothing interactive, because it sits inside the option's `<label>`. A description taller than the label, such as a default-size `Badge`, grows the row downward; the label stays on the radio's line. `undefined`, `null`, `false`, `true` and `''` are no description. Any other node is one, including `0` and a component that renders nothing, so pass `undefined` when there is nothing to say. The group's own `description` still describes the whole group.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<RadioGroup
|
|
159
|
+
legend="Sighting source"
|
|
160
|
+
onChange={setSource}
|
|
161
|
+
options={[
|
|
162
|
+
{
|
|
163
|
+
value: 'observer',
|
|
164
|
+
label: 'Field observer',
|
|
165
|
+
icon: <User />,
|
|
166
|
+
description: <Badge size="sm">Most recent</Badge>,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
value: 'camera',
|
|
170
|
+
label: 'Camera trap',
|
|
171
|
+
icon: <Camera />,
|
|
172
|
+
description: '2 sightings · last Sep 13, 2026',
|
|
173
|
+
},
|
|
174
|
+
]}
|
|
175
|
+
value={source}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
143
179
|
`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.
|
|
144
180
|
|
|
145
181
|
```tsx
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
export type RadioGroupOption
|
|
3
|
-
value: string;
|
|
4
|
-
label: string;
|
|
5
|
-
disabled?: boolean;
|
|
6
|
-
/**
|
|
7
|
-
* A decorative glyph between the radio and the label, such as a person or
|
|
8
|
-
* a building for the kind of check. It is `aria-hidden` and drawn in the
|
|
9
|
-
* text color: the option's accessible name stays its `label` text.
|
|
10
|
-
*/
|
|
11
|
-
icon?: ReactNode;
|
|
12
|
-
};
|
|
1
|
+
import { type RadioGroupOption } from './radio-group/RadioGroupOptionLabel.js';
|
|
2
|
+
export type { RadioGroupOption };
|
|
13
3
|
export type RadioGroupProps = {
|
|
14
4
|
legend: string;
|
|
15
5
|
value: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
5
|
+
import { RadioGroupOptionLabel, } from './radio-group/RadioGroupOptionLabel.js';
|
|
5
6
|
export function RadioGroup({ legend, value, onChange, options, required = false, disabled = false, description, error, }) {
|
|
6
7
|
const name = useId();
|
|
7
8
|
const descriptionId = useId();
|
|
@@ -19,10 +20,8 @@ export function RadioGroup({ legend, value, onChange, options, required = false,
|
|
|
19
20
|
throw new RangeError('options must have unique values');
|
|
20
21
|
if (value !== '' && !values.includes(value))
|
|
21
22
|
throw new RangeError('value must match an option or be empty');
|
|
22
|
-
return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), className: "sw-radio-group", disabled: disabled, children: [_jsx("legend", { className: "sw-radio-group-legend", children: legend }), description ? (_jsx("span", { className: "sw-radio-group-description", id: descriptionId, children: description })) : null, _jsx("span", { className: "sw-radio-group-options", children: options.map((option) => (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
onChange(option.value);
|
|
27
|
-
}, required: required, type: "radio", value: option.value }), _jsx("span", { "aria-hidden": "true", className: "sw-radio-group-mark" })] }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-radio-group-icon", children: option.icon })) : null, _jsx("span", { className: "sw-radio-group-text", children: option.label })] }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-radio-group-error", id: errorId, message: error })] }));
|
|
23
|
+
return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), className: "sw-radio-group", disabled: disabled, children: [_jsx("legend", { className: "sw-radio-group-legend", children: legend }), description ? (_jsx("span", { className: "sw-radio-group-description", id: descriptionId, children: description })) : null, _jsx("span", { className: "sw-radio-group-options", children: options.map((option) => (_jsx(RadioGroupOptionLabel, { checked: value === option.value, name: name, onSelect: () => {
|
|
24
|
+
if (!disabled && !option.disabled)
|
|
25
|
+
onChange(option.value);
|
|
26
|
+
}, option: option, required: required }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-radio-group-error", id: errorId, message: error })] }));
|
|
28
27
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type SelectOption } from '../select-list.js';
|
|
2
2
|
import { type FieldSize } from './Field.js';
|
|
3
3
|
export type { SelectOption };
|
|
4
|
+
/**
|
|
5
|
+
* `content` (the default) sizes the field to its longest option, as a native
|
|
6
|
+
* select is; `full` fills the container's inline size (in an Inline row, the
|
|
7
|
+
* space its siblings leave).
|
|
8
|
+
*/
|
|
9
|
+
export type SelectWidth = 'content' | 'full';
|
|
4
10
|
export type SelectAction = {
|
|
5
11
|
label: string;
|
|
6
12
|
onPress: () => void;
|
|
@@ -22,5 +28,11 @@ export type SelectProps = {
|
|
|
22
28
|
required?: boolean;
|
|
23
29
|
/** A validation message under the control, wired as Field's error is. */
|
|
24
30
|
error?: string;
|
|
31
|
+
/**
|
|
32
|
+
* `full` makes the field, its trigger and its open list fill the
|
|
33
|
+
* container's inline size; the chosen label still ellipsizes past it.
|
|
34
|
+
* Default `content`.
|
|
35
|
+
*/
|
|
36
|
+
width?: SelectWidth;
|
|
25
37
|
};
|
|
26
|
-
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, }: SelectProps): import("react").JSX.Element;
|
|
38
|
+
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, width, }: SelectProps): import("react").JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { SelectListbox } from './select/SelectListbox.js';
|
|
|
9
9
|
import { SelectValue } from './select/SelectValue.js';
|
|
10
10
|
import { useSelectList } from './select/use-select-list.js';
|
|
11
11
|
import { Stack } from './Stack.js';
|
|
12
|
-
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, }) {
|
|
12
|
+
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, width = 'content', }) {
|
|
13
13
|
if (placeholder !== undefined && !placeholder.trim())
|
|
14
14
|
throw new RangeError('placeholder must not be empty');
|
|
15
15
|
const labelId = useId();
|
|
@@ -22,7 +22,7 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
|
|
|
22
22
|
const showsPlaceholder = placeholder !== undefined &&
|
|
23
23
|
!options.some((option) => option.value === value);
|
|
24
24
|
const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
|
|
25
|
-
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
25
|
+
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
26
26
|
const next = event.relatedTarget;
|
|
27
27
|
if (next instanceof Node && rootRef.current?.contains(next)) {
|
|
28
28
|
return;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type RadioGroupOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* A decorative glyph between the radio and the label, such as a person or
|
|
8
|
+
* a building for the kind of check. It is `aria-hidden` and drawn in the
|
|
9
|
+
* text color: the option's accessible name stays its `label` text.
|
|
10
|
+
*/
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Secondary text for this option, such as its history: a muted caption at
|
|
14
|
+
* the row's inline end, or on a second line under the label when the row
|
|
15
|
+
* is narrower than md or the two do not fit. It is the radio's accessible
|
|
16
|
+
* description (`aria-describedby`), never part of its name, and a press on
|
|
17
|
+
* it chooses the option. It may hold phrasing content such as a `Badge`,
|
|
18
|
+
* but nothing interactive: it sits inside the option's `<label>`.
|
|
19
|
+
* `undefined`, `null`, `false`, `true` and `''` are no description; any
|
|
20
|
+
* other node, `0` or a component that renders nothing included, is one,
|
|
21
|
+
* so pass `undefined` when there is nothing to say.
|
|
22
|
+
*/
|
|
23
|
+
description?: ReactNode;
|
|
24
|
+
};
|
|
25
|
+
type RadioGroupOptionLabelProps = {
|
|
26
|
+
option: RadioGroupOption;
|
|
27
|
+
name: string;
|
|
28
|
+
checked: boolean;
|
|
29
|
+
required: boolean;
|
|
30
|
+
onSelect: () => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* One option of a RadioGroup: the native radio and its mark, the optional
|
|
34
|
+
* glyph, and the label. With a description, the label and the description
|
|
35
|
+
* share a wrapping body, and the radio is named by the label text alone and
|
|
36
|
+
* described by the description.
|
|
37
|
+
*/
|
|
38
|
+
export declare function RadioGroupOptionLabel({ option, name, checked, required, onSelect, }: RadioGroupOptionLabelProps): import("react").JSX.Element;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
/** Whether React renders something for a description: not nothing, a boolean or ''. */
|
|
4
|
+
function hasDescription(description) {
|
|
5
|
+
return (description !== undefined &&
|
|
6
|
+
description !== null &&
|
|
7
|
+
typeof description !== 'boolean' &&
|
|
8
|
+
description !== '');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One option of a RadioGroup: the native radio and its mark, the optional
|
|
12
|
+
* glyph, and the label. With a description, the label and the description
|
|
13
|
+
* share a wrapping body, and the radio is named by the label text alone and
|
|
14
|
+
* described by the description.
|
|
15
|
+
*/
|
|
16
|
+
export function RadioGroupOptionLabel({ option, name, checked, required, onSelect, }) {
|
|
17
|
+
const id = useId();
|
|
18
|
+
const textId = `${id}-text`;
|
|
19
|
+
const descriptionId = `${id}-description`;
|
|
20
|
+
const described = hasDescription(option.description);
|
|
21
|
+
const text = (_jsx("span", { className: "sw-radio-group-text", id: described ? textId : undefined, children: option.label }));
|
|
22
|
+
return (_jsxs("label", { className: "sw-radio-group-option", children: [_jsxs("span", { className: "sw-radio-group-control", children: [_jsx("input", { "aria-describedby": described ? descriptionId : undefined, "aria-labelledby": described ? textId : undefined, checked: checked, className: "sw-radio-group-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
23
|
+
if (event.currentTarget.checked)
|
|
24
|
+
onSelect();
|
|
25
|
+
}, required: required, type: "radio", value: option.value }), _jsx("span", { "aria-hidden": "true", className: "sw-radio-group-mark" })] }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-radio-group-icon", children: option.icon })) : null, described ? (_jsxs("span", { className: "sw-radio-group-body", children: [text, _jsx("span", { className: "sw-radio-group-option-description", id: descriptionId, children: option.description })] })) : (text)] }));
|
|
26
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
3
|
const label = typographyVariants.label;
|
|
3
4
|
const caption = typographyVariants.caption;
|
|
4
5
|
export function cssRadioGroupClasses() {
|
|
@@ -118,6 +119,8 @@ export function cssRadioGroupClasses() {
|
|
|
118
119
|
min-width: 0;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
${optionDescriptionRules()}
|
|
123
|
+
|
|
121
124
|
.sw-radio-group-description,
|
|
122
125
|
.sw-radio-group-error {
|
|
123
126
|
font-family: var(--sw-font-sans);
|
|
@@ -141,6 +144,57 @@ export function cssRadioGroupClasses() {
|
|
|
141
144
|
.sw-radio-group[aria-invalid='true'] .sw-radio-group-mark { border-color: CanvasText; }
|
|
142
145
|
}`;
|
|
143
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* An option with a description fills the group's width. Its label and
|
|
149
|
+
* description share a wrapping body: the label grows, so the description sits
|
|
150
|
+
* at the row's inline end while both fit, and wraps to its own line, starting
|
|
151
|
+
* under the label text, when they do not. The label keeps the row first.
|
|
152
|
+
* Below md the description always takes its own line. The radio and glyph
|
|
153
|
+
* sit on the label's line, which starts where a one-line option centers it.
|
|
154
|
+
* Items align to the line's start, not a shared baseline, so a description
|
|
155
|
+
* taller than the label (a default-size Badge) grows the row downward and
|
|
156
|
+
* never pushes the label off the radio's line; a caption line is centred
|
|
157
|
+
* on the label's line box.
|
|
158
|
+
*/
|
|
159
|
+
function optionDescriptionRules() {
|
|
160
|
+
return `.sw-radio-group-option:has(> .sw-radio-group-body) {
|
|
161
|
+
align-items: flex-start;
|
|
162
|
+
box-sizing: border-box;
|
|
163
|
+
padding-block-start: calc((var(--sw-control-md-min-height) - var(--sw-space-5)) / 2);
|
|
164
|
+
width: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sw-radio-group-option:has(> .sw-radio-group-body) > .sw-radio-group-icon {
|
|
168
|
+
min-height: var(--sw-space-5);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.sw-radio-group-body {
|
|
172
|
+
align-items: flex-start;
|
|
173
|
+
column-gap: var(--sw-space-3);
|
|
174
|
+
display: flex;
|
|
175
|
+
flex: 1 1 0;
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
padding-block-start: calc((var(--sw-space-5) - ${label.lineHeight}px) / 2);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.sw-radio-group-body > .sw-radio-group-text { flex: 1 1 auto; }
|
|
182
|
+
|
|
183
|
+
.sw-radio-group-option-description {
|
|
184
|
+
color: var(--sw-color-muted);
|
|
185
|
+
flex: 0 1 auto;
|
|
186
|
+
font-family: var(--sw-font-sans);
|
|
187
|
+
font-size: ${caption.fontSize}px;
|
|
188
|
+
font-weight: ${caption.fontWeight};
|
|
189
|
+
line-height: ${caption.lineHeight}px;
|
|
190
|
+
margin-block-start: calc((${label.lineHeight}px - ${caption.lineHeight}px) / 2);
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@media ${breakpointQuery('below', 'md')} {
|
|
195
|
+
.sw-radio-group-option-description { flex-basis: 100%; }
|
|
196
|
+
}`;
|
|
197
|
+
}
|
|
144
198
|
export function radioGroupClassCatalog() {
|
|
145
199
|
return [
|
|
146
200
|
'sw-radio-group',
|
|
@@ -152,6 +206,8 @@ export function radioGroupClassCatalog() {
|
|
|
152
206
|
'sw-radio-group-mark',
|
|
153
207
|
'sw-radio-group-icon',
|
|
154
208
|
'sw-radio-group-text',
|
|
209
|
+
'sw-radio-group-body',
|
|
210
|
+
'sw-radio-group-option-description',
|
|
155
211
|
'sw-radio-group-description',
|
|
156
212
|
'sw-radio-group-error',
|
|
157
213
|
];
|
package/dist/css/css-select.js
CHANGED
|
@@ -9,6 +9,25 @@ export function cssSelectClasses() {
|
|
|
9
9
|
width: max-content;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
* width="full": the field and its trigger fill the container's inline size.
|
|
14
|
+
* The value still takes the free space and ellipsizes, and the chevron stays
|
|
15
|
+
* last, at the inline end. In an Inline row the field takes only the space
|
|
16
|
+
* its siblings leave, so a Button beside it keeps its label on one line.
|
|
17
|
+
*/
|
|
18
|
+
.sw-select-full {
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.sw-inline > .sw-select-full {
|
|
23
|
+
flex: 1 1 0;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sw-select-full .sw-select-trigger {
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
.sw-select-control {
|
|
13
32
|
position: relative;
|
|
14
33
|
}
|
|
@@ -125,6 +144,15 @@ export function cssSelectClasses() {
|
|
|
125
144
|
${zIndex('popup')}
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
/*
|
|
148
|
+
* A full-width field's open list is exactly the trigger's width (min-width
|
|
149
|
+
* already beats the dialog-max cap), so it never runs past a phone's edge;
|
|
150
|
+
* a long option wraps inside it, even a single long word.
|
|
151
|
+
*/
|
|
152
|
+
.sw-select-full .sw-select-list {
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
.sw-select-option {
|
|
129
157
|
border-radius: var(--sw-radius-sm);
|
|
130
158
|
box-sizing: border-box;
|
|
@@ -135,6 +163,10 @@ export function cssSelectClasses() {
|
|
|
135
163
|
padding-inline: var(--sw-control-xs-padding-inline);
|
|
136
164
|
}
|
|
137
165
|
|
|
166
|
+
.sw-select-full .sw-select-option {
|
|
167
|
+
overflow-wrap: anywhere;
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
.sw-select-option[aria-selected='true'] {
|
|
139
171
|
font-weight: ${label.fontWeight};
|
|
140
172
|
}
|
|
@@ -159,6 +191,7 @@ export function selectClassCatalog() {
|
|
|
159
191
|
return [
|
|
160
192
|
'sw-select',
|
|
161
193
|
'sw-select-xs',
|
|
194
|
+
'sw-select-full',
|
|
162
195
|
'sw-select-control',
|
|
163
196
|
'sw-select-trigger',
|
|
164
197
|
'sw-select-value',
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { SplitProps } from './components/Split.js';
|
|
|
29
29
|
export { Nav, type NavProps } from './components/Nav.js';
|
|
30
30
|
export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, type SegmentedItem, } from './components/SegmentedControl.js';
|
|
31
31
|
export { Select } from './components/Select.js';
|
|
32
|
-
export type { SelectAction, SelectOption, SelectProps, } from './components/Select.js';
|
|
32
|
+
export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
|
|
33
33
|
export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
|
|
34
34
|
export { Table, TableBody, TableCell, TableHeader, TableRow, type TableBodyProps, type TableCellProps, type TableDensity, type TableHeaderProps, type TableProps, type TableRowProps, } from './components/Table.js';
|
|
35
35
|
export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
|
package/dist/styles.css
CHANGED
|
@@ -1125,6 +1125,44 @@ body {
|
|
|
1125
1125
|
min-width: 0;
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
1128
|
+
.sw-radio-group-option:has(> .sw-radio-group-body) {
|
|
1129
|
+
align-items: flex-start;
|
|
1130
|
+
box-sizing: border-box;
|
|
1131
|
+
padding-block-start: calc((var(--sw-control-md-min-height) - var(--sw-space-5)) / 2);
|
|
1132
|
+
width: 100%;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.sw-radio-group-option:has(> .sw-radio-group-body) > .sw-radio-group-icon {
|
|
1136
|
+
min-height: var(--sw-space-5);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.sw-radio-group-body {
|
|
1140
|
+
align-items: flex-start;
|
|
1141
|
+
column-gap: var(--sw-space-3);
|
|
1142
|
+
display: flex;
|
|
1143
|
+
flex: 1 1 0;
|
|
1144
|
+
flex-wrap: wrap;
|
|
1145
|
+
min-width: 0;
|
|
1146
|
+
padding-block-start: calc((var(--sw-space-5) - 20px) / 2);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.sw-radio-group-body > .sw-radio-group-text { flex: 1 1 auto; }
|
|
1150
|
+
|
|
1151
|
+
.sw-radio-group-option-description {
|
|
1152
|
+
color: var(--sw-color-muted);
|
|
1153
|
+
flex: 0 1 auto;
|
|
1154
|
+
font-family: var(--sw-font-sans);
|
|
1155
|
+
font-size: 13px;
|
|
1156
|
+
font-weight: 400;
|
|
1157
|
+
line-height: 18px;
|
|
1158
|
+
margin-block-start: calc((20px - 18px) / 2);
|
|
1159
|
+
min-width: 0;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
@media not all and (min-width: 48rem) {
|
|
1163
|
+
.sw-radio-group-option-description { flex-basis: 100%; }
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1128
1166
|
.sw-radio-group-description,
|
|
1129
1167
|
.sw-radio-group-error {
|
|
1130
1168
|
font-family: var(--sw-font-sans);
|
|
@@ -1912,6 +1950,25 @@ body {
|
|
|
1912
1950
|
width: max-content;
|
|
1913
1951
|
}
|
|
1914
1952
|
|
|
1953
|
+
/*
|
|
1954
|
+
* width="full": the field and its trigger fill the container's inline size.
|
|
1955
|
+
* The value still takes the free space and ellipsizes, and the chevron stays
|
|
1956
|
+
* last, at the inline end. In an Inline row the field takes only the space
|
|
1957
|
+
* its siblings leave, so a Button beside it keeps its label on one line.
|
|
1958
|
+
*/
|
|
1959
|
+
.sw-select-full {
|
|
1960
|
+
width: 100%;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.sw-inline > .sw-select-full {
|
|
1964
|
+
flex: 1 1 0;
|
|
1965
|
+
min-width: 0;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
.sw-select-full .sw-select-trigger {
|
|
1969
|
+
width: 100%;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1915
1972
|
.sw-select-control {
|
|
1916
1973
|
position: relative;
|
|
1917
1974
|
}
|
|
@@ -2032,6 +2089,15 @@ body {
|
|
|
2032
2089
|
z-index: 10;
|
|
2033
2090
|
}
|
|
2034
2091
|
|
|
2092
|
+
/*
|
|
2093
|
+
* A full-width field's open list is exactly the trigger's width (min-width
|
|
2094
|
+
* already beats the dialog-max cap), so it never runs past a phone's edge;
|
|
2095
|
+
* a long option wraps inside it, even a single long word.
|
|
2096
|
+
*/
|
|
2097
|
+
.sw-select-full .sw-select-list {
|
|
2098
|
+
width: 100%;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2035
2101
|
.sw-select-option {
|
|
2036
2102
|
border-radius: var(--sw-radius-sm);
|
|
2037
2103
|
box-sizing: border-box;
|
|
@@ -2042,6 +2108,10 @@ body {
|
|
|
2042
2108
|
padding-inline: var(--sw-control-xs-padding-inline);
|
|
2043
2109
|
}
|
|
2044
2110
|
|
|
2111
|
+
.sw-select-full .sw-select-option {
|
|
2112
|
+
overflow-wrap: anywhere;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2045
2115
|
.sw-select-option[aria-selected='true'] {
|
|
2046
2116
|
font-weight: 600;
|
|
2047
2117
|
}
|