@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,432 @@
1
+ # bf-radio-group
2
+
3
+ Group of radio buttons for choosing one option, with optional per-option descriptions
4
+
5
+ `BfRadioGroupComponent` from `@bitforgehq/ui` · Forms · since 0.1.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import { BfRadioGroupComponent, type BfRadioOption } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-radio-group overview.
15
+ *
16
+ * Live demo of `bf-radio-group`: a vertical group with a description, and a horizontal one with
17
+ * number values that starts at `null`; then the sm, md and lg sizes in a row.
18
+ */
19
+ @Component({
20
+ selector: 'app-example-radio-group-overview',
21
+ imports: [BfRadioGroupComponent],
22
+ template: `
23
+ <div class="flex flex-col gap-4">
24
+ <bf-radio-group label="Plan" [options]="plans" [(value)]="plan" />
25
+ <bf-radio-group label="Seats" orientation="horizontal" [options]="seats" [(value)]="seat" />
26
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
27
+ <div data-sizes class="flex flex-wrap items-center gap-3">
28
+ <bf-radio-group size="sm" label="sm · 32 px" [options]="sizeOptions" value="a" />
29
+ <bf-radio-group label="md · 40 px" [options]="sizeOptions" value="a" />
30
+ <bf-radio-group size="lg" label="lg · 48 px" [options]="sizeOptions" value="a" />
31
+ </div>
32
+ </div>
33
+ `,
34
+ })
35
+ export class RadioGroupOverviewExampleComponent {
36
+ readonly plan = signal('team');
37
+ readonly plans: BfRadioOption[] = [
38
+ { value: 'solo', label: 'Solo', description: 'For a single workspace' },
39
+ { value: 'team', label: 'Team', description: 'Shared projects and roles' },
40
+ { value: 'org', label: 'Organisation', description: 'Talk to sales', disabled: true },
41
+ ];
42
+
43
+ /** Number values; `null` until a seat count is picked. */
44
+ readonly seat = signal<number | null>(null);
45
+ /** The size row's options. */
46
+ readonly sizeOptions: BfRadioOption[] = [
47
+ { value: 'a', label: 'Option A' },
48
+ { value: 'b', label: 'Option B' },
49
+ ];
50
+ readonly seats: BfRadioOption<number>[] = [
51
+ { value: 1, label: '1 seat' },
52
+ { value: 5, label: '5 seats' },
53
+ { value: 10, label: '10 seats' },
54
+ ];
55
+
56
+ readonly status = computed(() => `Plan: ${this.plan()} · Seats: ${this.seat() ?? 'none'}`);
57
+ }
58
+ ```
59
+
60
+ ## Overview
61
+
62
+ A group of radio buttons for choosing one of a few options, with an optional description per
63
+ option. Implements the Signal Forms value contract; generic over the option value (a string by
64
+ default), `null` while nothing is selected.
65
+
66
+ Radio buttons for picking exactly one of a few visible options. Use it when the options need to be read side by side; use `bf-select` when the list is long, and `bf-segmented` when the choice switches a view.
67
+
68
+ ## When to use
69
+
70
+ Use `bf-radio-group` to choose exactly one of two to six options that should be read side by side, optionally with a description each. Use [bf-select](https://bitforge-ui.web.app/components/select) instead for a longer list, [bf-segmented](https://bitforge-ui.web.app/components/segmented) for a compact switch between views, and [bf-checkbox](https://bitforge-ui.web.app/components/checkbox) when several may be chosen.
71
+
72
+ ## Basic usage
73
+
74
+ ```ts
75
+ import { Component, signal } from '@angular/core';
76
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
77
+
78
+ /** A radio group of views, one with a description. */
79
+ @Component({
80
+ selector: 'app-example-radio-group-basic',
81
+ imports: [BfRadioGroupComponent],
82
+ template: `
83
+ <div class="flex flex-col gap-2">
84
+ <bf-radio-group label="View" [options]="views" [(value)]="view" />
85
+ <p class="text-caption text-text-muted">view: {{ view() }}</p>
86
+ </div>
87
+ `,
88
+ })
89
+ export class RadioGroupBasicExampleComponent {
90
+ readonly views: BfRadioOption[] = [
91
+ { value: 'day', label: 'Day' },
92
+ { value: 'week', label: 'Week', description: 'Seven days at a glance' },
93
+ ];
94
+ readonly view = signal<string | null>('day');
95
+ }
96
+ ```
97
+
98
+ `value` is `null` while nothing is checked. `(changed)` emits the chosen value after a user change.
99
+
100
+ ## Typed values
101
+
102
+ The group is generic over the option value: numbers, ids and objects bind directly.
103
+ `BfRadioOption` defaults to `string`; type it for anything else, and bind a `T | null` signal or
104
+ form field.
105
+
106
+ ```ts
107
+ import { Component, signal } from '@angular/core';
108
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
109
+
110
+ /** A radio group of numbers: the bound value is a `number | null`. */
111
+ @Component({
112
+ selector: 'app-example-radio-group-typed',
113
+ imports: [BfRadioGroupComponent],
114
+ template: `
115
+ <div class="flex flex-col gap-2">
116
+ <bf-radio-group label="Seats" [options]="seats" [(value)]="seat" />
117
+ <p class="text-caption text-text-muted">seat: {{ seat() ?? 'null' }}</p>
118
+ </div>
119
+ `,
120
+ })
121
+ export class RadioGroupTypedExampleComponent {
122
+ readonly seats: BfRadioOption<number>[] = [
123
+ { value: 1, label: '1 seat' },
124
+ { value: 5, label: '5 seats' },
125
+ ];
126
+ readonly seat = signal<number | null>(null);
127
+ }
128
+ ```
129
+
130
+ Options are matched to `value` with `Object.is`; for object values that are equal but not the same
131
+ instance, pass `compareWith`, e.g. `[compareWith]="byCode"` with
132
+ `byCode = (a: Plan, b: Plan) => a.code === b.code` (called as `compareWith(optionValue, value)`).
133
+ A string, number or boolean option value is also the radio's native `value`; an object option's is
134
+ its index. A choice is mapped back by position, so the model always gets the option's real value.
135
+ An option whose value is `null` is not supported: `null` means nothing is selected, so such an
136
+ option never shows as selected.
137
+
138
+ Migrating from 0.2: "nothing checked" is `null` (it was `''`); type bound signals and fields
139
+ `T | null` and compare against `null`, or listen to `(changed)`.
140
+
141
+ ## Horizontal
142
+
143
+ ```ts
144
+ import { Component, signal } from '@angular/core';
145
+ import { FormField, form } from '@angular/forms/signals';
146
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
147
+
148
+ /** A horizontal radio group bound to a Signal Forms field. */
149
+ @Component({
150
+ selector: 'app-example-radio-group-horizontal',
151
+ imports: [FormField, BfRadioGroupComponent],
152
+ template: `
153
+ <bf-radio-group
154
+ label="Priority"
155
+ orientation="horizontal"
156
+ [options]="priorities"
157
+ [formField]="task.priority"
158
+ />
159
+ `,
160
+ })
161
+ export class RadioGroupHorizontalExampleComponent {
162
+ protected readonly priorities: BfRadioOption[] = [
163
+ { value: 'low', label: 'Low' },
164
+ { value: 'medium', label: 'Medium' },
165
+ { value: 'high', label: 'High' },
166
+ ];
167
+ private readonly model = signal<{ priority: string | null }>({ priority: 'medium' });
168
+ protected readonly task = form(this.model);
169
+ }
170
+ ```
171
+
172
+ ## Sizes
173
+
174
+ `sm` (32 px rows, 18 px ring, 14 px label), `md` (default, 40 px rows, 22 px ring), `lg` (48 px rows, 24 px ring); the dot scales with the ring. 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 the row is the hit area (32 px and up, above the 24 px of WCAG 2.5.8); under a coarse pointer (touch) `sm` and `md` rows grow to 44 px.
175
+
176
+ ```ts
177
+ import { Component, signal } from '@angular/core';
178
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
179
+
180
+ /** A small radio group for a dense panel. */
181
+ @Component({
182
+ selector: 'app-example-radio-group-sizes',
183
+ imports: [BfRadioGroupComponent],
184
+ template: `
185
+ <bf-radio-group size="sm" label="Density" [options]="densities" [(value)]="density" />
186
+ `,
187
+ })
188
+ export class RadioGroupSizesExampleComponent {
189
+ protected readonly densities: BfRadioOption[] = [
190
+ { value: 'compact', label: 'Compact' },
191
+ { value: 'comfortable', label: 'Comfortable' },
192
+ ];
193
+ protected readonly density = signal<string | null>('compact');
194
+ }
195
+ ```
196
+
197
+ ## Read-only
198
+
199
+ `readonly` shows the choice but blocks changes: clicks, Space and the arrow keys do nothing, and
200
+ the options stay in the tab order on the muted surface. Native radios move focus and selection
201
+ together on the arrow keys, so cancelling them also keeps focus on the checked option: a keyboard
202
+ user tabs to the group and hears the current choice, but cannot walk the other options. Signal
203
+ Forms binds it from `readonly()` rules.
204
+
205
+ ```ts
206
+ import { Component } from '@angular/core';
207
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
208
+
209
+ /** A read-only radio group: it shows the plan and blocks changes. */
210
+ @Component({
211
+ selector: 'app-example-radio-group-readonly',
212
+ imports: [BfRadioGroupComponent],
213
+ template: `<bf-radio-group label="Plan" [options]="plans" [readonly]="true" [value]="plan" />`,
214
+ })
215
+ export class RadioGroupReadonlyExampleComponent {
216
+ protected readonly plans: BfRadioOption[] = [
217
+ { value: 'starter', label: 'Starter' },
218
+ { value: 'team', label: 'Team' },
219
+ ];
220
+ protected readonly plan = 'team';
221
+ }
222
+ ```
223
+
224
+ ## Without a visible label
225
+
226
+ When a heading or table cell already names the choice, skip `label` and give the group `ariaLabel`,
227
+ or `ariaLabelledby` pointing at that element. A group without a visible label needs one of the two.
228
+ Both are ignored while `label` is set.
229
+
230
+ ```ts
231
+ import { Component, signal } from '@angular/core';
232
+ import { type BfRadioOption, BfRadioGroupComponent } from '@bitforgehq/ui';
233
+
234
+ /** A radio group named by the heading above it. */
235
+ @Component({
236
+ selector: 'app-example-radio-group-unlabelled',
237
+ imports: [BfRadioGroupComponent],
238
+ template: `
239
+ <div class="flex flex-col gap-2">
240
+ <h3 id="radio-plan-heading" class="text-heading">Plan</h3>
241
+ <bf-radio-group ariaLabelledby="radio-plan-heading" [options]="plans" [(value)]="plan" />
242
+ </div>
243
+ `,
244
+ })
245
+ export class RadioGroupUnlabelledExampleComponent {
246
+ protected readonly plans: BfRadioOption[] = [
247
+ { value: 'starter', label: 'Starter' },
248
+ { value: 'team', label: 'Team' },
249
+ ];
250
+ protected readonly plan = signal<string | null>('starter');
251
+ }
252
+ ```
253
+
254
+ ## Validation
255
+
256
+ 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.
257
+
258
+ ```ts
259
+ import { Component, signal } from '@angular/core';
260
+ import { FormField, FormRoot, form, required } from '@angular/forms/signals';
261
+ import { BfButtonComponent, BfRadioGroupComponent, type BfRadioOption } from '@bitforgehq/ui';
262
+
263
+ /**
264
+ * A required radio group with the default message.
265
+ *
266
+ * `required(p.view)` has no message, so a submit with nothing chosen shows "Fill in this field".
267
+ */
268
+ @Component({
269
+ selector: 'app-example-radio-group-validation',
270
+ imports: [FormField, FormRoot, BfButtonComponent, BfRadioGroupComponent],
271
+ template: `
272
+ <form [formRoot]="prefs" class="flex flex-col items-start gap-3">
273
+ <bf-radio-group label="View" [options]="options" [formField]="prefs.view" />
274
+ <bf-button type="submit">Save</bf-button>
275
+ </form>
276
+ `,
277
+ })
278
+ export class RadioGroupValidationExampleComponent {
279
+ protected readonly options: BfRadioOption[] = [
280
+ { value: 'day', label: 'Day' },
281
+ { value: 'week', label: 'Week' },
282
+ ];
283
+ private readonly model = signal<{ view: string | null }>({ view: null });
284
+ protected readonly prefs = form(
285
+ this.model,
286
+ (p) => {
287
+ required(p.view);
288
+ },
289
+ { submission: { action: () => Promise.resolve(undefined) } },
290
+ );
291
+ }
292
+ ```
293
+
294
+ ## Desktop
295
+
296
+ Hovering an unselected, enabled option darkens its ring (`text-muted`) with the pointer cursor; a read-only group has no hover. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The ring shows around the radio on keyboard focus only. Under a fine pointer the row is the hit area, at least 24 × 24 px; `sm` (32 px rows) suits dense desktop forms and tables; set it with `provideBfDefaults({ size: 'sm' })` ([density guide](https://bitforge-ui.web.app/density)).
297
+
298
+ ## Theming
299
+
300
+ - Ring: `border-strong` unchecked → `primary` checked; error, unchecked: `danger-border`. Dot: `primary-strong` (≥ 3:1 in its `surface` ring for any configured primary; falls back to `CanvasText` under `forced-colors`, e.g. Windows high-contrast).
301
+ - Hover over an unselected, enabled option (pointer cursor): ring `text-muted` (`danger` on error).
302
+ - Pressed (an option held down): its ring border turns `text`; not while disabled or read-only.
303
+ - Disabled: ring border `surface-sunken`, background `surface-muted`; option label `text-disabled`, description `text-muted`.
304
+ - Read-only: ring background `surface-muted`, no hover, default cursor.
305
+ - Legend: `text-muted`. Error text: `danger`. Focus ring: `ring`.
306
+
307
+ The legend, the options and the error text sit 6 px apart; each ring sits 10 px from its label.
308
+
309
+ ## Accessibility
310
+
311
+ A `<fieldset role="radiogroup">` with the `label` as its `<legend>`; each option is a native `<input type="radio">`, visually hidden and sharing one `name`, so keyboard behaviour (arrow keys move the selection) comes for free. The fieldset carries `aria-required`, and once touched and invalid `aria-invalid` with `aria-describedby` and `aria-errormessage` pointing at the error text (an explicit `errorMessage` shows immediately). Without a `label`, `aria-label` and `aria-labelledby` carry `ariaLabel` and `ariaLabelledby`. Read-only: `aria-readonly="true"` on the group; the radios stay focusable, and clicks (which Space also triggers) and arrow keys are cancelled, so the value never changes. `focus()` focuses the checked radio, else the first enabled one; Signal Forms calls it from `focusBoundControl()`. A disabled option's label dims to `text-disabled`. Each option row is only as wide as its content, so a click on empty space beside it selects nothing.
312
+
313
+ ### Keyboard
314
+
315
+ The options are native radio buttons, so these are the browser's radio-group keys; under `dir="rtl"` the browser swaps <kbd>ArrowLeft</kbd> and <kbd>ArrowRight</kbd> to follow the text direction.
316
+
317
+ | Key | Action |
318
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
319
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves focus into and out of the group: one tab stop, on the checked option when there is one |
320
+ | <kbd>ArrowDown</kbd> / <kbd>ArrowRight</kbd> | Checks and focuses the next enabled option, wrapping at the end; nothing while `readonly` |
321
+ | <kbd>ArrowUp</kbd> / <kbd>ArrowLeft</kbd> | Checks and focuses the previous enabled option, wrapping at the start; nothing while `readonly` |
322
+ | <kbd>Space</kbd> | Checks the focused option; nothing while `readonly` |
323
+
324
+ ## Inputs
325
+
326
+ | Name | Type | Default | Description |
327
+ | ---------------- | ----------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
328
+ | `options` | `readonly BfRadioOption<T>[]` | required | The options to choose from. |
329
+ | `compareWith` | `BfCompareWith<T>` | `Object.is` | Equality for matching `value` to an option (default `Object.is`); pass one for object values. |
330
+ | `label` | `string` | `''` | Group label, rendered as the fieldset legend. |
331
+ | `ariaLabel` | `string` | `''` | Accessible name of the group when no visible `label` is rendered. Ignored when `label` is set. |
332
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the group when no visible `label` is rendered. Ignored when `label` is set. |
333
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step of the option row: sm 32, md 40, lg 48 px (44 px under a coarse pointer for sm and md); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
334
+ | `orientation` | `BfRadioGroupOrientation` (`'vertical' \| 'horizontal'`) | `'vertical'` | Stack the options vertically (default) or in a row. |
335
+ | `disabled` | `boolean` | `false` | Disables every option. Set by `[formField]`; do not bind it yourself. |
336
+ | `readonly` | `boolean` | `false` | Shows the choice but blocks changes; the options stay focusable and the group exposes `aria-readonly`. Set by `[formField]`; do not bind it yourself. |
337
+ | `required` | `boolean` | `false` | Marks the group required. Set by `[formField]`; do not bind it yourself. |
338
+ | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
339
+ | `touched` | `boolean` | `false` | Whether the user has left the field. Set by `[formField]`; do not bind it yourself. |
340
+ | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms. Set by `[formField]`; do not bind it yourself. |
341
+ | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
342
+ | `name` | `string` | `generated` | Shared name of the radio inputs; generated when not set. Set by `[formField]`; do not bind it yourself. |
343
+ | `id` | `string` | `generated` | Group id; generated when not set. |
344
+ | `value` | `T \| null` | `null` | The selected option's value, `null` while nothing is selected; two-way bindable and bound by `[formField]`. Two-way: `[(value)]`. |
345
+
346
+ ## Outputs
347
+
348
+ | Name | Payload | Description |
349
+ | --------- | ------- | ------------------------------------------------------------------------------------------------------- |
350
+ | `changed` | `T` | Emitted with the newly selected value after a user change (never `null`). |
351
+ | `touch` | `void` | Emitted on blur so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
352
+
353
+ ## Methods
354
+
355
+ ### `focus(options?: FocusOptions): void`
356
+
357
+ Focuses the checked radio, else the first enabled one; Signal Forms calls it from
358
+ `focusBoundControl()`.
359
+
360
+ | Parameter | Type | Description |
361
+ | ---------- | -------------- | --------------------------------------------------------------------- |
362
+ | `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
363
+
364
+ ## Notes
365
+
366
+ ### Do
367
+
368
+ - Give a description when the label alone is ambiguous.
369
+ - Preselect a value when there is a sensible default; otherwise make the group required.
370
+
371
+ ### Don't
372
+
373
+ - Don't offer more than five or six options; use `bf-select`.
374
+
375
+ ## Migration notes
376
+
377
+ ### 0.3.0
378
+
379
+ New exported `BfRadioGroupOrientation` (`'vertical' | 'horizontal'`), the type of `orientation`
380
+ (was an inline union with the same values).
381
+
382
+ **Breaking:** typed values. `BfRadioGroupComponent<T = string>` is generic over the option value
383
+ (`BfRadioOption<T = string>`); `value` is `T | null` and defaults to `null` (was `''`), with
384
+ nothing checked; `changed` emits `T` (was `string`). New `compareWith` (default `Object.is`)
385
+ matches `value` to an option. A radio's native `value` is its option value for a string, number
386
+ or boolean and its index for an object. See Migration 6, 7 and 9.
387
+
388
+ **Behaviour:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): rows
389
+ are `sm` 32, `md` 40 (default), `lg` 48 px under a fine pointer, and at least 44 px under a coarse
390
+ pointer (was 44 px everywhere); the ring is 18 / 22 / 24 px with a matching dot.
391
+
392
+ **Behaviour:** an error without a message shows its kind's default text (was a red border only):
393
+ once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
394
+ `validation` text for its kind (Signal Forms and Reactive interop kinds), else
395
+ `validation.fallback`. `errorMessage` still wins. See Migration 1.
396
+
397
+ **Behaviour:** the dot is `primary-strong` (was `primary`), so it reaches 3:1 in its `surface`
398
+ ring for any configured primary.
399
+
400
+ JSDoc of `radio-group.component.ts` for the API reference: `@summary`/`@since` on the exported
401
+ symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
402
+ leaves them out. No behaviour change.
403
+
404
+ **Behaviour:** a pressed state (F-045): an option's ring border turns `text` while the option is held down. None while disabled or read-only; under forced colours the system colours win.
405
+
406
+ **Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
407
+ `aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
408
+ elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
409
+ (F-020).
410
+
411
+ ### 0.2.0
412
+
413
+ The legend, the options and the error text sit 6 px apart (legend `mb-1.5`, fieldset `gap-1.5`;
414
+ the legend was 4 px above the options and the error 8 px below them).
415
+
416
+ Fixed: hovering an enabled, unselected option darkens its ring (`text-muted`, `danger` on
417
+ error).
418
+
419
+ Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
420
+ could be edited; it now declares `readonly` and blocks changes. While read-only clicks (and Space)
421
+ and arrow keys are cancelled, the fieldset carries `aria-readonly="true"`, the radios stay
422
+ focusable, and the rings rest on `surface-muted` without hover. `ariaLabel` / `ariaLabelledby` name
423
+ a group without a visible `label`. `focus(options?)` focuses the checked radio, else the first
424
+ enabled one, so Signal Forms' `focusBoundControl()` works.
425
+
426
+ Fixed: a disabled option's label dims to `text-disabled` (`has-[input:disabled]:text-text-disabled`
427
+ on the row; `peer-disabled:` could not reach the text through its wrapper span). Vertical option
428
+ rows are `items-start`, so a click on empty space beside an option no longer selects it.
429
+
430
+ ### 0.1.0
431
+
432
+ The fieldset now carries `role="radiogroup"`. The ring's focus ring clears in forced-colors mode and the selected dot renders in `CanvasText` under forced colors. The host no longer reflects a static `id` attribute onto `<bf-radio-group>`; the id lands on the `<fieldset>`. Each option's label now carries `min-h-11 items-center`, so a stacked row is itself at least 44 px tall and is its own hit area, rather than a `::after` pseudo-element that overlapped into the neighbouring row's 10px gap and stole its clicks near the boundary; the vertical option list's gap tightened from `gap-2.5` to `gap-1` to match the now taller rows.