@bitforgehq/ui 0.3.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.
Files changed (281) hide show
  1. package/CHANGELOG.md +3665 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -0
  4. package/ai/MANIFEST.index.json +383 -0
  5. package/ai/MANIFEST.json +7528 -0
  6. package/ai/REFERENCE.index.json +1461 -0
  7. package/ai/REFERENCE.json +7213 -0
  8. package/ai/UI_GUIDE.md +1159 -0
  9. package/ai/components/action-bar.md +209 -0
  10. package/ai/components/action-sheet.md +526 -0
  11. package/ai/components/alert-dialog.md +509 -0
  12. package/ai/components/alert.md +376 -0
  13. package/ai/components/avatar.md +146 -0
  14. package/ai/components/back-button.md +286 -0
  15. package/ai/components/badge.md +171 -0
  16. package/ai/components/button.md +431 -0
  17. package/ai/components/card.md +303 -0
  18. package/ai/components/checkbox.md +418 -0
  19. package/ai/components/chip-group.md +205 -0
  20. package/ai/components/chip.md +300 -0
  21. package/ai/components/content.md +474 -0
  22. package/ai/components/desktop-toast.md +420 -0
  23. package/ai/components/dialog.md +962 -0
  24. package/ai/components/empty-state.md +146 -0
  25. package/ai/components/field.md +319 -0
  26. package/ai/components/header.md +809 -0
  27. package/ai/components/icon-button.md +221 -0
  28. package/ai/components/icon.md +176 -0
  29. package/ai/components/input.md +455 -0
  30. package/ai/components/list-item.md +326 -0
  31. package/ai/components/list.md +178 -0
  32. package/ai/components/menu-button.md +377 -0
  33. package/ai/components/menu.md +661 -0
  34. package/ai/components/modal.md +613 -0
  35. package/ai/components/number-input.md +423 -0
  36. package/ai/components/page.md +299 -0
  37. package/ai/components/progress.md +215 -0
  38. package/ai/components/radio-group.md +432 -0
  39. package/ai/components/refresher.md +377 -0
  40. package/ai/components/segmented.md +517 -0
  41. package/ai/components/select.md +510 -0
  42. package/ai/components/sheet.md +845 -0
  43. package/ai/components/spinner.md +171 -0
  44. package/ai/components/stack-outlet.md +661 -0
  45. package/ai/components/tab-bar.md +427 -0
  46. package/ai/components/tab.md +295 -0
  47. package/ai/components/tabs.md +408 -0
  48. package/ai/components/textarea.md +371 -0
  49. package/ai/components/toast.md +522 -0
  50. package/ai/components/toggle.md +387 -0
  51. package/ai/reference/bf-action-directive.md +21 -0
  52. package/ai/reference/bf-action-sheet-action-role.md +27 -0
  53. package/ai/reference/bf-action-sheet-action.md +28 -0
  54. package/ai/reference/bf-action-sheet-options.md +28 -0
  55. package/ai/reference/bf-action-sheet-service.md +33 -0
  56. package/ai/reference/bf-alert-dialog-button-role.md +27 -0
  57. package/ai/reference/bf-alert-dialog-button.md +27 -0
  58. package/ai/reference/bf-alert-dialog-options.md +29 -0
  59. package/ai/reference/bf-alert-dialog-service.md +34 -0
  60. package/ai/reference/bf-alert-variant.md +28 -0
  61. package/ai/reference/bf-autocapitalize.md +30 -0
  62. package/ai/reference/bf-avatar-size.md +27 -0
  63. package/ai/reference/bf-avatar-variant.md +28 -0
  64. package/ai/reference/bf-back-at-root.md +28 -0
  65. package/ai/reference/bf-back-button-service.md +40 -0
  66. package/ai/reference/bf-badge-variant.md +29 -0
  67. package/ai/reference/bf-button-size.md +29 -0
  68. package/ai/reference/bf-button-type.md +28 -0
  69. package/ai/reference/bf-button-variant.md +29 -0
  70. package/ai/reference/bf-card-padding.md +26 -0
  71. package/ai/reference/bf-card-variant.md +28 -0
  72. package/ai/reference/bf-chip-group-mode.md +27 -0
  73. package/ai/reference/bf-chip-variant.md +26 -0
  74. package/ai/reference/bf-compare-with.md +24 -0
  75. package/ai/reference/bf-component-overlay-ref.md +33 -0
  76. package/ai/reference/bf-confirm-button-role.md +26 -0
  77. package/ai/reference/bf-confirm-button.md +27 -0
  78. package/ai/reference/bf-confirm-options.md +31 -0
  79. package/ai/reference/bf-confirm-size.md +26 -0
  80. package/ai/reference/bf-content-scroll-event.md +21 -0
  81. package/ai/reference/bf-defaults-options.md +32 -0
  82. package/ai/reference/bf-defaults.md +28 -0
  83. package/ai/reference/bf-desktop-toast-config.md +28 -0
  84. package/ai/reference/bf-desktop-toast-harness.md +33 -0
  85. package/ai/reference/bf-desktop-toast-options.md +28 -0
  86. package/ai/reference/bf-desktop-toast-position.md +28 -0
  87. package/ai/reference/bf-desktop-toast-service.md +42 -0
  88. package/ai/reference/bf-dialog-config.md +44 -0
  89. package/ai/reference/bf-dialog-harness.md +52 -0
  90. package/ai/reference/bf-dialog-ref.md +44 -0
  91. package/ai/reference/bf-dialog-role.md +26 -0
  92. package/ai/reference/bf-dialog-service.md +55 -0
  93. package/ai/reference/bf-dialog-size.md +31 -0
  94. package/ai/reference/bf-dialog-template-context.md +32 -0
  95. package/ai/reference/bf-empty-state-heading-level.md +26 -0
  96. package/ai/reference/bf-enter-key-hint.md +31 -0
  97. package/ai/reference/bf-field-control-class.md +32 -0
  98. package/ai/reference/bf-field-control-state.md +28 -0
  99. package/ai/reference/bf-field-size.md +29 -0
  100. package/ai/reference/bf-haptics-impact-style.md +27 -0
  101. package/ai/reference/bf-haptics-notification-type.md +27 -0
  102. package/ai/reference/bf-haptics-service.md +41 -0
  103. package/ai/reference/bf-harness-filters.md +29 -0
  104. package/ai/reference/bf-horizontal-scroll-directive.md +129 -0
  105. package/ai/reference/bf-icon-button-variant.md +28 -0
  106. package/ai/reference/bf-icon-size.md +27 -0
  107. package/ai/reference/bf-input-autocapitalize.md +36 -0
  108. package/ai/reference/bf-input-enter-key-hint.md +38 -0
  109. package/ai/reference/bf-input-input-mode.md +38 -0
  110. package/ai/reference/bf-input-mode.md +32 -0
  111. package/ai/reference/bf-input-type.md +35 -0
  112. package/ai/reference/bf-jsdom-shim-options.md +22 -0
  113. package/ai/reference/bf-keyboard-assist-behavior.md +26 -0
  114. package/ai/reference/bf-keyboard-assist-directive.md +185 -0
  115. package/ai/reference/bf-keyboard-phase.md +30 -0
  116. package/ai/reference/bf-keyboard-service.md +50 -0
  117. package/ai/reference/bf-labels-overrides.md +26 -0
  118. package/ai/reference/bf-labels.md +44 -0
  119. package/ai/reference/bf-menu-button-harness.md +40 -0
  120. package/ai/reference/bf-menu-button-variant.md +27 -0
  121. package/ai/reference/bf-menu-divider-directive.md +13 -0
  122. package/ai/reference/bf-menu-group-directive.md +14 -0
  123. package/ai/reference/bf-menu-harness.md +55 -0
  124. package/ai/reference/bf-menu-item-checkbox-directive.md +31 -0
  125. package/ai/reference/bf-menu-item-directive.md +31 -0
  126. package/ai/reference/bf-menu-item-harness.md +52 -0
  127. package/ai/reference/bf-menu-item-radio-directive.md +29 -0
  128. package/ai/reference/bf-menu-placement.md +29 -0
  129. package/ai/reference/bf-menu-trigger-directive.md +46 -0
  130. package/ai/reference/bf-modal-options.md +30 -0
  131. package/ai/reference/bf-modal-presentation.md +26 -0
  132. package/ai/reference/bf-modal-service.md +38 -0
  133. package/ai/reference/bf-nav-controller.md +59 -0
  134. package/ai/reference/bf-nav-direction.md +23 -0
  135. package/ai/reference/bf-nav-target.md +21 -0
  136. package/ai/reference/bf-number-input-mode.md +28 -0
  137. package/ai/reference/bf-number-input-step.md +25 -0
  138. package/ai/reference/bf-overlay-auto-focus.md +29 -0
  139. package/ai/reference/bf-overlay-controller.md +22 -0
  140. package/ai/reference/bf-overlay-host.md +36 -0
  141. package/ai/reference/bf-overlay-options.md +53 -0
  142. package/ai/reference/bf-overlay-position.md +30 -0
  143. package/ai/reference/bf-overlay-ref.md +53 -0
  144. package/ai/reference/bf-overlay-role.md +27 -0
  145. package/ai/reference/bf-overlay-service.md +53 -0
  146. package/ai/reference/bf-overlay-stack-options.md +24 -0
  147. package/ai/reference/bf-overlay-stack.md +81 -0
  148. package/ai/reference/bf-overlay-template-context.md +24 -0
  149. package/ai/reference/bf-overscroll-stretch-directive.md +163 -0
  150. package/ai/reference/bf-page-context.md +24 -0
  151. package/ai/reference/bf-platform-service.md +53 -0
  152. package/ai/reference/bf-platform.md +28 -0
  153. package/ai/reference/bf-progress-size.md +26 -0
  154. package/ai/reference/bf-progress-tone.md +28 -0
  155. package/ai/reference/bf-radio-group-harness.md +49 -0
  156. package/ai/reference/bf-radio-group-orientation.md +26 -0
  157. package/ai/reference/bf-radio-option.md +30 -0
  158. package/ai/reference/bf-refresher-refresh-event.md +22 -0
  159. package/ai/reference/bf-segment-option.md +29 -0
  160. package/ai/reference/bf-segmented-harness.md +48 -0
  161. package/ai/reference/bf-segmented-size.md +29 -0
  162. package/ai/reference/bf-select-harness.md +70 -0
  163. package/ai/reference/bf-select-option.md +29 -0
  164. package/ai/reference/bf-sheet-auto-focus.md +29 -0
  165. package/ai/reference/bf-sheet-options.md +32 -0
  166. package/ai/reference/bf-sheet-ref.md +45 -0
  167. package/ai/reference/bf-sheet-service.md +34 -0
  168. package/ai/reference/bf-shell-config-mode.md +28 -0
  169. package/ai/reference/bf-shell-config.md +38 -0
  170. package/ai/reference/bf-shell-dynamic-type-limit.md +21 -0
  171. package/ai/reference/bf-shell-dynamic-type.md +25 -0
  172. package/ai/reference/bf-shell-mode.md +28 -0
  173. package/ai/reference/bf-shell-testing-options.md +26 -0
  174. package/ai/reference/bf-size.md +45 -0
  175. package/ai/reference/bf-spinner-size.md +27 -0
  176. package/ai/reference/bf-stack-change.md +22 -0
  177. package/ai/reference/bf-stack-reuse-strategy.md +32 -0
  178. package/ai/reference/bf-stack-transition-event.md +26 -0
  179. package/ai/reference/bf-stack-transition.md +26 -0
  180. package/ai/reference/bf-stacked-overlay.md +23 -0
  181. package/ai/reference/bf-system-bars-style.md +38 -0
  182. package/ai/reference/bf-tab-bar-controller.md +51 -0
  183. package/ai/reference/bf-tab-bar-host.md +52 -0
  184. package/ai/reference/bf-tab-context.md +32 -0
  185. package/ai/reference/bf-tab-links.md +37 -0
  186. package/ai/reference/bf-testing-options.md +27 -0
  187. package/ai/reference/bf-text-scale-service.md +35 -0
  188. package/ai/reference/bf-theme.md +27 -0
  189. package/ai/reference/bf-toast-action.md +25 -0
  190. package/ai/reference/bf-toast-controller.md +17 -0
  191. package/ai/reference/bf-toast-dismiss-reason.md +29 -0
  192. package/ai/reference/bf-toast-options.md +29 -0
  193. package/ai/reference/bf-toast-position.md +26 -0
  194. package/ai/reference/bf-toast-ref.md +41 -0
  195. package/ai/reference/bf-toast-service.md +39 -0
  196. package/ai/reference/bf-toggle-label-position.md +26 -0
  197. package/ai/reference/bf-transition-kind.md +28 -0
  198. package/ai/reference/bf-validation-message.md +31 -0
  199. package/ai/reference/bf-validation-messages.md +33 -0
  200. package/ai/reference/bf-view-event.md +28 -0
  201. package/ai/reference/bf-view-lifecycle.md +41 -0
  202. package/ai/reference/bf-view-state.md +28 -0
  203. package/ai/reference/bf_default_labels.md +21 -0
  204. package/ai/reference/bf_default_values.md +21 -0
  205. package/ai/reference/bf_defaults.md +21 -0
  206. package/ai/reference/bf_desktop_toast_defaults.md +21 -0
  207. package/ai/reference/bf_dialog_data.md +17 -0
  208. package/ai/reference/bf_labels.md +21 -0
  209. package/ai/reference/bf_overlay_data.md +19 -0
  210. package/ai/reference/bf_overlay_host.md +21 -0
  211. package/ai/reference/bf_sheet_default_snap_points.md +17 -0
  212. package/ai/reference/bf_shell_config.md +21 -0
  213. package/ai/reference/bf_shell_defaults.md +21 -0
  214. package/ai/reference/bf_tab_bar.md +21 -0
  215. package/ai/reference/bf_tab_bar_controller.md +21 -0
  216. package/ai/reference/bf_tab_context.md +21 -0
  217. package/ai/reference/bf_tab_links.md +21 -0
  218. package/ai/reference/bf_theme_storage.md +19 -0
  219. package/ai/reference/create-transition.md +36 -0
  220. package/ai/reference/duration_fast.md +17 -0
  221. package/ai/reference/duration_md.md +17 -0
  222. package/ai/reference/duration_overlay.md +17 -0
  223. package/ai/reference/duration_push.md +17 -0
  224. package/ai/reference/fade-down.md +26 -0
  225. package/ai/reference/fade-in.md +25 -0
  226. package/ai/reference/fade-out.md +25 -0
  227. package/ai/reference/fade-up.md +26 -0
  228. package/ai/reference/inject-shell-mode.md +23 -0
  229. package/ai/reference/install-bf-jsdom-shims.md +31 -0
  230. package/ai/reference/ios_duration.md +17 -0
  231. package/ai/reference/ios_easing.md +17 -0
  232. package/ai/reference/is-text-entry.md +25 -0
  233. package/ai/reference/max_complete_ms.md +17 -0
  234. package/ai/reference/md-transition.md +31 -0
  235. package/ai/reference/md_duration.md +17 -0
  236. package/ai/reference/md_easing.md +17 -0
  237. package/ai/reference/overlay_duration.md +21 -0
  238. package/ai/reference/overlay_easing.md +19 -0
  239. package/ai/reference/overlay_reduced_duration.md +17 -0
  240. package/ai/reference/pop-frames-at.md +25 -0
  241. package/ai/reference/prefers-reduced-motion.md +25 -0
  242. package/ai/reference/provide-bf-defaults.md +33 -0
  243. package/ai/reference/provide-bf-desktop-toast.md +28 -0
  244. package/ai/reference/provide-bf-labels.md +38 -0
  245. package/ai/reference/provide-bf-shell-testing.md +34 -0
  246. package/ai/reference/provide-bf-shell.md +41 -0
  247. package/ai/reference/provide-bf-testing.md +33 -0
  248. package/ai/reference/reduced-transition.md +28 -0
  249. package/ai/reference/reduced_duration.md +17 -0
  250. package/ai/reference/resolve-shell-mode.md +33 -0
  251. package/ai/reference/scale-in.md +25 -0
  252. package/ai/reference/settle-duration.md +25 -0
  253. package/ai/reference/settle-pop.md +33 -0
  254. package/ai/reference/settle_easing.md +17 -0
  255. package/ai/reference/should-complete-pop.md +26 -0
  256. package/ai/reference/slide-down.md +26 -0
  257. package/ai/reference/slide-up.md +26 -0
  258. package/ai/reference/with-bf-shell-routing.md +19 -0
  259. package/bin/bf-ui.js +81 -0
  260. package/fesm2022/bitforgehq-ui-icons.mjs +230 -0
  261. package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -0
  262. package/fesm2022/bitforgehq-ui-mobile.mjs +5530 -0
  263. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -0
  264. package/fesm2022/bitforgehq-ui-number-input.mjs +680 -0
  265. package/fesm2022/bitforgehq-ui-number-input.mjs.map +1 -0
  266. package/fesm2022/bitforgehq-ui-overlay.mjs +2675 -0
  267. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -0
  268. package/fesm2022/bitforgehq-ui-testing.mjs +402 -0
  269. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -0
  270. package/fesm2022/bitforgehq-ui.mjs +7765 -0
  271. package/fesm2022/bitforgehq-ui.mjs.map +1 -0
  272. package/llms-full.txt +25394 -0
  273. package/llms.txt +329 -0
  274. package/package.json +94 -0
  275. package/theme.css +253 -0
  276. package/types/bitforgehq-ui-icons.d.ts +50 -0
  277. package/types/bitforgehq-ui-mobile.d.ts +2914 -0
  278. package/types/bitforgehq-ui-number-input.d.ts +282 -0
  279. package/types/bitforgehq-ui-overlay.d.ts +1514 -0
  280. package/types/bitforgehq-ui-testing.d.ts +495 -0
  281. package/types/bitforgehq-ui.d.ts +3499 -0
@@ -0,0 +1,517 @@
1
+ # bf-segmented
2
+
3
+ Segmented control for switching between two to four views, keyboard-navigable like radios
4
+
5
+ `BfSegmentedComponent` from `@bitforgehq/ui` · Forms · since 0.1.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import { BfSegmentedComponent, type BfSegmentOption } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-segmented overview.
15
+ *
16
+ * Live demo of `bf-segmented`: the default width and a full-width control sharing a number value
17
+ * (the range in days); then the sm, md and lg sizes in a row, and a disabled control with a
18
+ * selected segment (forced-colours coverage for a disabled selected item).
19
+ */
20
+ @Component({
21
+ selector: 'app-example-segmented-overview',
22
+ imports: [BfSegmentedComponent],
23
+ template: `
24
+ <div class="flex flex-col gap-3">
25
+ <bf-segmented label="Range" [options]="ranges" [(value)]="range" />
26
+ <bf-segmented label="Range" [options]="ranges" [(value)]="range" [fullWidth]="true" />
27
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
28
+ <div data-sizes class="flex flex-wrap items-center gap-3">
29
+ <bf-segmented size="sm" label="Small" [options]="sizeOptions" value="sm" />
30
+ <bf-segmented label="Medium" [options]="sizeOptions" value="md" />
31
+ <bf-segmented size="lg" label="Large" [options]="sizeOptions" value="lg" />
32
+ </div>
33
+ <bf-segmented
34
+ data-disabled
35
+ label="Disabled"
36
+ [options]="sizeOptions"
37
+ value="md"
38
+ [disabled]="true"
39
+ />
40
+ </div>
41
+ `,
42
+ })
43
+ export class SegmentedOverviewExampleComponent {
44
+ /** The range in days: a number value. */
45
+ readonly range = signal<number | null>(1);
46
+ /** The size row: each control selects its own step. */
47
+ readonly sizeOptions: BfSegmentOption[] = [
48
+ { value: 'sm', label: 'sm · 32' },
49
+ { value: 'md', label: 'md · 40' },
50
+ { value: 'lg', label: 'lg · 48' },
51
+ ];
52
+ readonly ranges: BfSegmentOption<number>[] = [
53
+ { value: 1, label: 'Day' },
54
+ { value: 7, label: 'Week' },
55
+ { value: 30, label: 'Month' },
56
+ ];
57
+
58
+ readonly status = computed(
59
+ () =>
60
+ `Range: ${this.ranges.find((r) => r.value === this.range())?.label ?? ''} (${this.range() ?? 0} days)`,
61
+ );
62
+ }
63
+ ```
64
+
65
+ ## Overview
66
+
67
+ Segmented control for switching between two to four views. Behaves like a radio group for
68
+ keyboard and assistive tech; implements the Signal Forms value contract. Generic over the option
69
+ value (a string by default), `null` while nothing is selected.
70
+
71
+ A segmented control switches a view between two to four options: Day / Week / Month, All / Open / Done. It is a radio group for keyboard and screen readers and binds to Signal Forms like any other field.
72
+
73
+ ## When to use
74
+
75
+ Use `bf-segmented` to switch a view between two to four short options: Day / Week / Month, List / Board. Use [bf-radio-group](https://bitforge-ui.web.app/components/radio-group) instead when the options need descriptions or there are more of them, [bf-select](https://bitforge-ui.web.app/components/select) for a long list, and [bf-chip](https://bitforge-ui.web.app/components/chip)s for filters that combine.
76
+
77
+ ## Basic usage
78
+
79
+ ```ts
80
+ import { Component, signal } from '@angular/core';
81
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
82
+
83
+ /** A segmented control for a date range. */
84
+ @Component({
85
+ selector: 'app-example-segmented-basic',
86
+ imports: [BfSegmentedComponent],
87
+ template: `
88
+ <div class="flex flex-col items-start gap-2">
89
+ <bf-segmented label="Range" [options]="ranges" [(value)]="range" />
90
+ <p class="text-caption text-text-muted">range: {{ range() }}</p>
91
+ </div>
92
+ `,
93
+ })
94
+ export class SegmentedBasicExampleComponent {
95
+ readonly ranges: BfSegmentOption[] = [
96
+ { value: 'day', label: 'Day' },
97
+ { value: 'week', label: 'Week' },
98
+ { value: 'month', label: 'Month' },
99
+ ];
100
+ readonly range = signal<string | null>('day');
101
+ }
102
+ ```
103
+
104
+ `value` is `null` while no segment is selected; the first enabled segment then owns the tab stop.
105
+ `(changed)` emits the chosen value after a user change.
106
+
107
+ ## Typed values
108
+
109
+ The control is generic over the option value: numbers, ids and objects bind directly.
110
+ `BfSegmentOption` defaults to `string`; type it for anything else, and bind a `T | null` signal or
111
+ form field.
112
+
113
+ ```ts
114
+ import { Component, signal } from '@angular/core';
115
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
116
+
117
+ /** A segmented control of numbers: the bound value is a day count. */
118
+ @Component({
119
+ selector: 'app-example-segmented-typed',
120
+ imports: [BfSegmentedComponent],
121
+ template: `
122
+ <div class="flex flex-col items-start gap-2">
123
+ <bf-segmented label="Range" [options]="ranges" [(value)]="days" />
124
+ <p class="text-caption text-text-muted">days: {{ days() }}</p>
125
+ </div>
126
+ `,
127
+ })
128
+ export class SegmentedTypedExampleComponent {
129
+ readonly ranges: BfSegmentOption<number>[] = [
130
+ { value: 1, label: 'Day' },
131
+ { value: 7, label: 'Week' },
132
+ { value: 30, label: 'Month' },
133
+ ];
134
+ readonly days = signal<number | null>(7);
135
+ }
136
+ ```
137
+
138
+ Options are matched to `value` with `Object.is`; for object values that are equal but not the same
139
+ instance, pass `compareWith`, e.g. `[compareWith]="byDays"` with
140
+ `byDays = (a: Range, b: Range) => a.days === b.days` (called as `compareWith(optionValue, value)`).
141
+ An option whose value is `null` is not supported: `null` means nothing is selected, so such an
142
+ option never shows as selected.
143
+
144
+ Migrating from 0.2: "nothing selected" is `null` (it was `''`); type bound signals and fields
145
+ `T | null` and compare against `null`, or listen to `(changed)`.
146
+
147
+ ## Full width
148
+
149
+ ```ts
150
+ import { Component, signal } from '@angular/core';
151
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
152
+
153
+ /** A segmented control that fills its container, segments sharing the width. */
154
+ @Component({
155
+ selector: 'app-example-segmented-full-width',
156
+ imports: [BfSegmentedComponent],
157
+ template: `
158
+ <bf-segmented label="Filter" [options]="filters" [fullWidth]="true" [(value)]="filter" />
159
+ `,
160
+ })
161
+ export class SegmentedFullWidthExampleComponent {
162
+ protected readonly filters: BfSegmentOption[] = [
163
+ { value: 'all', label: 'All' },
164
+ { value: 'open', label: 'Open' },
165
+ { value: 'done', label: 'Done' },
166
+ ];
167
+ protected readonly filter = signal<string | null>('all');
168
+ }
169
+ ```
170
+
171
+ ## Sizes
172
+
173
+ `size` sets the height of the whole control (the track): `sm` 32 px, `md` 40 px (default), `lg` 48 px, so it lines up with a field or button of the same step. Unset, `size` follows `provideBfDefaults()`'s `controlSize`, `md` by default (`provideBfDefaults({ size: 'lg' })` enlarges every control of a Capacitor app; `{ size: 'sm' }` in a component's `providers` makes a dense sub-tree). Under a fine pointer each segment is its own hit area (24 px tall at `sm`); under a coarse pointer (touch) hit areas grow to 44 px at every step.
174
+
175
+ ```ts
176
+ import { Component, signal } from '@angular/core';
177
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
178
+
179
+ /** The sm, md and lg segmented sizes. */
180
+ @Component({
181
+ selector: 'app-example-segmented-sizes',
182
+ imports: [BfSegmentedComponent],
183
+ template: `
184
+ <div class="flex flex-col items-start gap-3">
185
+ <bf-segmented size="sm" label="Range" [options]="ranges" [(value)]="range" />
186
+ <bf-segmented label="Range" [options]="ranges" [(value)]="range" />
187
+ <bf-segmented size="lg" label="Range" [options]="ranges" [(value)]="range" />
188
+ </div>
189
+ `,
190
+ })
191
+ export class SegmentedSizesExampleComponent {
192
+ protected readonly ranges: BfSegmentOption[] = [
193
+ { value: 'day', label: 'Day' },
194
+ { value: 'week', label: 'Week' },
195
+ ];
196
+ protected readonly range = signal<string | null>('week');
197
+ }
198
+ ```
199
+
200
+ ## With Signal Forms
201
+
202
+ Bind the field instead of the value. Required, touched, errors, disabled and readonly flow in; the
203
+ first error shows under the track once the user leaves the control, and `errorMessage` shows at
204
+ once (e.g. a server error). `name` is accepted for Signal Forms but not rendered, because the
205
+ control has no native form element.
206
+
207
+ ```ts
208
+ import { Component, signal } from '@angular/core';
209
+ import { FormField, form } from '@angular/forms/signals';
210
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
211
+
212
+ /** A segmented control bound to a Signal Forms field. */
213
+ @Component({
214
+ selector: 'app-example-segmented-signal-forms',
215
+ imports: [FormField, BfSegmentedComponent],
216
+ template: `
217
+ <div class="flex flex-col items-start gap-2">
218
+ <bf-segmented label="Range" [options]="ranges" [formField]="report.range" />
219
+ <p class="text-caption text-text-muted">range: {{ report.range().value() }}</p>
220
+ </div>
221
+ `,
222
+ })
223
+ export class SegmentedSignalFormsExampleComponent {
224
+ protected readonly ranges: BfSegmentOption[] = [
225
+ { value: 'day', label: 'Day' },
226
+ { value: 'week', label: 'Week' },
227
+ ];
228
+ private readonly model = signal<{ range: string | null }>({ range: 'day' });
229
+ protected readonly report = form(this.model);
230
+ }
231
+ ```
232
+
233
+ ## Read-only
234
+
235
+ `readonly` shows the choice but blocks changes: clicks and the arrow, Home and End keys do nothing,
236
+ focus stays on the selected segment and Tab leaves the group. The track rests on the muted surface.
237
+
238
+ ```ts
239
+ import { Component } from '@angular/core';
240
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
241
+
242
+ /** A read-only segmented control. */
243
+ @Component({
244
+ selector: 'app-example-segmented-readonly',
245
+ imports: [BfSegmentedComponent],
246
+ template: `<bf-segmented label="Range" [options]="ranges" [readonly]="true" [value]="range" />`,
247
+ })
248
+ export class SegmentedReadonlyExampleComponent {
249
+ protected readonly ranges: BfSegmentOption[] = [
250
+ { value: 'day', label: 'Day' },
251
+ { value: 'week', label: 'Week' },
252
+ ];
253
+ protected readonly range = 'week';
254
+ }
255
+ ```
256
+
257
+ ## Naming the group
258
+
259
+ `label` is the group's accessible name (it is not shown). Without it, use `ariaLabel`, or
260
+ `ariaLabelledby` pointing at a visible heading. A group needs one of the three.
261
+
262
+ ```ts
263
+ import { Component, signal } from '@angular/core';
264
+ import { type BfSegmentOption, BfSegmentedComponent } from '@bitforgehq/ui';
265
+
266
+ /** A segmented control named by the heading above it. */
267
+ @Component({
268
+ selector: 'app-example-segmented-named',
269
+ imports: [BfSegmentedComponent],
270
+ template: `
271
+ <div class="flex flex-col items-start gap-2">
272
+ <h3 id="segmented-range-heading" class="text-heading">Range</h3>
273
+ <bf-segmented ariaLabelledby="segmented-range-heading" [options]="ranges" [(value)]="range" />
274
+ </div>
275
+ `,
276
+ })
277
+ export class SegmentedNamedExampleComponent {
278
+ protected readonly ranges: BfSegmentOption[] = [
279
+ { value: 'day', label: 'Day' },
280
+ { value: 'week', label: 'Week' },
281
+ ];
282
+ protected readonly range = signal<string | null>('day');
283
+ }
284
+ ```
285
+
286
+ ## Validation
287
+
288
+ Once the control is touched it shows the first error as text: the error's own `message`, else the default for its kind from `provideBfLabels()`'s `validation` labels (`required` "Fill in this field", `email`, `pattern`, `parse`, `min(n)`, `max(n)`, `minLength(n)` "Use at least n characters", `maxLength(n)`, `minDate(d)`, `maxDate(d)`, and `fallback` "Check this value" for any other kind), so it never turns red without text. Reactive Forms `Validators.*` errors carry no message and get the same defaults (`minlength`/`maxlength` read their limit from the error's `context`). An explicit `errorMessage` wins over both and shows at once, e.g. for a server error.
289
+
290
+ ```ts
291
+ import { Component, signal } from '@angular/core';
292
+ import { FormField, FormRoot, form, required } from '@angular/forms/signals';
293
+ import { BfButtonComponent, BfSegmentedComponent, type BfSegmentOption } from '@bitforgehq/ui';
294
+
295
+ /**
296
+ * A required segmented with the default message.
297
+ *
298
+ * `required(p.range)` has no message, so a submit with nothing chosen shows "Fill in this field".
299
+ */
300
+ @Component({
301
+ selector: 'app-example-segmented-validation',
302
+ imports: [FormField, FormRoot, BfButtonComponent, BfSegmentedComponent],
303
+ template: `
304
+ <form [formRoot]="report" class="flex flex-col items-start gap-3">
305
+ <bf-segmented label="Range" [options]="options" [formField]="report.range" />
306
+ <bf-button type="submit">Run report</bf-button>
307
+ </form>
308
+ `,
309
+ })
310
+ export class SegmentedValidationExampleComponent {
311
+ protected readonly options: BfSegmentOption[] = [
312
+ { value: 'day', label: 'Day' },
313
+ { value: 'week', label: 'Week' },
314
+ ];
315
+ private readonly model = signal<{ range: string | null }>({ range: null });
316
+ protected readonly report = form(
317
+ this.model,
318
+ (p) => {
319
+ required(p.range);
320
+ },
321
+ { submission: { action: () => Promise.resolve(undefined) } },
322
+ );
323
+ }
324
+ ```
325
+
326
+ ## Desktop
327
+
328
+ Hovering an inactive segment darkens its label to `text`, and the active one takes a `surface-muted` fill, with the pointer cursor; a read-only control has no hover. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The focus ring, on a 2 px `surface` offset, shows on keyboard focus only. Under a fine pointer each segment is its own hit area (24 px tall at `sm`). Desktop density is the default `md` step; `provideBfDefaults({ size: 'sm' })`, or `{ size: 'sm' }` in a component's `providers`, makes a dense toolbar, table or sidebar ([density guide](https://bitforge-ui.web.app/density)).
329
+
330
+ ## Theming
331
+
332
+ - Track: `surface-sunken`.
333
+ - Active segment: `surface` / `text` with the `shadow-segment` elevation and a 1.5 px inset `primary-strong` outline (≥ 3:1 on `surface` and the track, so the selection does not rest on the fill), hover `surface-muted` (under `forced-colors`, `Highlight` / `HighlightText` with a `CanvasText` outline). Inactive: `text-muted`, hover `text`. Pressed (editable only): `surface-pressed` with `text` (the selected segment keeps `Highlight` under `forced-colors`).
334
+ - Read-only: track `surface-muted`, no hover, default cursor. Error: the track gets a 1.5 px `danger-border` ring and the message below is `danger`.
335
+ - Focus ring: `ring`, drawn past a 2 px `surface` offset so it sits on `surface` (≥ 3:1) rather than on the track. Disabled segments: `text-disabled` with `cursor-not-allowed`; an unselected one takes the `surface-sunken` fill, the selected one keeps its `surface` pill without the shadow, outlined in `text-disabled` (never opacity).
336
+ - Shape (track and segments): `rounded-control` (`--radius-control`, `9999px` by default).
337
+
338
+ Segments are at least 24/32/40 px tall inside the track's 4 px padding. Under a fine pointer the segment is its hit area; under a coarse pointer (touch) an invisible extension gives each a 44 px hit area. A label longer than its segment wraps between its words and the segment grows taller; a segment never gets narrower than its longest word, so no label is cut or broken between letters. The side padding is in px, so under iOS Dynamic Type the extra width goes to the label. When the segments cannot fit even so (a long compound word in a narrow column, three segments at iOS 235 % on a phone), the control scrolls sideways; at 200 % three one-word segments still fit a phone side by side.
339
+
340
+ ## Accessibility
341
+
342
+ `role="radiogroup"` with `aria-label` from `label` (else `ariaLabel`, or `aria-labelledby` from `ariaLabelledby`); each segment is a `<button role="radio">` with `aria-checked` mirroring selection. Only the selected (or first enabled) segment sits in the tab order — the rest carry `tabindex="-1"` — and ArrowRight/Down, ArrowLeft/Up, Home and End move the selection and focus like a native radio group, matching platform expectations. `disabled` segments are skipped by keyboard navigation and get the native attribute. The group carries `aria-required`, and once touched and invalid `aria-invalid="true"` with `aria-describedby` and `aria-errormessage` pointing at the error text. Read-only: `aria-readonly="true"` on the group; the tab stop stays focusable and selection by pointer or keyboard is ignored. `focus()` focuses the tab-stop segment; Signal Forms calls it from `focusBoundControl()`. Under forced colours (Windows High Contrast) the selected segment opts out of the adjustment (`forced-color-adjust: none`) and paints `Highlight`/`HighlightText` itself with a `CanvasText` selected outline, focus outline and focus ring (on a `Canvas` offset) and no shadow, so it paints system colours only and so no text backplate hides its label; a disabled selected segment's outline is `GrayText` instead; the others repaint in `ButtonText`.
343
+
344
+ ### Keyboard
345
+
346
+ While `readonly` the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> move nothing; <kbd>Tab</kbd> still leaves the control.
347
+
348
+ | Key | Action |
349
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
350
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves focus into and out of the control: one tab stop, on the selected segment (else the first enabled one) |
351
+ | <kbd>ArrowRight</kbd> / <kbd>ArrowDown</kbd> | Selects and focuses the next enabled segment in DOM order, wrapping at the end; not mirrored under `dir="rtl"` yet (a known gap) |
352
+ | <kbd>ArrowLeft</kbd> / <kbd>ArrowUp</kbd> | Selects and focuses the previous enabled segment in DOM order, wrapping at the start; not mirrored under `dir="rtl"` yet |
353
+ | <kbd>Home</kbd> / <kbd>End</kbd> | Selects and focuses the first / last enabled segment |
354
+ | <kbd>Space</kbd> / <kbd>Enter</kbd> | Selects the focused segment |
355
+
356
+ ## Inputs
357
+
358
+ | Name | Type | Default | Description |
359
+ | ---------------- | ----------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
360
+ | `options` | `readonly BfSegmentOption<T>[]` | required | The segments. |
361
+ | `compareWith` | `BfCompareWith<T>` | `Object.is` | Equality for matching `value` to an option (default `Object.is`); pass one for object values. |
362
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step of the control (track): sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
363
+ | `fullWidth` | `boolean` | `false` | Stretch the control and share the width between segments. |
364
+ | `disabled` | `boolean` | `false` | Disables every segment. Set by `[formField]`; do not bind it yourself. |
365
+ | `readonly` | `boolean` | `false` | Shows the choice but blocks changes; the tab stop stays focusable and the group exposes `aria-readonly`. Set by `[formField]`; do not bind it yourself. |
366
+ | `required` | `boolean` | `false` | Marks the choice required (`aria-required` on the group). Set by `[formField]`; do not bind it yourself. |
367
+ | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
368
+ | `touched` | `boolean` | `false` | Whether the user has left the field; errors show only once touched. Set by `[formField]`; do not bind it yourself. |
369
+ | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms; the first message is shown once touched. Set by `[formField]`; do not bind it yourself. |
370
+ | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
371
+ | `label` | `string` | `''` | Accessible name of the group (not rendered visibly). |
372
+ | `ariaLabel` | `string` | `''` | Accessible name when `label` is not set. |
373
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the group (e.g. a visible heading) when `label` is not set. |
374
+ | `name` | `string` | `''` | Name of the bound field. Not rendered, because the control has no native form element; kept so Signal Forms can bind it. Set by `[formField]`; do not bind it yourself. |
375
+ | `id` | `string` | `generated` | Group id; generated when not set. |
376
+ | `value` | `T \| null` | `null` | The selected option's value, `null` while nothing is selected; two-way bindable and bound by `[formField]`. Two-way: `[(value)]`. |
377
+
378
+ ## Outputs
379
+
380
+ | Name | Payload | Description |
381
+ | --------- | ------- | ------------------------------------------------------------------------------------------------------- |
382
+ | `changed` | `T` | Emitted with the newly selected value after a user change (never `null`). |
383
+ | `touch` | `void` | Emitted on blur so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
384
+
385
+ ## Methods
386
+
387
+ ### `focus(options?: FocusOptions): void`
388
+
389
+ Focuses the segment that owns the tab stop; Signal Forms calls it from `focusBoundControl()`.
390
+
391
+ | Parameter | Type | Description |
392
+ | ---------- | -------------- | --------------------------------------------------------------------- |
393
+ | `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
394
+
395
+ ### `select(next: T): void`
396
+
397
+ Selects the option whose value matches `next` under `compareWith`, as a click would.
398
+
399
+ | Parameter | Type | Description |
400
+ | --------- | ---- | ---------------------------------- |
401
+ | `next` | `T` | The value of the option to select. |
402
+
403
+ ### `selectAt(index: number): void`
404
+
405
+ Selects the option at `index` unless the control is disabled or read-only, the option is
406
+ missing, or it is already selected.
407
+
408
+ | Parameter | Type | Description |
409
+ | --------- | -------- | ---------------------------------------- |
410
+ | `index` | `number` | The position of the option in `options`. |
411
+
412
+ ## Notes
413
+
414
+ ### Do
415
+
416
+ - Keep labels to one short word so all segments fit on a phone; a longer label wraps onto a second line, and a control whose segments cannot fit scrolls sideways.
417
+ - Give the group a `label` (or `ariaLabel` / `ariaLabelledby`); it is the accessible name.
418
+
419
+ ### Don't
420
+
421
+ - Don't use it for more than four options or for actions; use `bf-select` or buttons.
422
+
423
+ ## Migration notes
424
+
425
+ ### 0.3.0
426
+
427
+ **Breaking:** typed values. `BfSegmentedComponent<T = string>` is generic over the option value
428
+ (`BfSegmentOption<T = string>`); `value` is `T | null` and defaults to `null` (was `''`), with
429
+ nothing checked and the first enabled segment owning the tab stop; `changed` emits `T` (was
430
+ `string`). New `compareWith` (default `Object.is`) matches `value` to an option. `tabStop` is now
431
+ the index of the segment that owns the tab stop (was its value, `-1` when every option is
432
+ disabled), and `select(value)` matches through `compareWith`. See Migration 6, 7 and 10.
433
+
434
+ **Breaking:** heights follow the shared `BfSize` scale: the track is `sm` 32, `md` 40 (default,
435
+ was 44), `lg` 48 px (new); unset, `size` follows `provideBfDefaults()`'s `controlSize`. The
436
+ segments' 44 px hit extension applies only under a coarse pointer (`pointer-coarse:`). See
437
+ Migration 3.
438
+
439
+ **Behaviour:** an error without a message shows its kind's default text (was a red border only):
440
+ once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
441
+ `validation` text for its kind (Signal Forms and Reactive interop kinds), else
442
+ `validation.fallback`. `errorMessage` still wins. See Migration 1.
443
+
444
+ **Behaviour:** the selected segment carries a 1.5 px inset `primary-strong` outline (≥ 3:1 on
445
+ `surface` and the track), so the selection no longer rests on a fill 1.18:1 from the track; a
446
+ disabled selected segment is outlined in `text-disabled`. The focus ring sits on a 2 px `surface`
447
+ offset (≥ 3:1) instead of on the track (2.79:1). The track and segments use `rounded-control`
448
+ (`--radius-control`).
449
+
450
+ **Behaviour:** under forced colours the selected segment's outline, focus ring (on a `Canvas`
451
+ offset) and shadow use system colours only (`CanvasText`; no shadow), since it opts out of the
452
+ adjustment; a disabled selected segment's outline is `GrayText` instead of the authored
453
+ `text-disabled`.
454
+
455
+ JSDoc of `segmented.component.ts` for the API reference: `@summary`/`@since` on the exported
456
+ symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
457
+ leaves them out. No behaviour change.
458
+
459
+ **Behaviour:** a pressed state (F-045): an editable segment takes `surface-pressed` (an unselected one with `text`, which holds 4.5:1 there) while held down. None while disabled or read-only; under forced colours the system colours win.
460
+
461
+ **Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
462
+ `aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
463
+ elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
464
+ (F-020).
465
+
466
+ **Behaviour:** labels stay whole at every text size. The segments' side padding is px
467
+ (`px-[12px]`/`px-[14px]`/`px-[18px]`, the rem values at the default size) and so is the track's
468
+ (`px-[4px]`, `gap-[2px]`), so Dynamic Type spends the width on the label; a segment shrinks only to
469
+ its longest word (`min-w-min`, was `min-w-0`) and a label wraps between words (was
470
+ `wrap-anywhere`, which broke "System" letter by letter at iOS AX5). When the segments still cannot
471
+ fit, the track grows past the control and a new `[data-bf-segmented-scroller]` wrapper
472
+ (`overflow-x-auto`, padded so the focus ring is not clipped) scrolls it sideways; the host gains
473
+ `max-w-full`. On the demo's Theme control the three segments fit side by side at iOS 200 %
474
+ (WCAG 1.4.4) on a 393 px phone, and scroll at 235 %. See Migration 38.
475
+
476
+ ### 0.2.0
477
+
478
+ Each segment's hit area reaches 44 px through an `::after` extension (`after:-inset-y-1` on md
479
+ 36 px, `after:-inset-y-1.5` on sm 32 px); the visual size is unchanged. Labels render in a
480
+ `data-label` span (it wraps, see "Changed" below), and segments are `min-w-0`, so a long label in
481
+ `fullWidth` no longer overflows its share. Disabled: unselected segments take `surface-sunken`, the selected one
482
+ keeps its `surface` pill without the shadow; text `text-disabled` as before.
483
+
484
+ Fixed: every segment shows a pointer cursor while enabled (`not-allowed` when disabled). The
485
+ active segment now also tints on hover (`enabled:hover:bg-surface-muted`), matching the active
486
+ tab's hover in `bf-tab-bar`.
487
+
488
+ Added: validation state, mirroring `bf-radio-group`. New `required`, `invalid`, `touched`, `errors`
489
+ and `errorMessage` inputs; once touched and invalid (or with an explicit `errorMessage`) the first
490
+ message renders under the track (`id="{id}-error"`), the radiogroup gets `aria-invalid="true"` and
491
+ `aria-describedby`, and the track a 1.5 px `danger-border` ring; `aria-required` follows
492
+ `required`. A required segmented choice no longer fails silently.
493
+
494
+ Layout change: the host is now a column (`inline-flex flex-col items-start gap-1.5`) so the message
495
+ sits under the track. Without an error it renders as before; `justify-*` or `items-*` utilities a
496
+ consumer put on the host now act on the column.
497
+
498
+ Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
499
+ could be edited; it now declares `readonly` and blocks changes. While read-only `select()` and the
500
+ arrow, Home and End keys do nothing, the group carries `aria-readonly="true"`, the tab stop stays
501
+ focusable, and the track rests on `surface-muted` without segment hover (default cursor).
502
+ `ariaLabel` / `ariaLabelledby` name the group when `label` is not set. `focus(options?)` focuses the
503
+ tab-stop segment, so Signal Forms' `focusBoundControl()` works. `name` is documented as bound but
504
+ not rendered (the control has no native form element).
505
+
506
+ Fixed: under forced colours the selected label is legible. The selected segment adds
507
+ `forced-colors:forced-color-adjust-none` next to `Highlight`/`HighlightText` (Chromium painted a
508
+ Canvas text backplate under `HighlightText`, white on white in light, black on black in dark) and
509
+ a `CanvasText` focus outline; unselected segments repaint in `ButtonText` on a transparent fill.
510
+
511
+ Changed: a long label wraps instead of truncating. Segments are `min-h-8`/`min-h-9` with `py-1`
512
+ (were fixed `h-8`/`h-9`, `whitespace-nowrap`), the label span is `wrap-anywhere text-center`, so a
513
+ segment grows taller rather than showing "Tages…".
514
+
515
+ ### 0.1.0
516
+
517
+ New `bf-segmented`: a `role="radiogroup"` control for switching between two to four views, implementing `FormValueControl<string>`. `sm`/`md` (32/36 px) item heights, optional `fullWidth`. Keyboard arrow keys move the roving-tabindex selection and wrap around, skipping disabled options; `Home`/`End` jump to the first/last enabled option. The roving tab stop (`tabStop`) now falls back to the first enabled option when the selected value is missing or disabled, instead of leaving every segment at `tabIndex -1`. A keyboard selection also moves DOM focus to the newly selected button. The active segment's fill is now visible under forced-colors (`forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]`), and the inactive segment explicitly clears its background there.