@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,845 @@
1
+ # bf-sheet
2
+
3
+ Bottom sheet with snap points, drag to dismiss and a backdrop
4
+
5
+ `BfSheetComponent` from `@bitforgehq/ui/mobile` · Overlays · since 0.2.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, inject, signal } from '@angular/core';
11
+ import { BfButtonComponent } from '@bitforgehq/ui';
12
+ import {
13
+ BfContentComponent,
14
+ BfHeaderComponent,
15
+ BfPageComponent,
16
+ BfSheetService,
17
+ } from '@bitforgehq/ui/mobile';
18
+ import { ShowcaseSheetComponent } from './showcase-sheet.component';
19
+
20
+ /** Snap points of the showcase sheet: half the screen, then almost all of it. */
21
+ export const SHOWCASE_SHEET_SNAP_POINTS = [0.5, 0.92];
22
+
23
+ /** `bf-sheet` showcase: a button opens a form in a bottom sheet with two snap points. */
24
+ @Component({
25
+ selector: 'app-sheet-showcase',
26
+ imports: [BfPageComponent, BfHeaderComponent, BfContentComponent, BfButtonComponent],
27
+ template: `
28
+ <bf-page data-showcase="sheet">
29
+ <bf-header title="Sheet" [largeTitle]="false" />
30
+ <bf-content>
31
+ <div class="flex flex-col gap-4 p-4">
32
+ <p class="text-caption text-text-muted">
33
+ Drag the grabber between the two snap points; drag it down or flick to close, or tap the
34
+ backdrop.
35
+ </p>
36
+ <bf-button (clicked)="open()">Open sheet</bf-button>
37
+ <p data-showcase-result class="text-body">{{ result() }}</p>
38
+ </div>
39
+ </bf-content>
40
+ </bf-page>
41
+ `,
42
+ })
43
+ export class SheetShowcasePage {
44
+ private readonly sheets = inject(BfSheetService);
45
+
46
+ protected readonly result = signal('No list yet.');
47
+
48
+ async open(): Promise<void> {
49
+ const ref = this.sheets.open<ShowcaseSheetComponent, string>(ShowcaseSheetComponent, {
50
+ snapPoints: SHOWCASE_SHEET_SNAP_POINTS,
51
+ ariaLabel: 'New list',
52
+ });
53
+ const name = await ref.afterClosed;
54
+ this.result.set(name ? `Created “${name}”.` : 'Dismissed.');
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Overview
60
+
61
+ The bottom-sheet container that `BfSheetService.open()` renders on the overlay core: a
62
+ bottom-anchored `bg-surface` card with a grabber, resting at snap points (fractions of the
63
+ viewport height, applied as `--bf-sheet-height`) and rendering the content component or
64
+ template with `BF_OVERLAY_DATA` and `BfSheetRef` injectable. Dragging the handle row, any
65
+ element marked `data-bf-sheet-drag` (a header) or the content (unless it can scroll up) moves
66
+ the sheet with Pointer Events under pointer capture: rubber-banded above the top point, released
67
+ onto the nearest point (a fling faster than 0.3 px/ms picks the direction), and dismissed when
68
+ released more than 25 % below the lowest point or flung down from it — if the sheet is
69
+ dismissible. The content scrolls only at the top snap point, and at every point while the soft
70
+ keyboard is up. While the keyboard overlays the sheet's bottom edge (iOS, a mobile browser) the
71
+ sheet is lifted by the covered height (`translate`, its bottom safe-area padding dropped); while
72
+ the keyboard is up its height is capped at the keyboard's top edge minus the safe-area top minus
73
+ 8 px (a drag starts from that capped height), and the content region's `bfKeyboardAssist`
74
+ scrolls the focused control 24 px clear of the keyboard. The lift follows the keyboard (250 ms,
75
+ instant under `prefers-reduced-motion`). Hiding the keyboard restores the snap height at once
76
+ (the drop is instant, so the top edge stays on screen) and scrolls content left scrolled below
77
+ the top point back to its top. With two or more snap points the grabber is a button: a press,
78
+ `Enter` or `Space` moves one point up (from the top point back to the lowest), the arrow keys
79
+ step, `Home` and `End` jump; it is named `expand` or `collapse` (`BF_LABELS`) after the next
80
+ press, then `sheetPosition`, and every settle on another point announces `sheetPosition`
81
+ politely. A dismissible sheet starts with a visually hidden Close (`BF_LABELS.close`) that screen
82
+ readers reach and a keyboard user sees when focused. Not used directly in templates.
83
+
84
+ A bottom sheet. `BfSheetService.open(content, options)` renders a component or template in a
85
+ `bf-sheet` container — a bottom-anchored card with a grabber, over a backdrop — that rests at snap
86
+ points given as fractions of the viewport height. Drag the grabber (or a header marked
87
+ `data-bf-sheet-drag`) to move between them; drag it far enough down, or flick it, and it closes.
88
+ The content reads its data from `BF_OVERLAY_DATA` and closes itself with a result through the
89
+ injected `BfSheetRef`. You never place `<bf-sheet>` in a template yourself.
90
+
91
+ ## When to use
92
+
93
+ Use `BfSheetService` in the Capacitor shell for secondary content over the current page that the user drags between heights or away: a filter panel, a detail preview. Use [BfModalService](https://bitforge-ui.web.app/api/bf-modal-service) instead for a full task that closes itself, [BfActionSheetService](https://bitforge-ui.web.app/api/bf-action-sheet-service) for a list of actions, and [BfDialogService](https://bitforge-ui.web.app/api/bf-dialog-service) on desktop.
94
+
95
+ ## Basic usage
96
+
97
+ The content is an ordinary standalone component. It injects the data passed as `data` and the
98
+ `BfSheetRef`, and closes with a result; the caller awaits `afterClosed`.
99
+
100
+ ```ts
101
+ import { Component, inject, signal } from '@angular/core';
102
+ import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
103
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
104
+ import { BF_OVERLAY_DATA } from '@bitforgehq/ui/overlay';
105
+
106
+ /** A task as the sheet creates it. */
107
+ export interface Task {
108
+ title: string;
109
+ }
110
+
111
+ /** The sheet: a draggable header, a field and Save. */
112
+ @Component({
113
+ selector: 'app-new-task',
114
+ imports: [BfButtonComponent, BfInputComponent],
115
+ template: `
116
+ <header data-bf-sheet-drag class="text-heading px-4 pb-2">
117
+ New task in {{ data.listName }}
118
+ </header>
119
+ <div class="flex flex-col gap-3 px-4">
120
+ <bf-input label="Title" [(value)]="title" />
121
+ <bf-button (clicked)="save()">Save</bf-button>
122
+ </div>
123
+ `,
124
+ })
125
+ export class NewTaskComponent {
126
+ protected readonly data = inject<{ listName: string }>(BF_OVERLAY_DATA);
127
+ private readonly sheet = inject<BfSheetRef<Task>>(BfSheetRef);
128
+ protected readonly title = signal('');
129
+
130
+ save(): void {
131
+ this.sheet.close({ title: this.title() || 'Buy milk' });
132
+ }
133
+ }
134
+
135
+ /**
136
+ * A sheet that creates a task.
137
+ *
138
+ * `open()` passes the list name as data and awaits the new task, or `undefined` when dismissed.
139
+ *
140
+ * @preview none
141
+ */
142
+ @Component({
143
+ selector: 'app-example-sheet-basic',
144
+ imports: [BfButtonComponent],
145
+ template: `
146
+ <div class="flex flex-col items-start gap-3">
147
+ <bf-button (clicked)="add()">New task</bf-button>
148
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
149
+ </div>
150
+ `,
151
+ })
152
+ export class SheetBasicExampleComponent {
153
+ private readonly sheets = inject(BfSheetService);
154
+ protected readonly result = signal('');
155
+
156
+ protected async add(): Promise<void> {
157
+ const ref = this.sheets.open<NewTaskComponent, Task>(NewTaskComponent, {
158
+ data: { listName: 'Groceries' },
159
+ });
160
+ const task = await ref.afterClosed; // undefined when dismissed
161
+ this.result.set(task ? `Added "${task.title}".` : 'Dismissed.');
162
+ }
163
+ }
164
+ ```
165
+
166
+ A template works too; its context is `{ $implicit: ref, data }`:
167
+
168
+ ```ts
169
+ import { Component, type TemplateRef, inject, signal, viewChild } from '@angular/core';
170
+ import { BfButtonComponent } from '@bitforgehq/ui';
171
+ import { BfSheetService } from '@bitforgehq/ui/mobile';
172
+
173
+ /** The filters a list shows. */
174
+ interface Filters {
175
+ open: boolean;
176
+ }
177
+
178
+ /**
179
+ * A sheet from a template.
180
+ *
181
+ * The `ng-template` receives `{ $implicit: ref, data }`; Apply closes the sheet with the data.
182
+ *
183
+ * @preview none
184
+ */
185
+ @Component({
186
+ selector: 'app-example-sheet-template',
187
+ imports: [BfButtonComponent],
188
+ template: `
189
+ <div class="flex flex-col items-start gap-3">
190
+ <bf-button variant="outline" (clicked)="open()">Filters</bf-button>
191
+ <p class="text-caption text-text-muted">{{ result() }}</p>
192
+ </div>
193
+
194
+ <ng-template #filters let-ref let-data="data">
195
+ <div class="flex flex-col gap-3 px-4 pb-4">
196
+ <p class="text-heading">Filters</p>
197
+ <bf-button (clicked)="ref.close(data)">Apply</bf-button>
198
+ </div>
199
+ </ng-template>
200
+ `,
201
+ })
202
+ export class SheetTemplateExampleComponent {
203
+ private readonly sheets = inject(BfSheetService);
204
+ private readonly filters = viewChild.required<TemplateRef<unknown>>('filters');
205
+ protected readonly result = signal('');
206
+
207
+ protected async open(): Promise<void> {
208
+ const ref = this.sheets.open<unknown, Filters>(this.filters(), { data: { open: true } });
209
+ const applied = await ref.afterClosed;
210
+ this.result.set(applied ? 'Filters applied.' : 'Dismissed.');
211
+ }
212
+ }
213
+ ```
214
+
215
+ ## Snap points
216
+
217
+ `snapPoints` are fractions of the viewport height (the keyboard-free height from
218
+ `BfKeyboardService.restHeight()`, else `innerHeight`), default `[0.5, 0.92]`; `initial` is the index
219
+ the sheet opens at (default 0). The height is applied as `--bf-sheet-height` on the `bf-sheet`
220
+ host. Move programmatically with `snapTo(index)` and read the current point from `snapIndex()`:
221
+
222
+ ```ts
223
+ import { Component, computed, inject } from '@angular/core';
224
+ import { BfButtonComponent } from '@bitforgehq/ui';
225
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
226
+
227
+ /** Project details that expand to the top snap point. */
228
+ @Component({
229
+ selector: 'app-details',
230
+ imports: [BfButtonComponent],
231
+ template: `
232
+ <div class="flex flex-col gap-3 px-4 pb-4">
233
+ <p data-bf-sheet-drag class="text-heading">Atlas rebrand</p>
234
+ @if (!expanded()) {
235
+ <bf-button variant="tonal" (clicked)="ref.snapTo(2)">Show everything</bf-button>
236
+ } @else {
237
+ <p class="text-body">Owner, budget, milestones and files.</p>
238
+ }
239
+ </div>
240
+ `,
241
+ })
242
+ export class DetailsComponent {
243
+ protected readonly ref = inject(BfSheetRef);
244
+ protected readonly expanded = computed(() => this.ref.snapIndex() === 2);
245
+ }
246
+
247
+ /**
248
+ * A sheet with three snap points.
249
+ *
250
+ * It opens at the middle point (`initial: 1`); `snapTo(2)` moves it up and `snapIndex()` tracks
251
+ * the current point, also after a drag.
252
+ *
253
+ * @preview none
254
+ */
255
+ @Component({
256
+ selector: 'app-example-sheet-snap-points',
257
+ imports: [BfButtonComponent],
258
+ template: `<bf-button (clicked)="open()">Project details</bf-button>`,
259
+ })
260
+ export class SheetSnapPointsExampleComponent {
261
+ private readonly sheets = inject(BfSheetService);
262
+
263
+ protected open(): void {
264
+ this.sheets.open(DetailsComponent, { snapPoints: [0.3, 0.6, 0.92], initial: 1 });
265
+ }
266
+ }
267
+ ```
268
+
269
+ On release, a fling faster than 0.3 px/ms goes to the next point in its direction; otherwise the
270
+ sheet settles on the nearest point. Above the top point the drag is rubber-banded (30 % of the
271
+ pointer's travel).
272
+
273
+ With two or more snap points the grabber is also a button, so the sheet can be resized without
274
+ dragging: a tap, a click, <kbd>Enter</kbd> or <kbd>Space</kbd> moves it one point up, and from the
275
+ top point back to the lowest; the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> step and jump
276
+ between the points (see [Keyboard](#keyboard)). Its name says what the next press does and where
277
+ the sheet is: `provideBfLabels()`'s `expand` ("Expand") below the top point, `collapse`
278
+ ("Collapse") at it, then `sheetPosition(position, count)` ("Sheet at 1 of 2"). Each time the sheet
279
+ settles on another snap point, however it got there (the button, a key, a drag or `snapTo()`),
280
+ `sheetPosition` is announced politely through CDK `LiveAnnouncer`. With a single snap point the
281
+ grabber is decorative, there is no button and nothing is announced.
282
+
283
+ ## Focus on open
284
+
285
+ A sheet opens with focus on itself (`autoFocus: 'panel'`, the default): the panel is a named
286
+ `role="dialog"` with `tabindex="-1"`, so a screen reader starts reading inside the sheet and no soft
287
+ keyboard comes up to cover it (on Android, focusing a text field on open raises the keyboard at
288
+ once). Tab moves on to the sheet's controls. Pass `autoFocus: 'first-field'` when the sheet exists
289
+ to be typed into (a quick-add field): focus goes to the element marked `autofocus` (or
290
+ `cdkFocusInitial`), else to the first focusable element of the content, never to the sheet's own
291
+ Close and grabber, and the keyboard comes up with the sheet.
292
+
293
+ ```ts
294
+ import { Component, inject, signal } from '@angular/core';
295
+ import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
296
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
297
+
298
+ /** A quick-add sheet whose only job is typing a title. */
299
+ @Component({
300
+ selector: 'app-quick-add',
301
+ imports: [BfButtonComponent, BfInputComponent],
302
+ template: `
303
+ <div class="flex flex-col gap-3 px-4">
304
+ <h2 class="text-heading">Quick add</h2>
305
+ <bf-input label="Title" [(value)]="title" />
306
+ <bf-button (clicked)="save()">Add</bf-button>
307
+ </div>
308
+ `,
309
+ })
310
+ export class QuickAddComponent {
311
+ private readonly sheet = inject<BfSheetRef<string>>(BfSheetRef);
312
+ protected readonly title = signal('');
313
+
314
+ save(): void {
315
+ this.sheet.close(this.title());
316
+ }
317
+ }
318
+
319
+ /**
320
+ * A sheet that opens with the keyboard up.
321
+ *
322
+ * By default a sheet focuses itself, so no soft keyboard hides it; `autoFocus: 'first-field'`
323
+ * focuses the Title field instead (or the element marked `autofocus`).
324
+ *
325
+ * @preview none
326
+ */
327
+ @Component({
328
+ selector: 'app-example-sheet-autofocus',
329
+ imports: [BfButtonComponent],
330
+ template: `
331
+ <div class="flex flex-col items-start gap-3">
332
+ <bf-button (clicked)="quickAdd()">Quick add</bf-button>
333
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
334
+ </div>
335
+ `,
336
+ })
337
+ export class SheetAutofocusExampleComponent {
338
+ private readonly sheets = inject(BfSheetService);
339
+ protected readonly result = signal('');
340
+
341
+ protected async quickAdd(): Promise<void> {
342
+ const ref = this.sheets.open<QuickAddComponent, string>(QuickAddComponent, {
343
+ snapPoints: [0.92],
344
+ autoFocus: 'first-field',
345
+ });
346
+ const title = await ref.afterClosed;
347
+ this.result.set(title ? `Added "${title}".` : 'Dismissed.');
348
+ }
349
+ }
350
+ ```
351
+
352
+ The sheet provides its content a fresh `BfPageContext` (with `mainLandmark` false), so a
353
+ `bf-content` inside it is no second `main` and never takes over the page's scroller, also when
354
+ the sheet is opened with the page's injector.
355
+
356
+ Action sheets and modals are unchanged: `BfActionSheetService` and `BfModalService` keep the
357
+ focus they had: an action sheet starts on its Cancel button, and a modal on the first focusable
358
+ element of its content.
359
+
360
+ ## Dragging and scrolling
361
+
362
+ The 24 px handle row always drags the sheet; mark any other element (typically the header of your
363
+ content) with `data-bf-sheet-drag` to make it a drag handle too — give it `cursor-grab
364
+ active:cursor-grabbing` as well, so its pointer affordance matches the handle row's. A press only
365
+ becomes a drag after 6 px of vertical travel, so buttons inside a header still get their clicks.
366
+ The same holds on the grabber button: a press released within 6 px is a click (it cycles the snap
367
+ points), a longer one drags the sheet and does not.
368
+
369
+ The content scrolls only at the top snap point (`overflow-y-auto` there, `overflow-hidden` below
370
+ it, always `overscroll-contain`) — and at every point while the soft keyboard is up. Below the top
371
+ point, dragging the content moves the sheet; at the top point, a downward drag on the content
372
+ moves the sheet only once the content is scrolled to the top — the same hand-off as iOS and Ionic
373
+ sheets.
374
+
375
+ ## Soft keyboard
376
+
377
+ A sheet keeps its focused field and its actions above the soft keyboard:
378
+
379
+ - **Lift.** Where the keyboard overlays the page (iOS with `Keyboard.resize: 'none'`, a mobile
380
+ browser), the sheet moves up by the covered height while the keyboard is up, and drops its
381
+ bottom safe-area padding (the keyboard covers the home indicator). It slides up with the
382
+ keyboard (250 ms, instant under `prefers-reduced-motion`); when it arrives, the content's
383
+ keyboard assist measures again. On Android the WebView itself shrinks above the keyboard, so
384
+ there is nothing to lift.
385
+ - **Cap.** While the keyboard is up, the sheet is at most as tall as the space between the
386
+ keyboard's top edge and the safe-area top, minus 8 px — in phone landscape that can be less than
387
+ its snap height. A drag starts from that capped height.
388
+ - **Scroll and reveal.** While the keyboard is up the content scrolls at every snap point, and its
389
+ keyboard assist (`bfKeyboardAssist`) scrolls the focused field 24 px above the keyboard.
390
+ - **Restore.** Hiding the keyboard removes the lift and the cap together, at once: the sheet
391
+ rests at its snap height again with its top edge on screen throughout, and content left
392
+ scrolled below the top point starts at its top again.
393
+
394
+ The snap heights themselves never change. Keep the primary action close below
395
+ the fields, so the lifted sheet shows it without scrolling in portrait.
396
+
397
+ ## Dismissible
398
+
399
+ By default a backdrop tap, `Escape`, the hardware back button and a release more than 25 % of the
400
+ lowest point's height below it (or a downward fling from it) close the sheet, resolving
401
+ `afterClosed` with `undefined`. With `dismissible: false` only `close()` does: the backdrop and
402
+ `Escape` are ignored, a downward drag rubber-bands and settles back, and the hardware back button
403
+ is swallowed while the sheet is open (it does not fall through to the page behind).
404
+
405
+ ```ts
406
+ import { Component, inject } from '@angular/core';
407
+ import { BfButtonComponent } from '@bitforgehq/ui';
408
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
409
+
410
+ /** Terms that must be accepted. */
411
+ @Component({
412
+ selector: 'app-terms-sheet',
413
+ imports: [BfButtonComponent],
414
+ template: `
415
+ <div class="flex flex-col gap-3 p-4">
416
+ <p class="text-body">Accept the updated terms to continue.</p>
417
+ <bf-button (clicked)="ref.close()">Done</bf-button>
418
+ </div>
419
+ `,
420
+ })
421
+ export class TermsComponent {
422
+ protected readonly ref = inject(BfSheetRef);
423
+ }
424
+
425
+ /**
426
+ * A sheet that must be answered.
427
+ *
428
+ * `dismissible: false` turns off the drag down, the backdrop and the back button.
429
+ *
430
+ * @preview none
431
+ */
432
+ @Component({
433
+ selector: 'app-example-sheet-dismissible',
434
+ imports: [BfButtonComponent],
435
+ template: `<bf-button (clicked)="open()">Show the terms</bf-button>`,
436
+ })
437
+ export class SheetDismissibleExampleComponent {
438
+ private readonly sheets = inject(BfSheetService);
439
+
440
+ protected open(): void {
441
+ this.sheets.open(TermsComponent, { dismissible: false });
442
+ }
443
+ }
444
+ ```
445
+
446
+ Include a visible close or cancel control in the sheet's content, as `bf-modal` requires; the
447
+ built-in hidden Close is the safety net for screen readers, not a replacement. Every dismissible
448
+ sheet renders that hidden Close (`provideBfLabels()`'s `close`, "Close") as its first element: a
449
+ screen reader reaches it by swiping, and a keyboard user sees it as a pill at the top-end corner
450
+ when <kbd>Tab</kbd> lands on it. A `dismissible: false` sheet has none.
451
+
452
+ ## Navigation
453
+
454
+ A navigation to another URL path closes the sheet, resolving `afterClosed` with `undefined`, also
455
+ a `dismissible: false` one: the page it belongs to is gone. A query- or fragment-only change
456
+ leaves it open; a matrix-parameter change (`/list;sort=name`) is a path change and closes it. In
457
+ the `/mobile` shell every push, every pop (the header back button, swipe-back,
458
+ `BfNavController.back()`) and every tab switch changes the path, so a sheet opened on a page
459
+ closes when that page is left. The Android hardware back button with a sheet open closes the
460
+ sheet only (a `dismissible: false` sheet swallows the press); its handler runs at priority 100,
461
+ above the page pop, so one press never closes it and pops the page as well. The same holds for
462
+ Android back in a browser (Chrome, a PWA, no Capacitor): where `CloseWatcher` exists, the back
463
+ press closes the sheet and leaves the URL alone, and the next press goes back in history. Pass
464
+ `closeOnNavigation: false` for a sheet that should stay over the next page.
465
+
466
+ ```ts
467
+ import { Component, inject } from '@angular/core';
468
+ import { BfButtonComponent } from '@bitforgehq/ui';
469
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
470
+
471
+ /** A player that outlives its page. */
472
+ @Component({
473
+ selector: 'app-player',
474
+ imports: [BfButtonComponent],
475
+ template: `
476
+ <div class="flex flex-col gap-3 p-4">
477
+ <p class="text-body">Now playing: Team call recording</p>
478
+ <bf-button (clicked)="ref.close()">Done</bf-button>
479
+ </div>
480
+ `,
481
+ })
482
+ export class PlayerComponent {
483
+ protected readonly ref = inject(BfSheetRef);
484
+ }
485
+
486
+ /**
487
+ * A sheet that stays open across navigations.
488
+ *
489
+ * `closeOnNavigation: false` keeps a player open while the page behind it changes.
490
+ *
491
+ * @preview none
492
+ */
493
+ @Component({
494
+ selector: 'app-example-sheet-navigation',
495
+ imports: [BfButtonComponent],
496
+ template: `<bf-button (clicked)="open()">Play</bf-button>`,
497
+ })
498
+ export class SheetNavigationExampleComponent {
499
+ private readonly sheets = inject(BfSheetService);
500
+
501
+ protected open(): void {
502
+ this.sheets.open(PlayerComponent, { closeOnNavigation: false });
503
+ }
504
+ }
505
+ ```
506
+
507
+ A sheet opened before the first navigation ends (a "What's new" sheet from the root component or
508
+ an `APP_INITIALIZER`) closes when the start URL redirects (`''` to `'today'`): open it after the
509
+ first `NavigationEnd`, or pass `closeOnNavigation: false`. A `history.pushState()` made outside
510
+ Angular's `Location` is not seen.
511
+
512
+ A route `canDeactivate` guard protects a form sheet's unsaved input only against in-app
513
+ navigations: the sheet stays open while the guard's confirm asks. On the web the browser's back
514
+ button changes the URL before the guard runs, so the sheet has already closed when "Stay" is
515
+ answered (the Android hardware back button never gets that far: the sheet takes it first). A
516
+ sheet that must keep its draft passes `closeOnNavigation: false` or saves the draft as it
517
+ changes.
518
+
519
+ ## Labels and the injector
520
+
521
+ Default strings come from `provideBfLabels()` (`sheet`, "Sheet"; `close`, "Close"; `expand`,
522
+ "Expand"; `collapse`, "Collapse"; `sheetPosition`, "Sheet at 1 of 2"). The service is a root singleton, so it reads
523
+ the app-level labels; to use a feature's own `provideBfLabels()` (in a component's `providers`),
524
+ pass that component's injector. It then also becomes the parent of the sheet's content injector,
525
+ so the content resolves the feature's providers.
526
+
527
+ ```ts
528
+ import { Component, Injector, inject } from '@angular/core';
529
+ import { BfButtonComponent, provideBfLabels } from '@bitforgehq/ui';
530
+ import { BfSheetRef, BfSheetService } from '@bitforgehq/ui/mobile';
531
+
532
+ /** A note in the feature's language. */
533
+ @Component({
534
+ selector: 'app-new-note',
535
+ imports: [BfButtonComponent],
536
+ template: `
537
+ <div class="flex flex-col gap-3 p-4">
538
+ <p class="text-body">Neue Notiz</p>
539
+ <bf-button (clicked)="ref.close()">Fertig</bf-button>
540
+ </div>
541
+ `,
542
+ })
543
+ export class NewNoteComponent {
544
+ protected readonly ref = inject(BfSheetRef);
545
+ }
546
+
547
+ /**
548
+ * A sheet that reads a feature's labels.
549
+ *
550
+ * This feature provides German labels; passing the opener's injector is what names the sheet
551
+ * "Blatt" for screen readers (its `sheet` label) and lets its content read the same labels.
552
+ *
553
+ * @preview none
554
+ */
555
+ @Component({
556
+ selector: 'app-example-sheet-injector',
557
+ imports: [BfButtonComponent],
558
+ providers: [provideBfLabels({ sheet: 'Blatt' })],
559
+ template: `<bf-button (clicked)="open()">Neue Notiz</bf-button>`,
560
+ })
561
+ export class SheetInjectorExampleComponent {
562
+ private readonly sheets = inject(BfSheetService);
563
+ private readonly injector = inject(Injector);
564
+
565
+ protected open(): void {
566
+ this.sheets.open(NewNoteComponent, { injector: this.injector });
567
+ }
568
+ }
569
+ ```
570
+
571
+ ## Desktop
572
+
573
+ `BfSheetService`'s bottom sheet is phone-shaped and meant for the Capacitor shell
574
+ (`provideBfShell()`). On a desktop web app, use the desktop overlays from `@bitforgehq/ui/overlay`
575
+ instead: they need only `@angular/cdk`, no Capacitor and no router. A desktop dialog locks the
576
+ page's scroll, makes the page behind it `inert`, and traps and restores focus.
577
+
578
+ Open a form or a detail view in a `bf-dialog` with `BfDialogService.open()`, and a short list of
579
+ actions in a `bf-menu` from a `bf-menu-button`.
580
+
581
+ ```ts
582
+ import { Component, inject, signal } from '@angular/core';
583
+ import { BfButtonComponent, BfCheckboxComponent } from '@bitforgehq/ui';
584
+ import {
585
+ BF_DIALOG_DATA,
586
+ BfDialogComponent,
587
+ BfDialogRef,
588
+ BfDialogService,
589
+ } from '@bitforgehq/ui/overlay';
590
+
591
+ /** The filters a list shows. */
592
+ export interface Filters {
593
+ open: boolean;
594
+ mine: boolean;
595
+ }
596
+
597
+ /** A filter dialog that returns the edited filters and can highlight one of them. */
598
+ @Component({
599
+ selector: 'app-filter-dialog',
600
+ imports: [BfButtonComponent, BfCheckboxComponent, BfDialogComponent],
601
+ template: `
602
+ <bf-dialog title="Filters">
603
+ <div class="flex flex-col gap-2">
604
+ <bf-checkbox
605
+ label="Open tasks only"
606
+ [class]="highlighted() === 'status' ? 'bg-primary-soft rounded-field' : ''"
607
+ [(checked)]="open"
608
+ />
609
+ <bf-checkbox label="Assigned to me" [(checked)]="mine" />
610
+ </div>
611
+ <div bfSlot="footer">
612
+ <bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
613
+ <bf-button (clicked)="ref.close({ open: open(), mine: mine() })">Apply</bf-button>
614
+ </div>
615
+ </bf-dialog>
616
+ `,
617
+ })
618
+ export class FilterDialogComponent {
619
+ protected readonly ref = inject<BfDialogRef<Filters>>(BfDialogRef);
620
+ private readonly current = inject<Filters>(BF_DIALOG_DATA);
621
+ protected readonly open = signal(this.current.open);
622
+ protected readonly mine = signal(this.current.mine);
623
+ protected readonly highlighted = signal('');
624
+
625
+ highlight(field: string): void {
626
+ this.highlighted.set(field);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * A typed dialog result.
632
+ *
633
+ * `BfDialogRef<Filters, FilterDialogComponent>` types both the result and `componentInstance`;
634
+ * a dismissed dialog keeps the current filters.
635
+ */
636
+ @Component({
637
+ selector: 'app-example-dialog-results',
638
+ imports: [BfButtonComponent],
639
+ template: `
640
+ <div class="flex flex-col items-start gap-3">
641
+ <bf-button variant="outline" (clicked)="edit()">Filters…</bf-button>
642
+ <p class="text-caption text-text-muted">
643
+ {{ filters().open ? 'Open' : 'All' }} tasks · {{ filters().mine ? 'mine' : 'everyone' }}
644
+ </p>
645
+ </div>
646
+ `,
647
+ })
648
+ export class DialogResultsExampleComponent {
649
+ private readonly dialogs = inject(BfDialogService);
650
+ protected readonly filters = signal<Filters>({ open: true, mine: false });
651
+
652
+ protected async edit(): Promise<void> {
653
+ const current = this.filters();
654
+ const ref = this.dialogs.open<FilterDialogComponent, Filters, Filters>(FilterDialogComponent, {
655
+ data: current,
656
+ });
657
+ ref.componentInstance?.highlight('status');
658
+ this.filters.set((await ref.afterClosed) ?? current);
659
+ }
660
+ }
661
+ ```
662
+
663
+ ## Theming
664
+
665
+ - Card: `bg-surface`, `rounded-t-card` (`--radius-card`), `shadow-floating`, `pb-safe-bottom`
666
+ (`--spacing-safe-bottom`).
667
+ - Grabber: `bg-control-off`, 36 × 5 px, `rounded-full`; `ButtonText` under forced colors. With a
668
+ mouse the handle row shows a `grab` cursor (`grabbing` while pressed) and the grabber darkens to
669
+ `text-muted` on hover.
670
+ - Grabber button (two or more snap points): the full-width 24 px handle row, extended to 44 px
671
+ under a coarse pointer, downward only (20 px into the top of the content, so a tap just above
672
+ the sheet still reaches the backdrop); `focus-visible:ring-4 ring-ring` (inset) for the
673
+ keyboard focus. Keep the content's own controls out of its top 20 px, or give them `relative`
674
+ positioning (library buttons have it), so they paint above the extension.
675
+ - Hidden Close (dismissible sheets): `sr-only` until focused, then a `rounded-full
676
+ bg-surface-muted text-label-sm` pill at the top-end corner (`end-3 top-3`) with the focus ring.
677
+ - Backdrop: `bf-overlay-backdrop` from the overlay core (a black scrim: 40 % in light mode, 60 % in dark).
678
+ - Height: `--bf-sheet-height` on the `bf-sheet` host, written by the component — do not override it.
679
+ - Keyboard: inline `translate`, `padding-bottom` and `max-height` on the host while the keyboard
680
+ is up — do not set your own.
681
+
682
+ ## Accessibility
683
+
684
+ The overlay panel is a `role="dialog"` with `aria-modal="true"` and a name: the
685
+ `ariaLabel` option when given; else the first heading in the content that is not `aria-hidden`
686
+ (`h1`–`h6` or `role="heading"`), found once the content has rendered (or when it first appears later, e.g. behind `@if`) and
687
+ referenced through
688
+ `aria-labelledby` (it gets an id `bf-overlay-title-<n>` when it has none); else
689
+ `provideBfLabels()`'s `sheet` ("Sheet"); the page stack behind it is `inert`. Focus moves to the
690
+ panel itself (or, with `autoFocus: 'first-field'`, to the content's first field), Tab is trapped
691
+ inside, and focus returns to the element that opened the sheet on close. `Escape` and the hardware back button close a dismissible
692
+ sheet. The sheet's own controls, the hidden Close and the grabber button, come first in the Tab
693
+ order but never take the initial focus. VoiceOver on iOS does not deliver its two-finger scrub
694
+ (escape) gesture to web content, so the hidden Close is how a VoiceOver user leaves a sheet: keep
695
+ a visible close or cancel control in the content as well.
696
+
697
+ With two or more snap points the grabber is a button (named after the next press and the current
698
+ position, "Expand, Sheet at 1 of 2"), the single-pointer alternative to dragging that WCAG 2.5.7
699
+ (Dragging Movements) asks for. A name change on a focused button is not reliably re-read, so the
700
+ new position is also announced politely ("Sheet at 2 of 2") whenever the sheet settles on another
701
+ point; focus stays on the button. A screen reader's double tap (a click with `detail` 0) always
702
+ moves the sheet, and the grabber ignores key combinations with <kbd>Alt</kbd>, <kbd>Ctrl</kbd> or
703
+ <kbd>Meta</kbd>. With one snap point the grabber is `aria-hidden`. Under
704
+ `prefers-reduced-motion` the sheet cross-fades in and out and settles on snap points instantly;
705
+ a drag still follows the pointer.
706
+
707
+ ### Keyboard
708
+
709
+ The grabber button (two or more snap points) takes these keys; none of them dismisses the sheet.
710
+
711
+ | Key | Action |
712
+ | ------------------------------------------------ | ---------------------------------------------------------------------- |
713
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves between the sheet's controls (Close and grabber first), wrapping |
714
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Grabber: one snap point up; from the top point, back to the lowest |
715
+ | <kbd>ArrowUp</kbd> | Grabber: one snap point up (stops at the top point) |
716
+ | <kbd>ArrowDown</kbd> | Grabber: one snap point down (stops at the lowest point, never closes) |
717
+ | <kbd>Home</kbd> | Grabber: the lowest snap point |
718
+ | <kbd>End</kbd> | Grabber: the top snap point |
719
+ | <kbd>Escape</kbd> | Closes a dismissible sheet |
720
+
721
+ ## Notes
722
+
723
+ ### Do
724
+
725
+ - Name the sheet by its purpose ("New task", "Filters"): start the content with a heading, or pass `ariaLabel`.
726
+ - Mark your content's header with `data-bf-sheet-drag` so the whole top area drags.
727
+ - Leave `autoFocus` at `'panel'` for a sheet the user reads or picks from; use `'first-field'` only
728
+ for a sheet that exists to be typed into.
729
+ - Close with a result through `BfSheetRef` and await `afterClosed` in the caller.
730
+ - Keep a visible close or cancel control in the content; the hidden Close is for screen readers.
731
+
732
+ ### Don't
733
+
734
+ - Don't use a non-dismissible sheet for content the user may want to leave — use a modal with an
735
+ explicit cancel instead.
736
+ - Don't make the lowest snap point so small that the content's primary action is hidden.
737
+ - Don't nest scrolling containers inside the content; let the sheet's content region scroll.
738
+
739
+ ## Migration notes
740
+
741
+ ### 0.3.0
742
+
743
+ **Behaviour:** a navigation to another URL path (a push, a pop, a tab switch) closes it with no
744
+ result, also a `dismissible: false` one; new `closeOnNavigation` option (default true). Migration:
745
+ pass `closeOnNavigation: false` to keep it open (see Migration 19).
746
+
747
+ **Behaviour:** named by the first heading: without `ariaLabel`, the panel is named by the first
748
+ heading in its content that is not `aria-hidden` (`h1`–`h6` or `role="heading"`, given an id
749
+ `bf-overlay-title-<n>` when it has none) through `aria-labelledby`, once the content has rendered;
750
+ without a heading, by `provideBfLabels()`'s `sheet` ("Sheet"). An explicit `ariaLabel` always wins.
751
+
752
+ New `injector` option: labels resolve from it per call (default the root injector) and it becomes
753
+ the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
754
+ other providers reach the sheet.
755
+
756
+ A heading that renders after the overlay opened (behind `@if`, a `resource()`) still names it: the
757
+ panel is watched until its first heading appears.
758
+
759
+ JSDoc of `sheet-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols; `@param`
760
+ docs. No behaviour change.
761
+
762
+ JSDoc of `sheet.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
763
+ `@default` on the option properties the code applies; `@param` docs. No behaviour change.
764
+
765
+ **Behaviour:** the keyboard lift and cap (`translate`, `padding-bottom`, `max-height`,
766
+ `transition`) are written in the render phase (`afterRenderEffect({ write })`) instead of an
767
+ `effect`; internal timing only, no API change.
768
+
769
+ With two or more snap points the grabber is a button (`[data-bf-sheet-grabber-button]`), so the
770
+ sheet can be resized without dragging (WCAG 2.5.7): a tap, click, `Enter` or `Space` moves it one
771
+ point up, and from the top point back to the lowest; `ArrowUp`/`ArrowDown` step one point (never
772
+ past the ends, never dismissing), `Home`/`End` jump to the lowest/top point, all through
773
+ `snapTo()` (instant under reduced motion). It is named after the next press and the position
774
+ ("Expand, Sheet at 1 of 2": `expand` below the top point, `collapse` at it, then
775
+ `sheetPosition`); focus stays on it, and every settle on another point (button, key, drag or
776
+ `snapTo()`) announces `sheetPosition` politely through CDK `LiveAnnouncer`. A drag that starts on
777
+ it still drags (a pointer click within the 6 px slop is the press; a keyboard or screen-reader
778
+ click, `detail` 0, is never swallowed). Alt, Ctrl and Meta combinations are left alone. It is
779
+ the full-width 24 px handle row, extended downward to 44 px under a coarse pointer, so a tap just
780
+ above the sheet reaches the backdrop. With one snap point the grabber stays decorative
781
+ (`aria-hidden`) and nothing is announced.
782
+
783
+ **Behaviour:** every dismissible sheet renders a visually hidden "Close" button
784
+ (`provideBfLabels()`'s `close`) as its first element, shown as a pill at the top-end corner when
785
+ focused; activating it closes the sheet. VoiceOver reaches it by swiping, since WKWebView does not
786
+ deliver the two-finger scrub to web content. A `dismissible: false` sheet has none. The Close and
787
+ the grabber button never take the initial focus. Keep a visible close or cancel control in the
788
+ content (Migration 28).
789
+
790
+ **Behaviour:** a sheet opens with focus on its panel (the `role="dialog"` pane, named and
791
+ `tabindex="-1"`), so a screen reader lands inside it and no soft keyboard comes up on open. New
792
+ option `autoFocus?: BfSheetAutoFocus` (`'panel' | 'first-field'`, default `'panel'`; new exported
793
+ type), mapped to the overlay core's `autoFocus` (`'first-field'` → `'first-tabbable'`, which
794
+ honours `[autofocus]`/`[cdkFocusInitial]` and skips the sheet's Close and grabber). Action sheets
795
+ and modals are unchanged. See Migration 33.
796
+
797
+ `bf-sheet` provides a fresh `BfPageContext` (`mainLandmark` false) to its content: a `bf-content`
798
+ in a sheet opened with a page's injector no longer resolves the page's context, so it is no second
799
+ `main` and does not replace the page's registered scroller.
800
+
801
+ Android back in a browser (no Capacitor) closes an open sheet first where `CloseWatcher` exists
802
+ (see `### overlay core`, Migration 35).
803
+
804
+ ### 0.2.0
805
+
806
+ New `BfSheetService.open(componentOrTemplate, { snapPoints, initial, dismissible, data, ariaLabel })`
807
+ in `@bitforgehq/ui/mobile`, a bottom sheet on the overlay core (`position: 'bottom'`, back priority
808
+ 100, not counted as a modal; slides up on open and down on close). It returns a `BfSheetRef`
809
+ (extends `BfOverlayRef`) with `snapTo(index)` and a `snapIndex` signal; the content injects the
810
+ caller's `data` as `BF_OVERLAY_DATA` and the ref as `BfSheetRef` and `BfOverlayRef` (a template
811
+ gets `{ $implicit: ref, data }`). The `bf-sheet` container is a `bg-surface rounded-t-card
812
+ shadow-floating` card with `pb-safe-bottom`, a 36 × 5 px `bg-control-off` grabber (`aria-hidden`,
813
+ `ButtonText` under forced colors)
814
+ in a 24 px `touch-none` handle row and a content region; its height is `--bf-sheet-height`, a snap
815
+ point (fraction of `BfKeyboardService.restHeight()`, else `innerHeight`; default `[0.5, 0.92]`)
816
+ re-applied on resize. Dragging the handle row, any `[data-bf-sheet-drag]` element or the content
817
+ (below the top point, or downward at the top point while scrolled to 0) moves the sheet with
818
+ Pointer Events under pointer capture and a non-passive `touchmove` guard: rubber-banded (30 %)
819
+ above the top point, released onto the nearest point or, faster than 0.3 px/ms, the next one in
820
+ the fling's direction; a release more than 25 % of the lowest point's height below it, or a
821
+ downward fling from it, dismisses. The content scrolls (`overflow-y-auto`) only at the top point,
822
+ `overflow-hidden` below it, always `overscroll-contain`. `dismissible: false` ignores the backdrop,
823
+ `Escape` and downward dismissal and swallows the hardware back button while open. Settling is a
824
+ 260 ms WAAPI animation, instant under `prefers-reduced-motion` (the drag still tracks the
825
+ pointer). The panel is `role="dialog"` `aria-modal` with `aria-label` (default "Sheet"), focus
826
+ moves in and returns on close.
827
+
828
+ Fixed: the handle row shows a `grab` cursor (`grabbing` while pressed) and the grabber darkens to
829
+ `text-muted` on hover.
830
+
831
+ Fixed (behaviour change): the soft keyboard no longer covers a sheet's focused field and actions.
832
+ While the keyboard overlays the sheet's bottom edge (iOS with `Keyboard.resize: 'none'`, a mobile
833
+ browser), `bf-sheet` is lifted by the covered height (an inline `translate`, bottom safe-area
834
+ padding dropped, since the keyboard covers the home indicator); while the keyboard is up at all
835
+ (also Android, where the WebView resizes and nothing is lifted) its height is capped at the
836
+ keyboard's top edge minus the safe-area top minus 8 px (`max-height`), and the content region
837
+ scrolls at every snap point, with `bfKeyboardAssist` revealing the focused control 24 px above the
838
+ keyboard. The lift follows the keyboard with a 250 ms `translate` transition on the iOS curve
839
+ (instant under `prefers-reduced-motion`), and is left alone once the sheet is closing. The snap
840
+ heights (`--bf-sheet-height`) are unchanged, and a drag of a capped sheet starts from its rendered
841
+ (capped) height, so it moves from the first pixel. Hiding the keyboard removes the lift and the
842
+ cap together and at once (the drop is not animated, so a sheet at a snap point taller than the cap
843
+ never has its top edge above the screen), so the sheet rests at its snap height again, and content
844
+ left scrolled while the keyboard was up returns to its top when the keyboard hides below the top
845
+ snap point (a drag between snap points without the keyboard keeps the scroll offset, as before).