@scalewing/react 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
3
9
  ## 1.13.0
4
10
 
5
11
  ### 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
@@ -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;
@@ -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
@@ -1912,6 +1912,25 @@ body {
1912
1912
  width: max-content;
1913
1913
  }
1914
1914
 
1915
+ /*
1916
+ * width="full": the field and its trigger fill the container's inline size.
1917
+ * The value still takes the free space and ellipsizes, and the chevron stays
1918
+ * last, at the inline end. In an Inline row the field takes only the space
1919
+ * its siblings leave, so a Button beside it keeps its label on one line.
1920
+ */
1921
+ .sw-select-full {
1922
+ width: 100%;
1923
+ }
1924
+
1925
+ .sw-inline > .sw-select-full {
1926
+ flex: 1 1 0;
1927
+ min-width: 0;
1928
+ }
1929
+
1930
+ .sw-select-full .sw-select-trigger {
1931
+ width: 100%;
1932
+ }
1933
+
1915
1934
  .sw-select-control {
1916
1935
  position: relative;
1917
1936
  }
@@ -2032,6 +2051,15 @@ body {
2032
2051
  z-index: 10;
2033
2052
  }
2034
2053
 
2054
+ /*
2055
+ * A full-width field's open list is exactly the trigger's width (min-width
2056
+ * already beats the dialog-max cap), so it never runs past a phone's edge;
2057
+ * a long option wraps inside it, even a single long word.
2058
+ */
2059
+ .sw-select-full .sw-select-list {
2060
+ width: 100%;
2061
+ }
2062
+
2035
2063
  .sw-select-option {
2036
2064
  border-radius: var(--sw-radius-sm);
2037
2065
  box-sizing: border-box;
@@ -2042,6 +2070,10 @@ body {
2042
2070
  padding-inline: var(--sw-control-xs-padding-inline);
2043
2071
  }
2044
2072
 
2073
+ .sw-select-full .sw-select-option {
2074
+ overflow-wrap: anywhere;
2075
+ }
2076
+
2045
2077
  .sw-select-option[aria-selected='true'] {
2046
2078
  font-weight: 600;
2047
2079
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",