@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,418 @@
1
+ # bf-checkbox
2
+
3
+ Rounded checkbox with an optional label that binds to Signal Forms
4
+
5
+ `BfCheckboxComponent` from `@bitforgehq/ui` · Forms · since 0.1.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-checkbox overview.
15
+ *
16
+ * Live demo of `bf-checkbox`: unchecked, checked and indeterminate boxes, all bound to signals; then the sm, md and lg sizes in a row.
17
+ */
18
+ @Component({
19
+ selector: 'app-example-checkbox-overview',
20
+ imports: [BfCheckboxComponent],
21
+ template: `
22
+ <div class="flex flex-col gap-3">
23
+ <bf-checkbox [(checked)]="subscribe">Subscribe to updates</bf-checkbox>
24
+ <bf-checkbox [(checked)]="agree">I agree to the terms</bf-checkbox>
25
+ <bf-checkbox [(checked)]="selectAll" [(indeterminate)]="mixed">
26
+ Select all (starts indeterminate)
27
+ </bf-checkbox>
28
+ <bf-checkbox [disabled]="true">Disabled</bf-checkbox>
29
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
30
+ <div data-sizes class="flex flex-wrap items-center gap-3">
31
+ <bf-checkbox size="sm" [checked]="true">sm · 32 px</bf-checkbox>
32
+ <bf-checkbox [checked]="true">md · 40 px</bf-checkbox>
33
+ <bf-checkbox size="lg" [checked]="true">lg · 48 px</bf-checkbox>
34
+ </div>
35
+ </div>
36
+ `,
37
+ })
38
+ export class CheckboxOverviewExampleComponent {
39
+ readonly subscribe = signal(false);
40
+ readonly agree = signal(true);
41
+ readonly selectAll = signal(false);
42
+ readonly mixed = signal(true);
43
+
44
+ readonly status = computed(() => {
45
+ const checked = [
46
+ this.subscribe() ? 'Subscribe' : '',
47
+ this.agree() ? 'Agree' : '',
48
+ this.mixed() ? 'Select all (mixed)' : this.selectAll() ? 'Select all' : '',
49
+ ].filter(Boolean);
50
+ return checked.length > 0 ? `Checked: ${checked.join(', ')}.` : 'Nothing checked.';
51
+ });
52
+ }
53
+ ```
54
+
55
+ ## Overview
56
+
57
+ Checkbox with a 22 px rounded box and an optional label, implementing the Signal Forms
58
+ checkbox contract so `[formField]` binds `checked`.
59
+
60
+ A checkbox for yes/no choices and multi-select lists. The native input stays in the DOM (visually hidden) so keyboard, screen readers and forms work as expected; the visible 22 px box is drawn with tokens.
61
+
62
+ ## When to use
63
+
64
+ Use `bf-checkbox` for a yes/no choice that takes effect when the form is submitted, and for picking several items of a list. Use [bf-toggle](https://bitforge-ui.web.app/components/toggle) instead for a setting that applies at once, and [bf-radio-group](https://bitforge-ui.web.app/components/radio-group) when exactly one option may be chosen.
65
+
66
+ ## Basic usage
67
+
68
+ Put the label in the default slot or in `label`.
69
+
70
+ ```ts
71
+ import { Component, signal } from '@angular/core';
72
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
73
+
74
+ /** A checkbox with its label in the default slot. */
75
+ @Component({
76
+ selector: 'app-example-checkbox-basic',
77
+ imports: [BfCheckboxComponent],
78
+ template: `
79
+ <div class="flex flex-col gap-2">
80
+ <bf-checkbox [(checked)]="agree">I agree to the terms</bf-checkbox>
81
+ <p class="text-caption text-text-muted">{{ agree() ? 'Agreed.' : 'Not agreed yet.' }}</p>
82
+ </div>
83
+ `,
84
+ })
85
+ export class CheckboxBasicExampleComponent {
86
+ protected readonly agree = signal(false);
87
+ }
88
+ ```
89
+
90
+ ## With Signal Forms
91
+
92
+ ```ts
93
+ import { Component, signal } from '@angular/core';
94
+ import { FormField, form } from '@angular/forms/signals';
95
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
96
+
97
+ /** A checkbox bound to a Signal Forms field. */
98
+ @Component({
99
+ selector: 'app-example-checkbox-signal-forms',
100
+ imports: [FormField, BfCheckboxComponent],
101
+ template: `
102
+ <div class="flex flex-col gap-2">
103
+ <bf-checkbox label="Send me the weekly summary" [formField]="prefs.weekly" />
104
+ <p class="text-caption text-text-muted">weekly: {{ prefs.weekly().value() }}</p>
105
+ </div>
106
+ `,
107
+ })
108
+ export class CheckboxSignalFormsExampleComponent {
109
+ private readonly model = signal({ weekly: true });
110
+ protected readonly prefs = form(this.model);
111
+ }
112
+ ```
113
+
114
+ ## Sizes
115
+
116
+ `sm` (32 px row, 18 px box, 14 px label), `md` (default, 40 px row, 22 px box), `lg` (48 px row, 24 px box); the check mark scales with the box. 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, at least 24×24 px (WCAG 2.5.8) even for a label-less box with only `ariaLabel`; under a coarse pointer (touch) `sm` and `md` rows grow to 44 px.
117
+
118
+ ```ts
119
+ import { Component, signal } from '@angular/core';
120
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
121
+
122
+ /** The sm, md and lg checkbox sizes. */
123
+ @Component({
124
+ selector: 'app-example-checkbox-sizes',
125
+ imports: [BfCheckboxComponent],
126
+ template: `
127
+ <div class="flex flex-col gap-2">
128
+ <bf-checkbox size="sm" [(checked)]="compact">Compact rows</bf-checkbox>
129
+ <bf-checkbox [(checked)]="agree">I agree to the terms</bf-checkbox>
130
+ <bf-checkbox size="lg" [(checked)]="notify">Notify me</bf-checkbox>
131
+ </div>
132
+ `,
133
+ })
134
+ export class CheckboxSizesExampleComponent {
135
+ protected readonly compact = signal(true);
136
+ protected readonly agree = signal(false);
137
+ protected readonly notify = signal(true);
138
+ }
139
+ ```
140
+
141
+ ## Indeterminate
142
+
143
+ Use for a "select all" control while only some items are selected. `indeterminate` is a model: a click clears it (as the native checkbox does) and writes `false` back through `[(indeterminate)]`, so the dash, the check mark and `aria-checked` always agree.
144
+
145
+ ```ts
146
+ import { Component, computed, signal } from '@angular/core';
147
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
148
+
149
+ /**
150
+ * A select-all checkbox over three tasks.
151
+ *
152
+ * It shows the dash while only some tasks are checked; a click selects or clears them all.
153
+ */
154
+ @Component({
155
+ selector: 'app-example-checkbox-indeterminate',
156
+ imports: [BfCheckboxComponent],
157
+ template: `
158
+ <div class="flex flex-col gap-2">
159
+ <bf-checkbox
160
+ label="All tasks"
161
+ [checked]="allSelected()"
162
+ [(indeterminate)]="someSelected"
163
+ (changed)="toggleAll($event)"
164
+ />
165
+ <div class="flex flex-col gap-2 ps-8">
166
+ @for (task of tasks(); track task.name; let i = $index) {
167
+ <bf-checkbox [label]="task.name" [checked]="task.done" (changed)="toggle(i, $event)" />
168
+ }
169
+ </div>
170
+ </div>
171
+ `,
172
+ })
173
+ export class CheckboxIndeterminateExampleComponent {
174
+ protected readonly tasks = signal([
175
+ { name: 'Brief', done: true },
176
+ { name: 'Moodboard', done: false },
177
+ { name: 'Deck', done: false },
178
+ ]);
179
+ protected readonly allSelected = computed(() => this.tasks().every((t) => t.done));
180
+ protected readonly someSelected = signal(true);
181
+
182
+ protected toggleAll(checked: boolean): void {
183
+ this.tasks.update((tasks) => tasks.map((t) => ({ ...t, done: checked })));
184
+ this.someSelected.set(false);
185
+ }
186
+
187
+ protected toggle(index: number, checked: boolean): void {
188
+ this.tasks.update((tasks) => tasks.map((t, i) => (i === index ? { ...t, done: checked } : t)));
189
+ const done = this.tasks().filter((t) => t.done).length;
190
+ this.someSelected.set(done > 0 && done < this.tasks().length);
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## In a selectable row
196
+
197
+ Skip the visible `label` when the row itself already shows the name, and give the box an `ariaLabel` instead so assistive tech still gets a name, or `ariaLabelledby` with the id(s) of the element(s) that already show it (a table's column header and row title).
198
+
199
+ ```ts
200
+ import { Component, signal } from '@angular/core';
201
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
202
+
203
+ /**
204
+ * A checkbox in a selectable row.
205
+ *
206
+ * The row shows the name, so the box takes an `ariaLabel` instead of a visible label.
207
+ */
208
+ @Component({
209
+ selector: 'app-example-checkbox-row',
210
+ imports: [BfCheckboxComponent],
211
+ template: `
212
+ <div class="bg-surface rounded-card flex items-center gap-3 p-3">
213
+ <bf-checkbox ariaLabel="Select Atlas rebrand" [(checked)]="selected" />
214
+ <span class="text-body">Atlas rebrand</span>
215
+ </div>
216
+ `,
217
+ })
218
+ export class CheckboxRowExampleComponent {
219
+ protected readonly selected = signal(false);
220
+ }
221
+ ```
222
+
223
+ ## Read-only
224
+
225
+ `readonly` shows the state but blocks changes: clicks and Space do nothing, the box stays in the
226
+ tab order, and the box rests on the muted surface without the hover tint, like a read-only radio.
227
+ A checked box keeps a `primary` border and mark instead of the filled box. Signal Forms binds it
228
+ from `readonly()` rules.
229
+
230
+ ```ts
231
+ import { Component } from '@angular/core';
232
+ import { BfCheckboxComponent } from '@bitforgehq/ui';
233
+
234
+ /** A read-only checkbox: it shows its state and blocks changes. */
235
+ @Component({
236
+ selector: 'app-example-checkbox-readonly',
237
+ imports: [BfCheckboxComponent],
238
+ template: `<bf-checkbox label="Terms accepted" [readonly]="true" [checked]="accepted" />`,
239
+ })
240
+ export class CheckboxReadonlyExampleComponent {
241
+ protected readonly accepted = true;
242
+ }
243
+ ```
244
+
245
+ ## Validation
246
+
247
+ 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.
248
+
249
+ ```ts
250
+ import { Component, signal } from '@angular/core';
251
+ import { FormField, FormRoot, form, required } from '@angular/forms/signals';
252
+ import { BfButtonComponent, BfCheckboxComponent } from '@bitforgehq/ui';
253
+
254
+ /**
255
+ * A required checkbox with the default message.
256
+ *
257
+ * `required(p.terms)` has no message, so a submit shows "Fill in this field".
258
+ */
259
+ @Component({
260
+ selector: 'app-example-checkbox-validation',
261
+ imports: [FormField, FormRoot, BfButtonComponent, BfCheckboxComponent],
262
+ template: `
263
+ <form [formRoot]="signup" class="flex flex-col items-start gap-3">
264
+ <bf-checkbox label="I accept the terms" [formField]="signup.terms" />
265
+ <bf-button type="submit">Sign up</bf-button>
266
+ </form>
267
+ `,
268
+ })
269
+ export class CheckboxValidationExampleComponent {
270
+ private readonly model = signal({ terms: false });
271
+ protected readonly signup = form(
272
+ this.model,
273
+ (p) => {
274
+ required(p.terms);
275
+ },
276
+ { submission: { action: () => Promise.resolve(undefined) } },
277
+ );
278
+ }
279
+ ```
280
+
281
+ ## Desktop
282
+
283
+ Hovering the row (box and label) darkens the box border, or the checked fill, with the pointer cursor; a read-only box has no hover and the default cursor. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The ring shows around the box 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)).
284
+
285
+ ## Theming
286
+
287
+ - Unchecked box: `border-strong` border, `surface` background; checked (or indeterminate): `primary` background with an `on-primary` mark.
288
+ - Error, unchecked: border switches to `danger-border`. Error text: `danger`.
289
+ - Hover over the row (pointer cursor): unchecked border `text-muted` (`danger` on error); checked box `primary-hover`.
290
+ - Pressed (the row held down): the box border turns `text`; not while disabled or read-only.
291
+ - Disabled: border `surface-sunken`, background `surface-muted`; label text `text-disabled`.
292
+ - Read-only: `surface-muted` background, no hover, default cursor; unchecked keeps the `border-strong` border, checked (or indeterminate) a `primary` border and mark (≥ 4.8:1 on `surface-muted` in both themes).
293
+ - Focus ring (on the visually hidden native input): `ring`.
294
+
295
+ The box sits 10 px from the label and the error text 6 px under the row, indented to the label (logical padding, so it follows `dir="rtl"`).
296
+
297
+ ## Accessibility
298
+
299
+ Native `<input type="checkbox">`, visually hidden (`sr-only`) and wrapped in a `<label>` so the whole row is tappable. Keyboard: native checkbox behaviour — Space toggles it while focused. `indeterminate` sets the native DOM property, which assistive tech reads as "mixed" without any extra ARIA; a user change clears it, so the visual and the reported state agree. The label row is only as wide as its content, so a click on empty space beside it does nothing. `aria-label` carries `ariaLabel` and `aria-labelledby` carries `ariaLabelledby` when there is no visible `label`; `aria-invalid`, `aria-describedby` and `aria-errormessage` point at the error text once the field is touched and invalid (an explicit `errorMessage` shows immediately). Read-only: `aria-readonly="true"`, still focusable; the click (which Space also triggers) is cancelled, so the state never changes. `focus()` focuses the native checkbox; Signal Forms calls it from `focusBoundControl()`.
300
+
301
+ ### Keyboard
302
+
303
+ | Key | Action |
304
+ | ------------------------------------------------ | ----------------------------------------------------------------------------- |
305
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves focus to and from the checkbox; a `readonly` one stays in the tab order |
306
+ | <kbd>Space</kbd> | Toggles it and clears `indeterminate`; nothing while `readonly` |
307
+
308
+ ## Inputs
309
+
310
+ | Name | Type | Default | Description |
311
+ | ---------------- | ----------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
312
+ | `label` | `string` | `''` | Label text; the default slot works too. |
313
+ | `ariaLabel` | `string` | `''` | Accessible name when there is no visible `label`, e.g. a row-selection box. |
314
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the control when no visible label is rendered (e.g. a table column header). |
315
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step of the 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). |
316
+ | `disabled` | `boolean` | `false` | Disables the control. Set by `[formField]`; do not bind it yourself. |
317
+ | `readonly` | `boolean` | `false` | Shows the state but blocks changes; the box stays focusable and exposes `aria-readonly`. Set by `[formField]`; do not bind it yourself. |
318
+ | `required` | `boolean` | `false` | Marks the control required. Set by `[formField]`; do not bind it yourself. |
319
+ | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
320
+ | `touched` | `boolean` | `false` | Whether the user has left the field. Set by `[formField]`; do not bind it yourself. |
321
+ | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms. Set by `[formField]`; do not bind it yourself. |
322
+ | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
323
+ | `name` | `string` | `''` | Native name attribute. Set by `[formField]`; do not bind it yourself. |
324
+ | `id` | `string` | `generated` | Control id; generated when not set. |
325
+ | `checked` | `boolean` | `false` | Whether the box is checked; two-way bindable and bound by `[formField]`. Two-way: `[(checked)]`. |
326
+ | `indeterminate` | `boolean` | `false` | Shows the mixed state (e.g. "select all" with some rows selected); two-way bindable. A user change clears it, like the native checkbox, so the box and `aria-checked` agree. Two-way: `[(indeterminate)]`. |
327
+
328
+ ## Outputs
329
+
330
+ | Name | Payload | Description |
331
+ | --------- | --------- | ------------------------------------------------------------------------------------------------------- |
332
+ | `changed` | `boolean` | Emitted with the new checked state after a user change. |
333
+ | `touch` | `void` | Emitted on blur so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
334
+
335
+ ## Slots
336
+
337
+ - `default`: Rich label content next to the box, before the `label` text.
338
+
339
+ ## Methods
340
+
341
+ ### `focus(options?: FocusOptions): void`
342
+
343
+ Focuses the native checkbox; Signal Forms calls it from `focusBoundControl()`.
344
+
345
+ | Parameter | Type | Description |
346
+ | ---------- | -------------- | --------------------------------------------------------------------- |
347
+ | `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
348
+
349
+ ## Notes
350
+
351
+ ### Do
352
+
353
+ - Use a checkbox when the choice takes effect on submit; use `bf-toggle` when it applies immediately.
354
+ - Keep labels positive ("Send notifications", not "Don't send").
355
+
356
+ ### Don't
357
+
358
+ - Don't use a lone checkbox for a required agreement without an error message.
359
+
360
+ ## Migration notes
361
+
362
+ ### 0.3.0
363
+
364
+ New `ariaLabelledby` input: the id(s) of the element(s) that name the box when no visible `label`
365
+ is shown (a table's column header and row title), set as the native checkbox's `aria-labelledby`.
366
+ Additive.
367
+
368
+ **Behaviour:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): rows
369
+ are `sm` 32, `md` 40 (default), `lg` 48 px under a fine pointer, and at least 44 px under a coarse
370
+ pointer (was 44 px everywhere); the box is 18 / 22 / 24 px with a matching check mark, and the error
371
+ text's indent is logical (`ps-*`). The row is at least 24 px wide (`min-w-6`), so a label-less box
372
+ with only `ariaLabel` still meets the 24×24 px target size of WCAG 2.5.8 under a fine pointer.
373
+
374
+ **Behaviour:** an error without a message shows its kind's default text (was a red border only):
375
+ once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
376
+ `validation` text for its kind (Signal Forms and Reactive interop kinds), else
377
+ `validation.fallback`. `errorMessage` still wins. See Migration 1.
378
+
379
+ JSDoc of `checkbox.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs;
380
+ template handlers and framework hooks marked `@internal`, so the reference leaves them out. No
381
+ behaviour change.
382
+
383
+ **Behaviour:** a pressed state (F-045): the box border turns `text` while the row is held down. None while disabled or read-only; under forced colours the system colours win.
384
+
385
+ **Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
386
+ `aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
387
+ elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
388
+ (F-020).
389
+
390
+ ### 0.2.0
391
+
392
+ The error text sits 6 px under the row (`gap-1.5`, was 4 px), the label-to-message spacing of the
393
+ other form widgets.
394
+
395
+ Fixed: hovering an enabled row tints the box (unchecked border `text-muted`, `danger` on error;
396
+ checked `primary-hover`); the label was already `cursor-pointer`.
397
+
398
+ Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
399
+ could be edited; it now declares `readonly` and blocks changes. While read-only the click (which
400
+ Space also triggers) is cancelled, `aria-readonly="true"` is set, the box stays focusable, and the
401
+ box rests on `surface-muted` without hover (default cursor), like a read-only radio; a checked box
402
+ keeps a `primary` border and mark instead of the fill. `focus(options?)` focuses the
403
+ native control, so Signal Forms' `focusBoundControl()` and "focus the first invalid field" on submit
404
+ now work (they were no-ops).
405
+
406
+ Behaviour change (breaking for a bare attribute): `indeterminate` is now a `model(false)` (it was an
407
+ `input` with `booleanAttribute`). A user change clears it and writes `false` back, so after a click
408
+ the box shows the check mark that assistive tech reports instead of a stale dash; the "select all"
409
+ pattern needs no `(changed)` handler to reset it. Migration: `[indeterminate]="expr"` still works
410
+ one-way and `[(indeterminate)]` binds both ways; a bare `indeterminate` attribute no longer compiles,
411
+ so write `[indeterminate]="true"`.
412
+
413
+ Fixed: the column is `items-start`, so the label row is only as wide as its content and a click on
414
+ empty space beside it no longer toggles the box.
415
+
416
+ ### 0.1.0
417
+
418
+ The label's cursor styling is now a computed `labelClasses`. The redundant `[attr.aria-checked]` binding was removed from the native checkbox; the native `checked`/`indeterminate` state already conveys it to assistive tech. The focus ring now clears in forced-colors mode. The host no longer reflects a static `id` attribute onto `<bf-checkbox>`. Fixed the checked box never showing its fill: `bg-surface`/`after:opacity-0` and the "on" branch's `bg-primary`/`after:opacity-100` were both present at once, and Tailwind's stylesheet order let `bg-surface` win, leaving a checked box white with an invisible check mark. The base class list and the on/off branches now each emit exactly one background and one after-opacity utility. The label's clickable hit area is now `min-h-11 items-center` instead of a `::after` pseudo-element; a `::after` extension over projected content (the label text) is the wrong tool — `min-h-11` gets the same ≥ 44 px target without an overlay. Dropped the box's `mt-0.5` top-margin nudge; with the row already `min-h-11 items-center`, the box centres on the label line on its own. Added `ariaLabel` (mirrors `bf-toggle`) for an accessible name when there's no visible `label`, e.g. a row-selection box; the label `<span>` no longer renders when `label` is empty, so an `ariaLabel`-only checkbox has no empty gap next to the box. Projected label content and the `label` input share one styled text span, which hides itself when both are empty (`empty:hidden`).
@@ -0,0 +1,205 @@
1
+ # bf-chip-group
2
+
3
+ Named row of chips that picks one like radios, or groups independent toggles
4
+
5
+ `BfChipGroupComponent` from `@bitforgehq/ui` · Forms · since 0.3.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, signal } from '@angular/core';
11
+ import { BfChipComponent, BfChipGroupComponent } from '@bitforgehq/ui';
12
+
13
+ type Status = 'all' | 'open' | 'done';
14
+
15
+ /**
16
+ * The bf-chip-group overview.
17
+ *
18
+ * Live demo of `bf-chip-group`: a single-choice status row that reads as radio buttons, with an
19
+ * arrow-key tab stop, and a multiple-choice tag row of independent toggles; the choices are echoed
20
+ * underneath.
21
+ */
22
+ @Component({
23
+ selector: 'app-example-chip-group-overview',
24
+ imports: [BfChipComponent, BfChipGroupComponent],
25
+ template: `
26
+ <div class="flex flex-col gap-3">
27
+ <bf-chip-group label="Status" [(value)]="status">
28
+ <bf-chip variant="ink" value="all">All</bf-chip>
29
+ <bf-chip variant="ink" value="open">Open</bf-chip>
30
+ <bf-chip variant="ink" value="done">Done</bf-chip>
31
+ </bf-chip-group>
32
+ <bf-chip-group mode="multiple" label="Tags">
33
+ <bf-chip [(selected)]="design">Design</bf-chip>
34
+ <bf-chip [(selected)]="copy">Copy</bf-chip>
35
+ <bf-chip disabled>Archived</bf-chip>
36
+ </bf-chip-group>
37
+ <p data-status aria-live="polite" class="text-caption text-text-muted">
38
+ Status: {{ status() }} · Tags: {{ tags() }}
39
+ </p>
40
+ </div>
41
+ `,
42
+ })
43
+ export class ChipGroupOverviewExampleComponent {
44
+ protected readonly status = signal<Status | null>('all');
45
+ protected readonly design = signal(true);
46
+ protected readonly copy = signal(false);
47
+
48
+ protected tags(): string {
49
+ const tags = [this.design() ? 'Design' : '', this.copy() ? 'Copy' : ''].filter(Boolean);
50
+ return tags.length > 0 ? tags.join(', ') : 'none';
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Overview
56
+
57
+ A named row of `bf-chip`s. In `single` mode (the default) the chips are a radio group: the
58
+ group owns the selected chip's `value`, one chip is the tab stop and the arrow keys move the
59
+ selection. In `multiple` mode the chips stay independent toggles and the group only names them.
60
+ Not a Signal Forms control: for a form field use `bf-segmented` or `bf-radio-group`.
61
+
62
+ A chip group names a row of `bf-chip`s and, in `single` mode, turns them into a radio group: one chip is selected, the group holds its `value`, and assistive tech announces each chip as "radio, 1 of 3, selected". In `multiple` mode the chips stay independent toggles and the group only names the row.
63
+
64
+ ## When to use
65
+
66
+ Use `bf-chip-group` for a row of filter chips that belong together: one status out of "All", "Open", "Done" (`single`), or any number of tags (`multiple`). Use [bf-segmented](https://bitforge-ui.web.app/components/segmented) or [bf-radio-group](https://bitforge-ui.web.app/components/radio-group) instead for a choice in a form that is submitted: the chip group is not a Signal Forms control. Use [bf-chip](https://bitforge-ui.web.app/components/chip) on its own for a single toggle.
67
+
68
+ ## Single choice
69
+
70
+ Give every chip a `value` and bind the group's `[(value)]`. The chip whose `value` matches (compared with `Object.is`) is selected; a click, <kbd>Space</kbd> or <kbd>Enter</kbd> selects another one and `(changed)` emits its value. There is no deselect: clicking the selected chip keeps it, as with radio buttons. `value` is `null` while nothing is selected. Inside a `single` group the group's `value` decides which chip is selected, so the chips' own `selected` is ignored. A programmatic write to `value` does not emit.
71
+
72
+ ```ts
73
+ import { Component, signal } from '@angular/core';
74
+ import { BfChipComponent, BfChipGroupComponent } from '@bitforgehq/ui';
75
+
76
+ type View = 'all' | 'open' | 'done';
77
+
78
+ /**
79
+ * A single-choice filter row.
80
+ *
81
+ * The group holds the selected chip's value and makes the row one radio group: Tab reaches the
82
+ * selected chip, the arrow keys move the selection.
83
+ */
84
+ @Component({
85
+ selector: 'app-example-chip-group-single',
86
+ imports: [BfChipComponent, BfChipGroupComponent],
87
+ template: `
88
+ <bf-chip-group label="View" [(value)]="view">
89
+ <bf-chip value="all">All</bf-chip>
90
+ <bf-chip value="open">Open</bf-chip>
91
+ <bf-chip value="done">Done</bf-chip>
92
+ </bf-chip-group>
93
+ `,
94
+ })
95
+ export class ChipGroupSingleExampleComponent {
96
+ protected readonly view = signal<View | null>('open');
97
+ }
98
+ ```
99
+
100
+ ## Multiple choice
101
+
102
+ With `mode="multiple"` each chip keeps its own `[(selected)]` and `(changed)` and announces as a toggle button (`aria-pressed`); the group adds the `group` role and its name. The group's `value` is unused.
103
+
104
+ ```ts
105
+ import { Component, signal } from '@angular/core';
106
+ import { BfChipComponent, BfChipGroupComponent } from '@bitforgehq/ui';
107
+
108
+ /**
109
+ * A multiple-choice tag row.
110
+ *
111
+ * `mode="multiple"` names the row and leaves each chip an independent toggle with its own
112
+ * `[(selected)]`.
113
+ */
114
+ @Component({
115
+ selector: 'app-example-chip-group-multiple',
116
+ imports: [BfChipComponent, BfChipGroupComponent],
117
+ template: `
118
+ <bf-chip-group mode="multiple" label="Tags">
119
+ <bf-chip [(selected)]="design">Design</bf-chip>
120
+ <bf-chip [(selected)]="development">Development</bf-chip>
121
+ <bf-chip [(selected)]="research">Research</bf-chip>
122
+ </bf-chip-group>
123
+ `,
124
+ })
125
+ export class ChipGroupMultipleExampleComponent {
126
+ protected readonly design = signal(true);
127
+ protected readonly development = signal(false);
128
+ protected readonly research = signal(true);
129
+ }
130
+ ```
131
+
132
+ ## Desktop
133
+
134
+ The chips keep their own hover (a darker border unselected, a deeper tint selected), pointer cursor and pressed fill. In `single` mode the whole row is one tab stop and the arrow keys move through it, so a keyboard user reaches the next control with one <kbd>Tab</kbd>. The row wraps with 8 px between chips and rows. Density follows the chips' `size` (`sm` 32 px for a toolbar or table filter, [density guide](https://bitforge-ui.web.app/density)).
135
+
136
+ ## Theming
137
+
138
+ The group draws nothing itself: every colour comes from its chips (see [bf-chip](https://bitforge-ui.web.app/components/chip)), selected chips `primary-soft` / `primary-strong` (`tonal`) or `text` / `bg` (`ink`). The host is a wrapping flex row with an 8 px gap (`gap-2`); add classes to the element to change the layout.
139
+
140
+ ## Accessibility
141
+
142
+ `single`: the host is `role="radiogroup"` named by `label` (`aria-label`) or `ariaLabelledby`; each chip's button is `role="radio"` with `aria-checked` and no `aria-pressed`. Only the selected chip (else the first enabled one) sits in the tab order, the others carry `tabindex="-1"`, and selection follows focus, as in `bf-segmented`. Disabled chips, and every chip of a `disabled` group, get the native `disabled` attribute and are skipped. `multiple`: the host is `role="group"` with the same name, and the chips stay `aria-pressed` toggle buttons in the normal tab order. Give the group a `label` or `ariaLabelledby`: it is the only name of the row.
143
+
144
+ ### Keyboard
145
+
146
+ | Key | Where | Action |
147
+ | ------------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
148
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | `single` | Moves focus into and out of the group: one tab stop, on the selected chip (else the first enabled one) |
149
+ | <kbd>ArrowRight</kbd> / <kbd>ArrowDown</kbd> | `single` | Selects and focuses the next enabled chip in DOM order, wrapping at the end; not mirrored under `dir="rtl"` yet (a known gap) |
150
+ | <kbd>ArrowLeft</kbd> / <kbd>ArrowUp</kbd> | `single` | Selects and focuses the previous enabled chip in DOM order, wrapping at the start; not mirrored under `dir="rtl"` yet |
151
+ | <kbd>Home</kbd> / <kbd>End</kbd> | `single` | Selects and focuses the first / last enabled chip |
152
+ | <kbd>Space</kbd> / <kbd>Enter</kbd> | `single` | Selects the focused chip |
153
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | `multiple` | Moves focus from chip to chip |
154
+ | <kbd>Space</kbd> / <kbd>Enter</kbd> | `multiple` | Toggles the focused chip |
155
+
156
+ ## Inputs
157
+
158
+ | Name | Type | Default | Description |
159
+ | ---------------- | -------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `mode` | `BfChipGroupMode` (`'single' \| 'multiple'`) | `'single'` | `single` (default): one chip selected, radio semantics; `multiple`: independent toggles. |
161
+ | `label` | `string` | `''` | Accessible name of the group (required unless `ariaLabelledby` is set). |
162
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the group (e.g. a visible heading) when `label` is not set. |
163
+ | `disabled` | `boolean` | `false` | Disables every chip of the group. |
164
+ | `value` | `T \| null` | `null` | single: the selected chip's value (`null` = none); ignored in multiple mode. Two-way. Every chip of a single-mode group needs a `value` (of type `T`); one without it cannot be selected. Two-way: `[(value)]`. |
165
+
166
+ ## Outputs
167
+
168
+ | Name | Payload | Description |
169
+ | --------- | ----------- | ------------------------------------------------------------- |
170
+ | `changed` | `T \| null` | Emitted with the new value after a user change (single mode). |
171
+
172
+ ## Slots
173
+
174
+ - `default`: The `bf-chip`s of the group, each with its `value`.
175
+
176
+ ## Notes
177
+
178
+ ### Do
179
+
180
+ - Give every chip of a `single` group a distinct `value` of the group's value type. A chip without a `value` cannot be selected: it never matches, and a click on it changes nothing.
181
+ - Name the group with `label`, or `ariaLabelledby` pointing at a visible heading.
182
+
183
+ ### Don't
184
+
185
+ - Don't bind `[formField]` to a chip group; use `bf-segmented` or `bf-radio-group` in a form.
186
+ - Don't nest other interactive controls between the chips of a `single` group; the arrow keys move between chips only.
187
+
188
+ ## Migration notes
189
+
190
+ ### 0.3.0
191
+
192
+ New `bf-chip-group` (`BfChipGroupComponent<T>`, `BfChipGroupMode` `'single' | 'multiple'`, root
193
+ entry, F-038): a named row of `bf-chip`s. `single` (default): the host is `role="radiogroup"`
194
+ named by `label` or `ariaLabelledby`, the chips are `role="radio"` with `aria-checked`, one roving
195
+ tab stop (the selected chip, else the first enabled), and the arrow keys (wrapping, skipping
196
+ disabled chips), `Home` and `End` move focus and the selection, as in `bf-segmented`; `Space` and
197
+ `Enter` select, there is no deselect. `[(value)]` holds the selected chip's `value` (`null` for
198
+ none) and `(changed)` emits it after a user change. `multiple`: the host is `role="group"` and the
199
+ chips stay independent `aria-pressed` toggles with their own `[(selected)]`. `disabled` disables
200
+ every chip. It is not a Signal Forms control; for a form field use `bf-segmented` or
201
+ `bf-radio-group`.
202
+
203
+ A chip without a `value` inside a `single` group never matches the group's value and cannot be
204
+ selected, so chips that all leave it unset never read as selected together; give every chip a
205
+ `value`.