@reliability-design/web 1.4.9 → 1.4.10

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +155 -2
  2. package/custom-elements.json +18600 -0
  3. package/dist/components/rel-accordion/README.md +9 -2
  4. package/dist/components/rel-alert/README.md +4 -7
  5. package/dist/components/rel-autocomplete/README.md +209 -209
  6. package/dist/components/rel-avatar/README.md +228 -118
  7. package/dist/components/rel-breadcrumb/README.md +1 -0
  8. package/dist/components/rel-button/README.md +9 -9
  9. package/dist/components/rel-calendar/README.md +111 -109
  10. package/dist/components/rel-cardmedia/README.md +1 -1
  11. package/dist/components/rel-carousel/README.md +187 -187
  12. package/dist/components/rel-checkbox/README.md +159 -147
  13. package/dist/components/rel-chip/README.md +141 -138
  14. package/dist/components/rel-container/README.md +1 -1
  15. package/dist/components/rel-date-picker/README.md +221 -206
  16. package/dist/components/rel-drawer/README.md +4 -4
  17. package/dist/components/rel-kbd/README.md +188 -141
  18. package/dist/components/rel-layout/README.md +34 -34
  19. package/dist/components/rel-menu/README.md +17 -10
  20. package/dist/components/rel-menubar/README.md +218 -218
  21. package/dist/components/rel-otp/README.md +4 -8
  22. package/dist/components/rel-pagination/README.md +141 -100
  23. package/dist/components/rel-popover/README.md +220 -203
  24. package/dist/components/rel-radio/README.md +142 -131
  25. package/dist/components/rel-range-picker/README.md +194 -171
  26. package/dist/components/rel-resizable/README.md +220 -199
  27. package/dist/components/rel-segmented-button/README.md +158 -157
  28. package/dist/components/rel-select/README.md +20 -17
  29. package/dist/components/rel-sheet/README.md +3 -2
  30. package/dist/components/rel-signature-pad/README.md +161 -0
  31. package/dist/components/rel-slider/README.md +140 -131
  32. package/dist/components/rel-snackbar/README.md +170 -168
  33. package/dist/components/rel-split-button/README.md +129 -0
  34. package/dist/components/rel-stepper/README.md +171 -171
  35. package/dist/components/rel-switch/README.md +122 -117
  36. package/dist/components/rel-tab/README.md +184 -174
  37. package/dist/components/rel-textfield/README.md +21 -28
  38. package/dist/components/rel-time-picker/README.md +188 -170
  39. package/dist/components/rel-toast/README.md +20 -11
  40. package/dist/components/rel-tooltip/README.md +5 -4
  41. package/dist/components/rel-tree/README.md +157 -0
  42. package/dist/components/rel-upload/README.md +8 -5
  43. package/dist/index.d.mts +1602 -624
  44. package/dist/index.d.ts +1602 -624
  45. package/dist/index.js +11755 -6673
  46. package/dist/index.js.map +1 -1
  47. package/dist/index.mjs +10955 -5880
  48. package/dist/index.mjs.map +1 -1
  49. package/dist/styles/motion.css +31 -0
  50. package/dist/styles/motion.tokens.json +38 -0
  51. package/dist/styles/vars.css +88 -64
  52. package/dist/{vars-DXX6QOAG.css → vars-E22LYFMV.css} +88 -64
  53. package/package.json +60 -37
  54. package/dist/styles/focus-ring.ts +0 -84
@@ -1,157 +1,158 @@
1
- # rel-segmented-button
2
-
3
- A single-select segmented button group: a row of buttons on a 2px-inset track where exactly one segment is active at a time. Each entry in `options` renders as a native `<button role="radio">` inside a `role="radiogroup"`, so the group behaves like a compact radio group (one Tab stop, arrow keys move the selection). Use it for mutually exclusive view or mode switches with two to five short options (list / grid, day / week / month, sort order); do not use it for navigation between pages (use `rel-tab`) or for actions (use `rel-button`).
4
-
5
- The control comes in three sizes that match the text field ramp — `small` 32px, `medium` 40px, `large` 48px — and each option can carry a leading `icon`.
6
-
7
- ## Usage
8
-
9
- ```html
10
- <!-- Basic: options are set as a property, `active` holds the selected value -->
11
- <rel-segmented-button id="view-mode" active="grid" field-aria-label="View mode"></rel-segmented-button>
12
- <script>
13
- document.getElementById('view-mode').options = [
14
- { label: 'List', value: 'list' },
15
- { label: 'Grid', value: 'grid' },
16
- { label: 'Table', value: 'table' }
17
- ];
18
- </script>
19
-
20
- <!-- Options as a JSON attribute, small size -->
21
- <rel-segmented-button
22
- size="small"
23
- active="week"
24
- field-aria-label="Calendar range"
25
- options='[{"label":"Day","value":"day"},{"label":"Week","value":"week"},{"label":"Month","value":"month"}]'>
26
- </rel-segmented-button>
27
-
28
- <!-- Large size (48px), matching a large text field -->
29
- <rel-segmented-button
30
- size="large"
31
- active="grid"
32
- field-aria-label="View mode"
33
- options='[{"label":"List","value":"list"},{"label":"Grid","value":"grid"}]'>
34
- </rel-segmented-button>
35
-
36
- <!-- Leading icons: `icon` names a rel-icon, sized 16 / 18 / 20 with the control -->
37
- <rel-segmented-button
38
- active="list"
39
- field-aria-label="View mode"
40
- options='[{"label":"List","value":"list","icon":"bullet-list"},{"label":"Grid","value":"grid","icon":"dashboard"}]'>
41
- </rel-segmented-button>
42
-
43
- <!-- Disabled option: rendered but not selectable or focusable -->
44
- <rel-segmented-button id="plan" active="free" field-aria-label="Plan"></rel-segmented-button>
45
- <script>
46
- document.getElementById('plan').options = [
47
- { label: 'Free', value: 'free' },
48
- { label: 'Pro', value: 'pro' },
49
- { label: 'Enterprise', value: 'enterprise', disabled: true }
50
- ];
51
- </script>
52
-
53
- <!-- Alignment inside a wider host (align only matters when the host is stretched) -->
54
- <rel-segmented-button
55
- style="display: block; width: 100%"
56
- align="middle"
57
- active="asc"
58
- field-aria-label="Sort direction"
59
- options='[{"label":"Ascending","value":"asc"},{"label":"Descending","value":"desc"}]'>
60
- </rel-segmented-button>
61
-
62
- <!-- Listen for changes -->
63
- <rel-segmented-button id="sort-order" active="newest" field-aria-label="Sort order"></rel-segmented-button>
64
- <script>
65
- const sortOrder = document.getElementById('sort-order');
66
- sortOrder.options = [
67
- { label: 'Newest', value: 'newest' },
68
- { label: 'Oldest', value: 'oldest' },
69
- { label: 'Popular', value: 'popular' }
70
- ];
71
- sortOrder.addEventListener('rel-change', (e) => {
72
- console.log('Selected:', e.detail.value);
73
- });
74
- </script>
75
- ```
76
-
77
- The `SegmentOption` interface and the `SegmentSize` / `SegmentAlign` unions are exported from the package:
78
-
79
- ```typescript
80
- import type { SegmentOption } from '@reliability-design/web';
81
-
82
- const options: SegmentOption[] = [
83
- { label: 'List', value: 'list' }, // label = visible text, value = what `active` / the event carry
84
- { label: 'Grid', value: 'grid', icon: 'dashboard' }, // icon = a rel-icon name, drawn before the label
85
- { label: 'Archive', value: 'archive', disabled: true } // rendered at 50% opacity, skipped by the arrow keys
86
- ];
87
- ```
88
-
89
- <!-- readme-gen:start -->
90
-
91
- <!-- Generated from rel-segmented-button.ts — do not edit; run readme-generate.mjs -->
92
-
93
- ## API
94
-
95
- ### Attributes / Properties
96
-
97
- | Property | Attribute | Type | Default | Description |
98
- | --- | --- | --- | --- | --- |
99
- | `options` | `options` | `SegmentOption[]` | `[]` | The segments in order, each `{ label: string; value: string; disabled?: boolean; icon?: string; badge?: number \| string \| boolean }`. Set as a property (or a JSON array attribute); a disabled entry is rendered but cannot be selected, and `icon` names a `rel-icon` drawn before the label. |
100
- | `active` | `active` | `string` | `""` | `value` of the currently selected segment. Updated by the component on click / arrow keys; set it to pre-select a segment (an unknown value selects nothing). |
101
- | `size` | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Control height, matching the TextField ramp: `small` (32px — 28px segments, `action-md` 12px text, 16px icons), `medium` (default, 40px — 36px segments, `action-lg` 14px text, 18px icons) or `large` (48px — 44px segments, `action-xl` 16px text, 20px icons). |
102
- | `align` | `align` | `"left" \| "middle" \| "right"` | `"left"` | `justify-content` of the group's inline-flex container: `left` (default), `middle` or `right`. Only visible when the host is made wider than its content. |
103
- | `fieldAriaLabel` | `field-aria-label` | `string` | `undefined` | Accessible name for the radiogroup (it renders no visible group label). |
104
-
105
- ## Events
106
-
107
- | Event | Detail | Bubbles / composed | Description |
108
- | --- | --- | --- | --- |
109
- | `rel-change` | `{ value: option.value }` | bubbles / composed | Fired after a non-disabled segment is selected by click or arrow / Home / End key (also when it was already the active one). Detail: `{ value: string }`. The canonical name — prefer it over the legacy `changed`, which fires alongside it with the same detail. |
110
- | `changed` | `{ value: option.value }` | bubbles / composed | Legacy alias of `rel-change`, fired with the same detail at the same time. Kept indefinitely so existing listeners keep working; new code should use `rel-change`. |
111
-
112
- ## Slots
113
-
114
- | Slot | Description |
115
- | --- | --- |
116
- | — | This element has no slots. |
117
-
118
- ## CSS Parts
119
-
120
- | Part | Description |
121
- | --- | --- |
122
- | `option` | Each segment's native `<button role="radio">`; the active one carries the `active` class and `aria-checked="true"`. |
123
- | `icon` | A segment's optional leading `rel-icon` element. |
124
- | `label` | The wrapper around a segment's label text. |
125
- | `badge` | A segment's optional trailing `rel-badge`; rendered only when the option sets `badge`. |
126
- | `base` | The `role="radiogroup"` track that lays out the segments. |
127
-
128
- <!-- readme-gen:end -->
129
-
130
- ## Accessibility
131
-
132
- - The container is a `role="radiogroup"`; give it a name with `field-aria-label` — the component renders no visible group label, so without it screen readers announce an unnamed group.
133
- - Every segment is a native `<button role="radio">` with `aria-checked="true"` on the active one and `aria-checked="false"` on the rest. Disabled options get the native `disabled` attribute, so they are not focusable and do not fire `click`.
134
- - Roving tabindex (APG radio-group pattern): the whole group is a single Tab stop. The active segment carries `tabindex="0"`; if `active` matches no enabled option, the first enabled segment takes the Tab stop instead (without becoming selected). All other segments are `tabindex="-1"`.
135
- - Arrow keys both move focus and select, so they emit the change event immediately. Focus is moved to the newly active segment after the re-render.
136
- - Focus ring: a 2px `border-focus` outline drawn inside the segment (`outline-offset: -2px`) and raised with `z-index: 1` so it stays above the neighbouring segment.
137
- - Nothing is announced beyond the native `aria-checked` change.
138
-
139
- | Key | Action |
140
- | --- | --- |
141
- | `Tab` / `Shift+Tab` | Move focus into / out of the group (one stop, on the active segment) |
142
- | `ArrowRight` / `ArrowDown` | Select and focus the next enabled segment (wraps to the first) |
143
- | `ArrowLeft` / `ArrowUp` | Select and focus the previous enabled segment (wraps to the last) |
144
- | `Home` / `End` | Select and focus the first / last enabled segment |
145
- | `Enter` / `Space` | Native button activation: selects the focused segment |
146
-
147
- ## Notes / Edge Cases
148
-
149
- - **Two events fire for one selection — listen to ONE of them or your handler runs twice.** The change event is `rel-change`, matching the rest of the library. The legacy un-prefixed `changed` fires alongside it with the same detail and is kept so existing listeners keep working — listen to one, not both. Both bubble and are composed, so either can be caught on any ancestor, and both fire when the already-active segment is clicked again.
150
- - `active` is a plain string compared against `option.value`; setting it to a value that is not in `options` (or to a disabled option's value) renders no active segment.
151
- - `options` is compared by reference: mutate-in-place changes are not picked up — assign a new array.
152
- - The host is `display: inline-block` and the group `inline-flex`, so it sizes to its content; `align` has no visible effect until the host is stretched (e.g. `style="display: block; width: 100%"`). Every segment has `min-width: 80px`, so a group is never narrower than 80px per segment.
153
- - Sizing: the track is a `bg-neutral` surface with `spacing-3xs` (2px) padding and a `border-radius-xs` corner — there is no outer border and no dividers between segments. Each segment carries the full `border-radius-xs` on all four corners in every state. Segment heights are 28 / 36 / 44 for `small` / `medium` / `large`, which with the 2px inset give control heights of 32 / 40 / 48.
154
- - Per size: padding `spacing-3xs spacing-sm` / `spacing-2xs spacing-md` / `spacing-xs spacing-lg`, text `action-md` (12px) / `action-lg` (14px) / `action-xl` (16px), and leading icons at 16 / 18 / 20px with an `spacing-xs` (8px) gap.
155
- - Colours: the active segment uses `action-neutral-fill` with inverse text (and `action-neutral-fill-hover` on hover); a non-active segment is transparent so the `action-overlay-hover` alpha composites over the track; disabled segments are drawn at 50% opacity with `bg-neutral-elevated-1` and a `not-allowed` cursor. Background and colour transitions are 0.2s.
156
- - A per-option `icon` renders a `rel-icon` before the label. Its size rides `--rel-icon-size` set directly on the `rel-icon` element — not on a wrapper, since `rel-icon` reflects `size="md"` and its own `:host([size])` rule would otherwise win.
157
- - No design-token overrides or `--rel-segmented-button-*` hooks are exposed; style through the `base` / `option` / `icon` / `label` parts.
1
+ # rel-segmented-button
2
+
3
+ A single-select segmented button group: a row of buttons on a 2px-inset track where exactly one segment is active at a time. Each entry in `options` renders as a native `<button role="radio">` inside a `role="radiogroup"`, so the group behaves like a compact radio group (one Tab stop, arrow keys move the selection). Use it for mutually exclusive view or mode switches with two to five short options (list / grid, day / week / month, sort order); do not use it for navigation between pages (use `rel-tab`) or for actions (use `rel-button`).
4
+
5
+ The control comes in three sizes that match the text field ramp — `small` 32px, `medium` 40px, `large` 48px — and each option can carry a leading `icon`.
6
+
7
+ ## Usage
8
+
9
+ ```html
10
+ <!-- Basic: options are set as a property, `active` holds the selected value -->
11
+ <rel-segmented-button id="view-mode" active="grid" field-aria-label="View mode"></rel-segmented-button>
12
+ <script>
13
+ document.getElementById('view-mode').options = [
14
+ { label: 'List', value: 'list' },
15
+ { label: 'Grid', value: 'grid' },
16
+ { label: 'Table', value: 'table' }
17
+ ];
18
+ </script>
19
+
20
+ <!-- Options as a JSON attribute, small size -->
21
+ <rel-segmented-button
22
+ size="small"
23
+ active="week"
24
+ field-aria-label="Calendar range"
25
+ options='[{"label":"Day","value":"day"},{"label":"Week","value":"week"},{"label":"Month","value":"month"}]'>
26
+ </rel-segmented-button>
27
+
28
+ <!-- Large size (48px), matching a large text field -->
29
+ <rel-segmented-button
30
+ size="large"
31
+ active="grid"
32
+ field-aria-label="View mode"
33
+ options='[{"label":"List","value":"list"},{"label":"Grid","value":"grid"}]'>
34
+ </rel-segmented-button>
35
+
36
+ <!-- Leading icons: `icon` names a rel-icon, sized 16 / 18 / 20 with the control -->
37
+ <rel-segmented-button
38
+ active="list"
39
+ field-aria-label="View mode"
40
+ options='[{"label":"List","value":"list","icon":"bullet-list"},{"label":"Grid","value":"grid","icon":"dashboard"}]'>
41
+ </rel-segmented-button>
42
+
43
+ <!-- Disabled option: rendered but not selectable or focusable -->
44
+ <rel-segmented-button id="plan" active="free" field-aria-label="Plan"></rel-segmented-button>
45
+ <script>
46
+ document.getElementById('plan').options = [
47
+ { label: 'Free', value: 'free' },
48
+ { label: 'Pro', value: 'pro' },
49
+ { label: 'Enterprise', value: 'enterprise', disabled: true }
50
+ ];
51
+ </script>
52
+
53
+ <!-- Alignment inside a wider host (align only matters when the host is stretched) -->
54
+ <rel-segmented-button
55
+ style="display: block; width: 100%"
56
+ align="middle"
57
+ active="asc"
58
+ field-aria-label="Sort direction"
59
+ options='[{"label":"Ascending","value":"asc"},{"label":"Descending","value":"desc"}]'>
60
+ </rel-segmented-button>
61
+
62
+ <!-- Listen for changes -->
63
+ <rel-segmented-button id="sort-order" active="newest" field-aria-label="Sort order"></rel-segmented-button>
64
+ <script>
65
+ const sortOrder = document.getElementById('sort-order');
66
+ sortOrder.options = [
67
+ { label: 'Newest', value: 'newest' },
68
+ { label: 'Oldest', value: 'oldest' },
69
+ { label: 'Popular', value: 'popular' }
70
+ ];
71
+ sortOrder.addEventListener('rel-change', (e) => {
72
+ console.log('Selected:', e.detail.value);
73
+ });
74
+ </script>
75
+ ```
76
+
77
+ The `SegmentOption` interface and the `SegmentSize` / `SegmentAlign` unions are exported from the package:
78
+
79
+ ```typescript
80
+ import type { SegmentOption } from '@reliability-design/web';
81
+
82
+ const options: SegmentOption[] = [
83
+ { label: 'List', value: 'list' }, // label = visible text, value = what `active` / the event carry
84
+ { label: 'Grid', value: 'grid', icon: 'dashboard' }, // icon = a rel-icon name, drawn before the label
85
+ { label: 'Archive', value: 'archive', disabled: true } // rendered at 50% opacity, skipped by the arrow keys
86
+ ];
87
+ ```
88
+
89
+ <!-- readme-gen:start -->
90
+
91
+ <!-- Generated from rel-segmented-button.ts — do not edit; run readme-generate.mjs -->
92
+
93
+ ## API
94
+
95
+ ### Attributes / Properties
96
+
97
+ | Property | Attribute | Type | Default | Description |
98
+ | --- | --- | --- | --- | --- |
99
+ | `options` | `options` | `SegmentOption[]` | `[]` | The segments in order, each `{ label: string; value: string; disabled?: boolean; icon?: string; badge?: number \| string \| boolean }`. Set as a property (or a JSON array attribute); a disabled entry is rendered but cannot be selected, and `icon` names a `rel-icon` drawn before the label. |
100
+ | `active` | `active` | `string` | `""` | `value` of the currently selected segment. Updated by the component on click / arrow keys; set it to pre-select a segment (an unknown value selects nothing). |
101
+ | `disabled` | `disabled` | `boolean` | `false` | Disables the whole group (click and keyboard) without changing individual `options[].disabled` settings. Angular `control.disable()` uses it. New in 1.5.0. Reflected. |
102
+ | `size` | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Control height, matching the TextField ramp: `small` (32px — 28px segments, `action-md` 12px text, 16px icons), `medium` (default, 40px — 36px segments, `action-lg` 14px text, 18px icons) or `large` (48px — 44px segments, `action-xl` 16px text, 20px icons). |
103
+ | `align` | `align` | `"left" \| "middle" \| "right"` | `"left"` | `justify-content` of the group's inline-flex container: `left` (default), `middle` or `right`. Only visible when the host is made wider than its content. |
104
+ | `fieldAriaLabel` | `field-aria-label` | `string` | `undefined` | Accessible name for the radiogroup (it renders no visible group label). |
105
+
106
+ ## Events
107
+
108
+ | Event | Detail | Bubbles / composed | Description |
109
+ | --- | --- | --- | --- |
110
+ | `changed` | `{ value: option.value }` | bubbles / composed | Legacy alias of `rel-change`, fired with the same detail at the same time. Kept indefinitely so existing listeners keep working; new code should use `rel-change`. |
111
+ | `rel-change` | `{ value: option.value }` | bubbles / composed | Fired after a non-disabled segment is selected by click or arrow / Home / End key (also when it was already the active one). Detail: `{ value: string }`. The canonical name — prefer it over the legacy `changed`, which fires alongside it with the same detail. |
112
+
113
+ ## Slots
114
+
115
+ | Slot | Description |
116
+ | --- | --- |
117
+ | — | This element has no slots. |
118
+
119
+ ## CSS Parts
120
+
121
+ | Part | Description |
122
+ | --- | --- |
123
+ | `option` | Each segment's native `<button role="radio">`; the active one carries the `active` class and `aria-checked="true"`. |
124
+ | `icon` | A segment's optional leading `rel-icon` element. |
125
+ | `label` | The wrapper around a segment's label text. |
126
+ | `badge` | A segment's optional trailing `rel-badge`; rendered only when the option sets `badge`. |
127
+ | `base` | The `role="radiogroup"` track that lays out the segments. |
128
+
129
+ <!-- readme-gen:end -->
130
+
131
+ ## Accessibility
132
+
133
+ - The container is a `role="radiogroup"`; give it a name with `field-aria-label` — the component renders no visible group label, so without it screen readers announce an unnamed group.
134
+ - Every segment is a native `<button role="radio">` with `aria-checked="true"` on the active one and `aria-checked="false"` on the rest. Disabled options get the native `disabled` attribute, so they are not focusable and do not fire `click`.
135
+ - Roving tabindex (APG radio-group pattern): the whole group is a single Tab stop. The active segment carries `tabindex="0"`; if `active` matches no enabled option, the first enabled segment takes the Tab stop instead (without becoming selected). All other segments are `tabindex="-1"`.
136
+ - Arrow keys both move focus and select, so they emit the change event immediately. Focus is moved to the newly active segment after the re-render.
137
+ - Focus ring: a 2px `border-focus` outline drawn inside the segment (`outline-offset: -2px`) and raised with `z-index: 1` so it stays above the neighbouring segment.
138
+ - Nothing is announced beyond the native `aria-checked` change.
139
+
140
+ | Key | Action |
141
+ | --- | --- |
142
+ | `Tab` / `Shift+Tab` | Move focus into / out of the group (one stop, on the active segment) |
143
+ | `ArrowRight` / `ArrowDown` | Select and focus the next enabled segment (wraps to the first) |
144
+ | `ArrowLeft` / `ArrowUp` | Select and focus the previous enabled segment (wraps to the last) |
145
+ | `Home` / `End` | Select and focus the first / last enabled segment |
146
+ | `Enter` / `Space` | Native button activation: selects the focused segment |
147
+
148
+ ## Notes / Edge Cases
149
+
150
+ - **Two events fire for one selection — listen to ONE of them or your handler runs twice.** The change event is `rel-change`, matching the rest of the library. The legacy un-prefixed `changed` fires alongside it with the same detail and is kept so existing listeners keep working — listen to one, not both. Both bubble and are composed, so either can be caught on any ancestor, and both fire when the already-active segment is clicked again.
151
+ - `active` is a plain string compared against `option.value`; setting it to a value that is not in `options` (or to a disabled option's value) renders no active segment.
152
+ - `options` is compared by reference: mutate-in-place changes are not picked up — assign a new array.
153
+ - The host is `display: inline-block` and the group `inline-flex`, so it sizes to its content; `align` has no visible effect until the host is stretched (e.g. `style="display: block; width: 100%"`). Every segment has `min-width: 80px`, so a group is never narrower than 80px per segment.
154
+ - Sizing: the track is a `bg-neutral` surface with `spacing-3xs` (2px) padding and a `border-radius-xs` corner — there is no outer border and no dividers between segments. Each segment carries the full `border-radius-xs` on all four corners in every state. Segment heights are 28 / 36 / 44 for `small` / `medium` / `large`, which with the 2px inset give control heights of 32 / 40 / 48.
155
+ - Per size: padding `spacing-3xs spacing-sm` / `spacing-2xs spacing-md` / `spacing-xs spacing-lg`, text `action-md` (12px) / `action-lg` (14px) / `action-xl` (16px), and leading icons at 16 / 18 / 20px with an `spacing-xs` (8px) gap.
156
+ - Colours: the active segment uses `action-neutral-fill` with inverse text (and `action-neutral-fill-hover` on hover); a non-active segment is transparent so the `action-overlay-hover` alpha composites over the track; disabled segments are drawn at 50% opacity with `bg-neutral-elevated-1` and a `not-allowed` cursor. Background and colour transitions are 0.2s.
157
+ - A per-option `icon` renders a `rel-icon` before the label. Its size rides `--rel-icon-size` set directly on the `rel-icon` element — not on a wrapper, since `rel-icon` reflects `size="md"` and its own `:host([size])` rule would otherwise win.
158
+ - No design-token overrides or `--rel-segmented-button-*` hooks are exposed; style through the `base` / `option` / `icon` / `label` parts.
@@ -10,7 +10,7 @@
10
10
  <script>
11
11
  const country = document.getElementById("country");
12
12
  country.options = ["USA", "Canada", "Mexico"];
13
- country.addEventListener("changed", (e) => console.log(e.detail.value)); // "Canada"
13
+ country.addEventListener("rel-change", (e) => console.log(e.detail.value)); // "Canada"
14
14
  </script>
15
15
 
16
16
  <!-- Object options: value / label / icon / supportingText (a second line under the label, in single and multi rows) -->
@@ -22,8 +22,8 @@
22
22
  </script>
23
23
 
24
24
  <!-- Preselected value, helper text, icons -->
25
- <rel-select label="Email preference" selected="weekly" supporting-text="How often we email you" left-icon="mail"></rel-select>
26
- <rel-select label="Custom chevron" right-icon="expand-more"></rel-select>
25
+ <rel-select label="Email preference" selected="weekly" supporting-text="How often we email you" left-icon="email"></rel-select>
26
+ <rel-select label="Custom chevron" right-icon="chevron-down"></rel-select>
27
27
 
28
28
  <!-- Sizes, corners, label styles -->
29
29
  <rel-select label="Small" size="small"></rel-select>
@@ -52,7 +52,7 @@
52
52
  <script>
53
53
  const tags = document.getElementById("tags");
54
54
  tags.options = ["urgent", "fragile", "hazmat", "oversize", "refrigerated"];
55
- tags.addEventListener("changed", (e) => console.log(e.detail.value)); // ["urgent", "fragile", ...]
55
+ tags.addEventListener("rel-change", (e) => console.log(e.detail.value)); // ["urgent", "fragile", ...]
56
56
  </script>
57
57
  ```
58
58
 
@@ -85,13 +85,14 @@
85
85
  | `searchPlaceholder` | `search-placeholder` | `string` | `"Search option"` | Placeholder of the in-panel search field (the field is hidden when `noSearch` is set). |
86
86
  | `selectAllLabel` | `select-all-label` | `string` | `"Select all"` | Label of the Select-all row shown at the top of the panel in multi-select mode. |
87
87
  | `maxVisibleChips` | `max-visible-chips` | `number` | `Infinity` | Optional hard cap on visible chips. By default (no cap) the visible count adapts to the trigger's width: chips collapse into the "+N" badge one by one instead of wrapping to a second line. |
88
- | `noSearch` | `no-search` | `boolean` | `false` | DELIBERATE DIVERGENCE — do not default this to `!multiple`. Figma's `.Menu Dropdown` shows the search row only on `Type=Multioption`; its `Type=Single Option` panels contain five Select Option items and nothing else. Here the search row renders for single-select too unless this is set. Ruled by the user: our behaviour stands and the DESIGN FILE follows our docs. A long single-select list is unusable without search, and making the default depend on `multiple` would change the panel of every existing single-select. |
88
+ | `noSearch` | `no-search` | `boolean` | `false` | Hides the in-panel search field. The search row shows in single- and multi-select panels alike unless this is set; use it for short lists that don't need searching (Figma's "Single Option" dropdown has no search bar). |
89
89
 
90
90
  ## Events
91
91
 
92
92
  | Event | Detail | Bubbles / composed | Description |
93
93
  | --- | --- | --- | --- |
94
- | `changed` | `{ value: this.multiple ? this.selectedValues : this.selected }` | bubbles / composed | Fired after any selection change (pick, checkbox toggle, Select-all, chip removal). Detail: `{ value }` — the selected value string in single mode, or the array of selected values when `multiple`. |
94
+ | `changed` | `{ value: this.multiple ? this.selectedValues : this.selected }` | bubbles / composed | Fired after any selection change (pick, checkbox toggle, Select-all, chip removal). Detail: `{ value }` — the selected value string in single mode, or the array of selected values when `multiple`. Legacy name, kept indefinitely: it fires together with `rel-change`, same detail, same moment. |
95
+ | `rel-change` | `{ value: this.multiple ? this.selectedValues : this.selected }` | bubbles / composed | Prefixed alias of `changed`, fired on the same action with the same detail (added in 1.4.9). The name the rest of the library uses — prefer it in new code. |
95
96
 
96
97
  ## Slots
97
98
 
@@ -105,9 +106,9 @@
105
106
  | --- | --- |
106
107
  | `label` | The visible label above the trigger (default label style) |
107
108
  | `trigger` | The button that opens the panel; styled like a text field |
108
- | `value` | The value area inside the trigger (selected label, placeholder or chips) |
109
+ | `value` | The value area: the selected label or placeholder inside the trigger. In multi-select with chips (since 1.5.0) it is the chip overlay, a sibling laid over the trigger's value area rather than a child of it |
109
110
  | `helper` | The supporting / error text below the trigger |
110
- | `panel` | The dropdown listbox container (same node as `list`) |
111
+ | `panel` | The dropdown panel (a `role="group"` holding the search, Select-all and the options listbox; same node as `list`) |
111
112
  | `list` | Alias of `panel` |
112
113
  | `input` | The search input inside the panel |
113
114
  | `option` | A single-select option row |
@@ -123,10 +124,9 @@ Set these on the host (or an ancestor) to customise the element without touching
123
124
  | Custom property | Default | Description |
124
125
  | --- | --- | --- |
125
126
  | `--rel-field-label-reserve` | `calc(var(--rel-body-md-regular-line-height) + var(--rel-spacing-2xs))` | Height held for the label row whenever no label renders above the trigger — `labelStyle="none"`, and `labelStyle="floating"` where the label sits inside the trigger — so the trigger still lines up with labelled siblings in a form row. Defaults to that size's label line-height plus the wrapper gap (24px at `small`, 26px at `medium` and `large`). Set to `0` to collapse the row entirely (toolbars, table cells). |
126
- | `--rel-select-bg` | `var(--rel-bg-neutral)` | Background of the trigger, the panel and its sticky header. No longer paints behind the floating label — see `--rel-select-surface` |
127
+ | `--rel-overlay-fallback-z` | `1001` | z-index of the panel on the **no-Popover-API fallback path only**. Wherever the browser top layer is available the panel is promoted into it and this value is inert, because top-layer elements are not in the page stacking order at all. Shared with the other overlays (select, autocomplete, the three pickers) so the whole family can be moved from one place. |
128
+ | `--rel-select-bg` | `var(--rel-bg-neutral)` | Background of the trigger, the panel and its sticky header. No longer paints behind the floating label — see `--rel-select-surface`. |
127
129
  | `--rel-select-bg-disabled` | `var(--rel-bg-neutral-elevated-2)` | Trigger background when `state="disabled"` |
128
- | `--rel-select-surface` | `var(--rel-select-bg)` | Colour of the strip the floating label lays over the trigger's top border and focus ring, so the label appears to sit in a notch. It overshoots the band by 2px on the page side, so it must match the surface the field sits on. Set it when the field sits on a surface that is not the trigger's own fill, otherwise the strip shows as a short dash of the wrong colour. Mirrors `--rel-textfield-surface` |
129
- | `--rel-select-surface-disabled` | `var(--rel-select-bg-disabled)` | The same strip while `state="disabled"`, where the trigger fill changes |
130
130
  | `--rel-select-border-default` | `var(--rel-border-default)` | Trigger border colour at rest |
131
131
  | `--rel-select-border-disabled` | `var(--rel-border-accent)` | Trigger border colour when `state="disabled"` |
132
132
  | `--rel-select-border-error` | `var(--rel-border-danger)` | Trigger border colour when `state="error"` |
@@ -134,7 +134,7 @@ Set these on the host (or an ancestor) to customise the element without touching
134
134
  | `--rel-select-divider` | `var(--rel-border-subtle)` | Hairline under the search field, the Select-all row and every option row |
135
135
  | `--rel-select-icon-color` | `var(--rel-text-default)` | Colour of the trigger icons, the search icon and option prefix icons |
136
136
  | `--rel-select-label-color` | `var(--rel-text-label)` | Colour of the static label and of the floated label |
137
- | `--rel-select-menu-border` | `var(--rel-border-default)` | Panel border colour. The panel is borderless by design and only draws this on dark surfaces, where its shadow alone can't separate it from the page. |
137
+ | `--rel-select-menu-border` | `var(--rel-border-subtle)` | Panel border colour. The panel is borderless by design and only draws this on dark surfaces, where its shadow alone can't separate it from the page. |
138
138
  | `--rel-select-menu-radius` | `var(--rel-border-radius-sm)` | Panel corner radius |
139
139
  | `--rel-select-menu-shadow` | `var(--rel-shadows-md)` | Panel box-shadow |
140
140
  | `--rel-select-option-hover` | `var(--rel-action-overlay-hover)` | Background of the hovered / keyboard-highlighted / selected option and Select-all row |
@@ -144,8 +144,9 @@ Set these on the host (or an ancestor) to customise the element without touching
144
144
  | `--rel-select-radius` | `var(--rel-border-radius-xs)` | Trigger corner radius when `border="rounded"` |
145
145
  | `--rel-select-search-border` | `var(--rel-border-default)` | Border colour of the search field inside the panel |
146
146
  | `--rel-select-supporting-color` | `var(--rel-text-body)` | Supporting text colour (non-error) |
147
+ | `--rel-select-surface` | `var(--rel-select-bg, var(--rel-bg-neutral))` | Colour of the strip the floating label lays over the trigger's top border and focus ring, so the label appears to sit in a notch. It overshoots the band by 2px on the page side, so it must match the surface the field sits on. Defaults to `--rel-select-bg`. Set it when the field sits on a surface that is not the trigger's own fill, otherwise the strip shows as a short dash of the wrong colour. Mirrors `--rel-textfield-surface`. |
148
+ | `--rel-select-surface-disabled` | `var(--rel-select-bg-disabled, var(--rel-bg-neutral-elevated-2))` | The same strip while `state="disabled"`, where the trigger fill changes. Defaults to `--rel-select-bg-disabled`. |
147
149
  | `--rel-select-value-color` | `var(--rel-text-default)` | Colour of the selected value, the search text, option labels and the check mark |
148
- | `--rel-overlay-fallback-z` | `1001` | z-index of the panel on the **no-Popover-API fallback path only**. Wherever the browser top layer is available the panel is promoted into it and this value is inert, because top-layer elements are not in the page stacking order at all. Shared with the other overlays (select, autocomplete, the three pickers) so the whole family can be moved from one place. |
149
150
 
150
151
  <!-- readme-gen:end -->
151
152
 
@@ -153,10 +154,11 @@ Set these on the host (or an ancestor) to customise the element without touching
153
154
 
154
155
  - The trigger is a native `<button role="combobox">` with `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls` (the listbox id) and `aria-invalid` (true in the error state). Its accessible name is composed as `"{label}, {selected label}"` in single mode and `"{label}, a, b, c, plus N more"` in multi mode (first three labels), so the current selection is always announced with the name. `field-aria-labelledby` replaces that computed name entirely.
155
156
  - The static label (`label-style="default"`) is a `<label for>` linked to the trigger. The floating label is a visual `<span>`; the trigger's `aria-label` still carries the name. With `label-style="none"` the name comes from `field-aria-label` and a console warning is logged once if neither `field-aria-label` nor `field-aria-labelledby` is set.
156
- - The panel is `role="listbox"` with `aria-multiselectable` and an `aria-label` of the field name (or "Options"). Every row is `role="option"` with `aria-selected`; multi-select rows and the Select-all row carry an explicit `aria-label` and contain an `inert`, `aria-hidden` `rel-checkbox` used purely as the visual.
157
+ - The panel (part `panel`) is a `role="group"` named after the field (or "Options"): it holds the search combobox and the Select-all row, which a listbox may not contain. The `role="listbox"` (same name, `aria-multiselectable` in multi mode, and the id the trigger and search `aria-controls` point at) is the options container inside it; the Select-all row joins it through `aria-owns`. With no matches the listbox holds one disabled option with the "No matches found" / "No options available" text. Every row is `role="option"` with `aria-selected`; multi-select rows and the Select-all row carry an explicit `aria-label` and contain an `inert`, `aria-hidden` `rel-checkbox` used purely as the visual.
157
158
  - The in-panel search input is itself a `role="combobox"` with `aria-expanded`, `aria-controls` and `aria-activedescendant` tracking the highlighted row. With `no-search` the trigger keeps focus while open and carries `aria-activedescendant` instead.
158
159
  - Screen-reader announcements via the shared `announce()` helper: `"{label} selected"` / `"{label} deselected"` on a checkbox toggle, `"All options selected"` / `"All options deselected"`, `"{label} removed"` when a chip is removed, and `"N options available"` / `"No matches found"` / `"No options available"` while typing in the search field.
159
- - Focus: opening the panel moves focus to the search input (with `preventScroll`) unless `no-search`; closing always returns focus to the trigger. Chips render inside the trigger `<button>`, so their remove buttons are not Tab stops — Backspace / Delete is the keyboard path.
160
+ - Focus: opening the panel moves focus to the search input (with `preventScroll`) unless `no-search`; closing always returns focus to the trigger.
161
+ - Multi-select chips (changed in 1.5.0): the chips are no longer inside the trigger `<button>` (interactive content in a button is invalid HTML), and their bodies and the "+N" chip are no longer Tab stops that did nothing. They sit in an overlay over the trigger's value area: a click on a chip label or on "+N" opens the panel like any click on the field. The Tab order is the trigger, then each visible chip's remove button ("Remove {label}", at least 24 × 24px). Removing a chip from the keyboard moves focus to the next chip's remove button, else the previous one, else the trigger. Backspace / Delete on the trigger (or Backspace in an empty search field) still removes the most recent selection. The "+N" chip is hidden from assistive technology, because the trigger's name already ends "plus N more". Before 1.5.0 this README said the remove buttons were not Tab stops; they were, alongside the dead stops.
160
162
 
161
163
  | Key | Action |
162
164
  |-----|--------|
@@ -174,8 +176,9 @@ Set these on the host (or an ancestor) to customise the element without touching
174
176
 
175
177
  ## Notes / Edge Cases
176
178
 
177
- - The change event is named `changed` (not `rel-change`); `detail.value` is a string in single mode and an array of strings when `multiple`.
179
+ - **Two events fire for one selection — listen to ONE of them or your handler runs twice.** `rel-change` (added in 1.4.9, matching the rest of the library) and the legacy `changed` fire together with the same detail; `detail.value` is a string in single mode and an array of strings when `multiple`. Existing `changed` listeners keep working.
178
180
  - Opening the panel does not pre-highlight any row; the highlight appears on the first hover or arrow key so nothing reads as an unintended default.
181
+ - Since 1.5.0 the multi-select chip overlay is positioned from the field's own size (border, inline padding, icon size), not from the trigger's computed padding. Custom CSS that changes the inline padding through `::part(trigger)` therefore no longer moves the chips with it. Style `::part(value)` to adjust them as well. `::part(trigger)` is still the same button with the same box, and the chips are still `rel-chip` elements.
179
182
  - `state="error"` clears itself to `"default"` on the next selection, toggle, Select-all or chip removal. `state="disabled"` dims the whole field to 50% opacity, makes chips non-removable and blocks pointer events on them.
180
183
  - Turning `multiple` off resets `selectedValues` to `[]`. `selected` is ignored in multi mode and `selectedValues` in single mode.
181
184
  - Chips never wrap: the number shown adapts to the trigger width (measured through a hidden copy of every chip and a `ResizeObserver`), with the remainder collapsed into a trailing "+N" chip. At least one chip always stays visible; `max-visible-chips` is an extra hard cap. A selected value with no matching option is shown as its raw value.
@@ -183,6 +186,6 @@ Set these on the host (or an ancestor) to customise the element without touching
183
186
  - A custom `right-icon` replaces the chevron and does not rotate when the panel opens; the default chevron rotates 180°.
184
187
  - The search field uses the small field sizing for both `small` and `medium` selects; only `large` bumps it to medium. Empty results show "No matches found" (or "No options available" when `options` is empty).
185
188
  - The `helper` part exists only while `supporting-text` is non-empty.
186
- - The open listbox is promoted to the browser top layer (native Popover API, `popover="manual"`) so it renders correctly inside transformed or `overflow: hidden` containers such as a centered `rel-modal` or a `rel-drawer`; engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction. The panel matches the trigger width, flips above the trigger when there is not enough room below, is capped at 360px with internal scrolling, and closes on any outside pointerdown or outside scroll (scrolling the list itself does not close it).
189
+ - The open listbox is promoted to the browser top layer (native Popover API, `popover="manual"`) so it renders correctly inside transformed or `overflow: hidden` containers such as a centered `rel-modal` or a `rel-drawer`; engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction. The panel matches the trigger width, flips above the trigger when there is not enough room below, is capped at 360px with internal scrolling, and closes on any outside pointerdown. On an outside scroll (since 1.5.0) it **stays open and moves with the field while the field is still visible**, and closes once the field scrolls out of the viewport or its scroll container, or under a sticky header — so the top-layer panel never paints over page furniture (the reason 1.4.9 closed it on every scroll). Only the window and the field's own scroll containers count, including ones inside shadow roots such as `rel-modal`'s body; scrolls of unrelated elements and of the panel itself are ignored, and a wheel or swipe over the panel never scrolls the page (`overscroll-behavior: contain`).
187
190
  - `--rel-field-label-reserve` (see Component hooks) keeps a `label-style="none"` select bottom-aligned with labelled siblings; set it to `0` in toolbars or table cells.
188
191
  - The trigger heights (32 / 40 / 48px) match `rel-textfield` and the date / time / range picker triggers so mixed form rows line up.
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Centred dialog, small width -->
27
27
  <rel-modal side="center" size="small" title="Session expired" description="Sign in again to continue.">
28
- <rel-button slot="footer" variant="primary">Sign in</rel-button>
28
+ <rel-button slot="footer" variant="primary" tone="brand">Sign in</rel-button>
29
29
  </rel-modal>
30
30
 
31
31
  <!-- Top-anchored sheet with extra header content -->
@@ -60,7 +60,7 @@
60
60
  <!-- Non-dismissible: no close button, backdrop and Escape ignored -->
61
61
  <rel-modal id="locked" title="Accept terms">
62
62
  <p>You must accept to continue.</p>
63
- <rel-button slot="footer" variant="primary">Accept</rel-button>
63
+ <rel-button slot="footer" variant="primary" tone="brand">Accept</rel-button>
64
64
  </rel-modal>
65
65
  <script>
66
66
  // Boolean props that default to true can only be switched off from JS —
@@ -159,6 +159,7 @@ Set these on the host (or an ancestor) to customise the element without touching
159
159
  - `title` is also HTML's global tooltip attribute. The component reads the attribute into the property and then removes it from the host, so no native tooltip appears over the modal or its scrim.
160
160
  - `subtitle` is an alias for `description` and is only rendered when `description` is empty.
161
161
  - The footer is only rendered when the `footer` slot has content; the host gains a `has-footer` attribute while it does.
162
+ - The header always has a bottom border. The footer's top divider shows only while the body scrolls (the host gains `body-scrolls`); short content has no footer divider. Scrolling is re-measured when the body or any slotted child resizes, so content added after opening, children growing in place (including in a `fullscreen` modal, whose body never changes size itself), images loading and window resizes all update it.
162
163
  - `dismissible` is the coarse switch and is ANDed with `close-on-backdrop-click`, `close-on-escape` and `show-close-button`; turning it off locks all three regardless of their own values.
163
164
  - `loading` blocks backdrop clicks and the close button but does **not** block `Escape`; set `disabled` (or `close-on-escape` = false) if Escape must be blocked as well.
164
165
  - Boolean props default to `true` (`dismissible`, `modal`, `close-on-*`, `show-close-button`, `auto-focus`, `restore-focus`, `scrollable`); an absent attribute keeps the default, so switch them off from JavaScript (`modal.closeOnEscape = false`).