@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,613 @@
1
+ # bf-modal
2
+
3
+ Modal overlay shown as an inset card over the scaled-down page, or full screen
4
+
5
+ `BfModalComponent` 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
+ type BfModalPresentation,
16
+ BfModalService,
17
+ BfPageComponent,
18
+ } from '@bitforgehq/ui/mobile';
19
+ import { type ShowcaseModalData, ShowcaseModalComponent } from './showcase-modal.component';
20
+
21
+ /** `bf-modal` showcase: the same note editor as a page-sheet card and as a fullscreen modal. */
22
+ @Component({
23
+ selector: 'app-modal-showcase',
24
+ imports: [BfPageComponent, BfHeaderComponent, BfContentComponent, BfButtonComponent],
25
+ template: `
26
+ <bf-page data-showcase="modal">
27
+ <bf-header title="Modal" [largeTitle]="false" />
28
+ <bf-content>
29
+ <div class="flex flex-col gap-4 p-4">
30
+ <p class="text-caption text-text-muted">
31
+ In iPhone mode the sheet presentation scales the page behind it down; the fullscreen one
32
+ leaves it as it is.
33
+ </p>
34
+ <bf-button (clicked)="open('sheet')">Open sheet modal</bf-button>
35
+ <bf-button variant="tonal" (clicked)="open('fullscreen')"
36
+ >Open fullscreen modal</bf-button
37
+ >
38
+ <p data-showcase-result class="text-body">{{ result() }}</p>
39
+ </div>
40
+ </bf-content>
41
+ </bf-page>
42
+ `,
43
+ })
44
+ export class ModalShowcasePage {
45
+ private readonly modals = inject(BfModalService);
46
+
47
+ protected readonly result = signal('No note yet.');
48
+
49
+ async open(presentation: BfModalPresentation): Promise<void> {
50
+ const data: ShowcaseModalData = { presentation };
51
+ const ref = this.modals.open<ShowcaseModalComponent, string>(ShowcaseModalComponent, {
52
+ data,
53
+ presentation,
54
+ ariaLabel: 'New note',
55
+ });
56
+ const title = await ref.afterClosed;
57
+ this.result.set(title ? `Saved “${title}”.` : 'Closed without saving.');
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Overview
63
+
64
+ The modal frame that `BfModalService.open()` renders on the overlay core, `position:
65
+ 'fullscreen'`: a `sheet` presentation (default) is a card whose top sits 10 px below the top
66
+ safe area (UIKit's page-sheet inset; `bg-bg rounded-t-card shadow-floating`) over the parked page stack, which scales down and
67
+ grows rounded corners behind it (`--bf-modal-depth`, written by `BfOverlayStack`, consumed by
68
+ `bf-stack-outlet`'s host rule); `fullscreen` covers the viewport edge to edge with no scale.
69
+ Like `bf-page`, it provides a fresh `BfPageContext` so the content's `bf-header` and
70
+ `bf-content` work inside it (with `mainLandmark` false, so its content is no `main` landmark,
71
+ and in the `sheet` presentation with `safeAreaTop` false, so the header adds no status-bar band
72
+ inside the card) — the content supplies its own header (typically a close
73
+ `bf-icon-button` in `start`) and content region. While the soft keyboard overlays the bottom of
74
+ a `sheet` card (iOS, a mobile browser), the card is padded at the bottom by the covered height,
75
+ so the content's `bf-content` ends at the keyboard's top edge and its keyboard assist reveals
76
+ the focused control there. Not used directly in templates.
77
+
78
+ A page-level modal. `BfModalService.open(component, options)` renders a component in a `bf-modal`
79
+ frame on the overlay core, either a page-sheet card over the page stack (default) or a fullscreen
80
+ frame. Unlike `bf-sheet`, there is no backdrop dismiss and no drag-to-dismiss — the content closes
81
+ itself, typically from a close control in its own `bf-header`. You never place `<bf-modal>` in a
82
+ template yourself.
83
+
84
+ ## When to use
85
+
86
+ Use `BfModalService` in the Capacitor shell for a self-contained task that takes over the screen and closes itself: compose a message, edit a profile. Use [BfSheetService](https://bitforge-ui.web.app/api/bf-sheet-service) instead for content the user can drag away or keep half open, and [BfDialogService](https://bitforge-ui.web.app/api/bf-dialog-service) on desktop.
87
+
88
+ ## Basic usage
89
+
90
+ The content is an ordinary standalone component with its own `bf-header` (`largeTitle="false"`)
91
+ and `bf-content` — `bf-modal` gives it a `BfPageContext`, exactly like `bf-page`, so the two work
92
+ together (the header can, for example, collapse against the content's scroll).
93
+
94
+ ```ts
95
+ import { Component, inject, signal } from '@angular/core';
96
+ import { BfButtonComponent, BfIconButtonComponent, BfInputComponent } from '@bitforgehq/ui';
97
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
98
+ import { BfContentComponent, BfHeaderComponent, BfModalService } from '@bitforgehq/ui/mobile';
99
+ import { BF_OVERLAY_DATA, BfOverlayRef } from '@bitforgehq/ui/overlay';
100
+ import { X } from 'lucide';
101
+
102
+ /** A task as the modal edits it. */
103
+ export interface Task {
104
+ id: string;
105
+ title: string;
106
+ }
107
+
108
+ /** The modal: its own header with a close button and Save, then the form. */
109
+ @Component({
110
+ selector: 'app-edit-task',
111
+ imports: [
112
+ BfHeaderComponent,
113
+ BfContentComponent,
114
+ BfIconButtonComponent,
115
+ BfIconComponent,
116
+ BfButtonComponent,
117
+ BfInputComponent,
118
+ ],
119
+ template: `
120
+ <bf-header title="Edit task" [largeTitle]="false">
121
+ <bf-icon-button bfSlot="start" variant="text" label="Close" (clicked)="close()">
122
+ <bf-icon [icon]="xIcon" />
123
+ </bf-icon-button>
124
+ <bf-button bfSlot="end" variant="text" (clicked)="save()">Save</bf-button>
125
+ </bf-header>
126
+ <bf-content class="px-4">
127
+ <bf-input label="Title" [(value)]="title" />
128
+ </bf-content>
129
+ `,
130
+ })
131
+ export class EditTaskComponent {
132
+ private readonly data = inject<{ taskId: string }>(BF_OVERLAY_DATA);
133
+ private readonly ref = inject<BfOverlayRef<Task>>(BfOverlayRef);
134
+ protected readonly xIcon = X;
135
+ protected readonly title = signal('Buy milk');
136
+
137
+ close(): void {
138
+ this.ref.close();
139
+ }
140
+
141
+ save(): void {
142
+ this.ref.close({ id: this.data.taskId, title: this.title() });
143
+ }
144
+ }
145
+
146
+ /**
147
+ * A modal that edits a task.
148
+ *
149
+ * `open()` passes the task id as data and awaits the saved task, or `undefined` when closed
150
+ * without a result.
151
+ *
152
+ * @preview none
153
+ */
154
+ @Component({
155
+ selector: 'app-example-modal-basic',
156
+ imports: [BfButtonComponent],
157
+ template: `
158
+ <div class="flex flex-col items-start gap-3">
159
+ <bf-button (clicked)="edit('task-1')">Edit task</bf-button>
160
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
161
+ </div>
162
+ `,
163
+ })
164
+ export class ModalBasicExampleComponent {
165
+ private readonly modals = inject(BfModalService);
166
+ protected readonly result = signal('');
167
+
168
+ protected async edit(taskId: string): Promise<void> {
169
+ const ref = this.modals.open<EditTaskComponent, Task>(EditTaskComponent, {
170
+ data: { taskId },
171
+ });
172
+ const saved = await ref.afterClosed; // undefined when closed without a result
173
+ this.result.set(saved ? `Saved "${saved.title}".` : 'Closed.');
174
+ }
175
+ }
176
+ ```
177
+
178
+ ## Presentations
179
+
180
+ `presentation: 'sheet'` (default) is a card whose top edge sits 10 px below the top safe area
181
+ (`top-[calc(var(--spacing-safe-top)+10px)]`, UIKit's page-sheet inset, so the card starts below
182
+ the status bar and the parent page peeks above it; `bg-bg rounded-t-card shadow-floating`) over
183
+ the parked page stack; in iOS mode opening it scales the page stack down and
184
+ rounds its corners behind the card (Material mode leaves the page stack full size) (driven by `--bf-modal-depth`, written by `BfOverlayStack` on every
185
+ registered `bf-stack-outlet` root, consumed by the outlet's own host styles). `presentation:
186
+ 'fullscreen'` covers the viewport edge to edge and leaves the page stack full size — use it for
187
+ forms and flows that need the whole screen.
188
+
189
+ The sheet card already sits below the status bar, so it sets its `BfPageContext`'s `safeAreaTop`
190
+ to false: the content's `bf-header` (and a `fullscreen` `bf-content`) adds no status-bar band of
191
+ its own inside the card. A fullscreen modal reaches the top of the screen and keeps `safeAreaTop`
192
+ true, like a page.
193
+
194
+ A `bf-page` inside a sheet modal's content (for example a stack of its own) is not supported: it
195
+ provides a fresh `BfPageContext` with `safeAreaTop` true, so its header would add the status-bar
196
+ band back inside the card. Put the `bf-header` and `bf-content` directly in the modal's content
197
+ component, or use the `fullscreen` presentation for a flow that needs its own pages.
198
+
199
+ ```ts
200
+ import { Component, inject } from '@angular/core';
201
+ import { BfButtonComponent } from '@bitforgehq/ui';
202
+ import { BfModalService } from '@bitforgehq/ui/mobile';
203
+ import { BfOverlayRef } from '@bitforgehq/ui/overlay';
204
+
205
+ /** Onboarding that fills the screen. */
206
+ @Component({
207
+ selector: 'app-onboarding',
208
+ imports: [BfButtonComponent],
209
+ template: `
210
+ <div class="flex flex-col gap-3 p-4">
211
+ <p class="text-body">Welcome! Three steps set up your workspace.</p>
212
+ <bf-button (clicked)="ref.close()">Done</bf-button>
213
+ </div>
214
+ `,
215
+ })
216
+ export class OnboardingComponent {
217
+ protected readonly ref = inject(BfOverlayRef);
218
+ }
219
+
220
+ /**
221
+ * A full-screen modal.
222
+ *
223
+ * `presentation: 'fullscreen'` covers the whole screen instead of the card.
224
+ *
225
+ * @preview none
226
+ */
227
+ @Component({
228
+ selector: 'app-example-modal-presentations',
229
+ imports: [BfButtonComponent],
230
+ template: `<bf-button (clicked)="open()">Start onboarding</bf-button>`,
231
+ })
232
+ export class ModalPresentationsExampleComponent {
233
+ private readonly modals = inject(BfModalService);
234
+
235
+ protected open(): void {
236
+ this.modals.open(OnboardingComponent, { presentation: 'fullscreen' });
237
+ }
238
+ }
239
+ ```
240
+
241
+ In iOS mode, stacking two sheet modals scales the page stack twice as much (`--bf-modal-depth: 2`); a
242
+ fullscreen modal never adds to that depth.
243
+
244
+ iOS mode slides the card (or frame) up from the bottom and back down on close; Material mode fades
245
+ it up 16 px and back down.
246
+
247
+ ## Dismissible
248
+
249
+ By default (`dismissible: true`) `Escape` and the hardware back button close the modal in addition
250
+ to the content's own close control. With `dismissible: false` only the content closes it: `Escape`
251
+ is ignored and the back button is swallowed while the modal is open instead of falling through to
252
+ the page behind (like a non-dismissible `bf-sheet`). There is no backdrop dismiss for either case —
253
+ give the content an explicit close control.
254
+
255
+ ```ts
256
+ import { Component, inject } from '@angular/core';
257
+ import { BfButtonComponent } from '@bitforgehq/ui';
258
+ import { BfModalService } from '@bitforgehq/ui/mobile';
259
+ import { BfOverlayRef } from '@bitforgehq/ui/overlay';
260
+
261
+ /** Terms that must be accepted. */
262
+ @Component({
263
+ selector: 'app-terms',
264
+ imports: [BfButtonComponent],
265
+ template: `
266
+ <div class="flex flex-col gap-3 p-4">
267
+ <p class="text-body">Accept the updated terms to continue.</p>
268
+ <bf-button (clicked)="ref.close()">Done</bf-button>
269
+ </div>
270
+ `,
271
+ })
272
+ export class TermsComponent {
273
+ protected readonly ref = inject(BfOverlayRef);
274
+ }
275
+
276
+ /**
277
+ * A modal that must be answered.
278
+ *
279
+ * `dismissible: false` turns off the swipe down and the back button; only its own button closes it.
280
+ *
281
+ * @preview none
282
+ */
283
+ @Component({
284
+ selector: 'app-example-modal-dismissible',
285
+ imports: [BfButtonComponent],
286
+ template: `<bf-button (clicked)="open()">Show the terms</bf-button>`,
287
+ })
288
+ export class ModalDismissibleExampleComponent {
289
+ private readonly modals = inject(BfModalService);
290
+
291
+ protected open(): void {
292
+ this.modals.open(TermsComponent, { dismissible: false });
293
+ }
294
+ }
295
+ ```
296
+
297
+ ## Navigation
298
+
299
+ A navigation to another URL path closes the modal, resolving `afterClosed` with `undefined`, also
300
+ a `dismissible: false` one. A query- or fragment-only change leaves it open; a matrix-parameter
301
+ change (`/list;sort=name`) is a path change and closes it. In the `/mobile` shell every push,
302
+ every pop (the header back button, swipe-back, `BfNavController.back()`) and every tab switch
303
+ changes the path, so a modal opened on a page closes when that page is left. The Android hardware
304
+ back button with a modal open closes the modal only (a `dismissible: false` modal swallows the
305
+ press); its handler runs at priority 100, above the page pop, so one press never closes it and
306
+ pops the page as well. Pass `closeOnNavigation: false` for a modal that should stay over the next
307
+ page.
308
+
309
+ ```ts
310
+ import { Component, inject } from '@angular/core';
311
+ import { BfButtonComponent } from '@bitforgehq/ui';
312
+ import { BfModalService } from '@bitforgehq/ui/mobile';
313
+ import { BfOverlayRef } from '@bitforgehq/ui/overlay';
314
+
315
+ /** A message being written. */
316
+ @Component({
317
+ selector: 'app-compose',
318
+ imports: [BfButtonComponent],
319
+ template: `
320
+ <div class="flex flex-col gap-3 p-4">
321
+ <p class="text-body">Your draft stays while you look something up.</p>
322
+ <bf-button (clicked)="ref.close()">Done</bf-button>
323
+ </div>
324
+ `,
325
+ })
326
+ export class ComposeComponent {
327
+ protected readonly ref = inject(BfOverlayRef);
328
+ }
329
+
330
+ /**
331
+ * A modal that stays open across navigations.
332
+ *
333
+ * `closeOnNavigation: false` keeps a compose modal open while the page behind it changes.
334
+ *
335
+ * @preview none
336
+ */
337
+ @Component({
338
+ selector: 'app-example-modal-navigation',
339
+ imports: [BfButtonComponent],
340
+ template: `<bf-button (clicked)="open()">Compose</bf-button>`,
341
+ })
342
+ export class ModalNavigationExampleComponent {
343
+ private readonly modals = inject(BfModalService);
344
+
345
+ protected open(): void {
346
+ this.modals.open(ComposeComponent, { closeOnNavigation: false });
347
+ }
348
+ }
349
+ ```
350
+
351
+ A modal opened before the first navigation ends (an onboarding modal from the root component or
352
+ an `APP_INITIALIZER`) closes when the start URL redirects (`''` to `'today'`): open it after the
353
+ first `NavigationEnd`, or pass `closeOnNavigation: false`. A `history.pushState()` made outside
354
+ Angular's `Location` is not seen.
355
+
356
+ A route `canDeactivate` guard protects a form modal's unsaved input only against in-app
357
+ navigations: the modal stays open while the guard's confirm asks. On the web the browser's back
358
+ button changes the URL before the guard runs, so the modal has already closed when "Stay" is
359
+ answered (the Android hardware back button never gets that far: the modal takes it first). A
360
+ modal that must keep its draft passes `closeOnNavigation: false` or saves the draft as it
361
+ changes.
362
+
363
+ ## Soft keyboard
364
+
365
+ A sheet modal ends above the soft keyboard. Where the keyboard overlays the page (iOS with
366
+ `Keyboard.resize: 'none'`, a mobile browser), `bf-modal` pads its bottom by the covered height
367
+ while the keyboard is up, so the content's `bf-content` ends at the keyboard's top edge and its
368
+ keyboard assist scrolls the focused field 24 px clear of it. Hiding the keyboard removes the
369
+ padding. On Android the WebView itself shrinks, so nothing is added; a fullscreen modal leaves the
370
+ keyboard to its `bf-content`, as a page does. Put the form in the content's `bf-content` and its
371
+ primary action in the header (`end` slot), where the keyboard never reaches it.
372
+
373
+ ## Labels and the injector
374
+
375
+ Default strings come from `provideBfLabels()` (`modal`, "Modal"). The service is a root singleton, so it reads
376
+ the app-level labels; to use a feature's own `provideBfLabels()` (in a component's `providers`),
377
+ pass that component's injector. It then also becomes the parent of the modal's content injector,
378
+ so the content resolves the feature's providers.
379
+
380
+ ```ts
381
+ import { Component, Injector, inject } from '@angular/core';
382
+ import { BfButtonComponent, provideBfLabels } from '@bitforgehq/ui';
383
+ import { BfModalService } from '@bitforgehq/ui/mobile';
384
+ import { BfOverlayRef } from '@bitforgehq/ui/overlay';
385
+
386
+ /** A note in the feature's language. */
387
+ @Component({
388
+ selector: 'app-note',
389
+ imports: [BfButtonComponent],
390
+ template: `
391
+ <div class="flex flex-col gap-3 p-4">
392
+ <p class="text-body">Notiz bearbeiten</p>
393
+ <bf-button (clicked)="ref.close()">Fertig</bf-button>
394
+ </div>
395
+ `,
396
+ })
397
+ export class NoteComponent {
398
+ protected readonly ref = inject(BfOverlayRef);
399
+ }
400
+
401
+ /**
402
+ * A modal that reads a feature's labels.
403
+ *
404
+ * This feature provides German labels; passing the opener's injector is what names the modal
405
+ * "Fenster" for screen readers (its `modal` label) and lets its content read the same labels.
406
+ *
407
+ * @preview none
408
+ */
409
+ @Component({
410
+ selector: 'app-example-modal-injector',
411
+ imports: [BfButtonComponent],
412
+ providers: [provideBfLabels({ modal: 'Fenster' })],
413
+ template: `<bf-button (clicked)="open()">Notiz bearbeiten</bf-button>`,
414
+ })
415
+ export class ModalInjectorExampleComponent {
416
+ private readonly modals = inject(BfModalService);
417
+ private readonly injector = inject(Injector);
418
+
419
+ protected open(): void {
420
+ this.modals.open(NoteComponent, { injector: this.injector });
421
+ }
422
+ }
423
+ ```
424
+
425
+ ## Desktop
426
+
427
+ `BfModalService`'s modal is phone-shaped and meant for the Capacitor shell (`provideBfShell()`). On
428
+ a desktop web app, use the desktop overlays from `@bitforgehq/ui/overlay` instead: they need only
429
+ `@angular/cdk`, no Capacitor and no router. A desktop dialog locks the page's scroll, makes the page
430
+ behind it `inert`, and traps and restores focus.
431
+
432
+ `BfDialogService.open()` renders the same kind of content (your component, with a typed ref and
433
+ `BF_DIALOG_DATA`) centred over a backdrop; lay it out with `bf-dialog`.
434
+
435
+ ```ts
436
+ import { Component, inject, signal } from '@angular/core';
437
+ import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
438
+ import {
439
+ BF_DIALOG_DATA,
440
+ BfDialogComponent,
441
+ BfDialogRef,
442
+ BfDialogService,
443
+ } from '@bitforgehq/ui/overlay';
444
+
445
+ /** The dialog: a `bf-dialog` that closes itself with the new name. */
446
+ @Component({
447
+ selector: 'app-rename-dialog',
448
+ imports: [BfDialogComponent, BfButtonComponent, BfInputComponent],
449
+ template: `
450
+ <bf-dialog title="Rename project">
451
+ <bf-input label="Name" [(value)]="name" />
452
+ <div bfSlot="footer">
453
+ <bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
454
+ <bf-button (clicked)="ref.close(name())">Save</bf-button>
455
+ </div>
456
+ </bf-dialog>
457
+ `,
458
+ })
459
+ export class RenameDialogComponent {
460
+ protected readonly ref = inject<BfDialogRef<string>>(BfDialogRef);
461
+ protected readonly name = signal(inject<{ name: string }>(BF_DIALOG_DATA).name);
462
+ }
463
+
464
+ /**
465
+ * A dialog that renames a project.
466
+ *
467
+ * `open()` passes the current name as data and awaits the result: the new name, or `undefined`
468
+ * when the dialog is dismissed.
469
+ */
470
+ @Component({
471
+ selector: 'app-example-dialog-basic',
472
+ imports: [BfButtonComponent],
473
+ template: `
474
+ <div class="flex flex-col items-start gap-3">
475
+ <p class="text-heading">{{ project().name }}</p>
476
+ <bf-button (clicked)="rename()">Rename…</bf-button>
477
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
478
+ </div>
479
+ `,
480
+ })
481
+ export class DialogBasicExampleComponent {
482
+ private readonly dialogs = inject(BfDialogService);
483
+ protected readonly project = signal({ name: 'Atlas rebrand' });
484
+ protected readonly result = signal('');
485
+
486
+ protected async rename(): Promise<void> {
487
+ const ref = this.dialogs.open<RenameDialogComponent, { name: string }, string>(
488
+ RenameDialogComponent,
489
+ { data: { name: this.project().name } },
490
+ );
491
+ const newName = await ref.afterClosed; // undefined when dismissed
492
+ if (newName) {
493
+ this.project.set({ name: newName });
494
+ }
495
+ this.result.set(newName ? `Renamed to ${newName}.` : 'Not renamed.');
496
+ }
497
+ }
498
+ ```
499
+
500
+ ## Theming
501
+
502
+ - Sheet card: `bg-bg`, `rounded-t-card` (`--radius-card`), `shadow-floating`; its top at
503
+ `--spacing-safe-top` plus 10 px.
504
+ - Fullscreen frame: `bg-bg`, no radius or shadow.
505
+ - Page stack behind a sheet modal (iOS mode): scaled and rounded through `--bf-modal-depth` on the
506
+ `bf-stack-outlet` host — do not set that variable yourself.
507
+ - Keyboard: an inline `padding-bottom` on the sheet card while the keyboard overlays it — do not
508
+ set your own inline bottom padding on `bf-modal`.
509
+
510
+ ## Accessibility
511
+
512
+ The overlay panel is a `role="dialog"` with `aria-modal="true"` and a name: the
513
+ `ariaLabel` option when given; else the first heading in the content that is not `aria-hidden`
514
+ (`h1`–`h6` or `role="heading"`, such as the `bf-header` title), found once the content has
515
+ rendered (or when it first appears later, e.g. behind `@if`) and referenced through `aria-labelledby` (it gets an id `bf-overlay-title-<n>` when it has
516
+ none); else `provideBfLabels()`'s `modal` ("Modal"). The page stack behind it is `inert`. Focus moves to the first focusable element of the
517
+ content (or the panel), unlike a sheet, which focuses its panel; Tab is trapped inside, and focus
518
+ returns to the element that opened the modal on close. The content's `bf-content` is no `main`
519
+ landmark (the modal's `BfPageContext.mainLandmark` is false): content in a dialog is not the
520
+ page's main content, and the page behind keeps its `main`, `inert` while the modal is open.
521
+ `Escape`, the hardware back button and, in a browser with `CloseWatcher`, Android back close a
522
+ dismissible modal. Under
523
+ `prefers-reduced-motion` the modal cross-fades in and out instead of sliding or rising, and the
524
+ page stack behind a sheet modal skips the scale/rounding transition.
525
+
526
+ ### Keyboard
527
+
528
+ | Key | Action |
529
+ | ------------------------------------------------ | ------------------------------------------------------ |
530
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves between the modal's controls, wrapping inside it |
531
+ | <kbd>Escape</kbd> | Closes a dismissible modal |
532
+
533
+ ## Notes
534
+
535
+ ### Do
536
+
537
+ - Give the content its own `bf-header` with a close control (`bf-icon-button` in `start`) —
538
+ `bf-modal` never adds one for you.
539
+ - Use `presentation: 'sheet'` for editors and detail forms opened from a page; reserve
540
+ `'fullscreen'` for onboarding and other flows that need the whole screen.
541
+ - Close with a result through the injected `BfOverlayRef` and await `afterClosed` in the caller.
542
+
543
+ ### Don't
544
+
545
+ - Don't rely on a backdrop tap or a drag to close the modal — there is neither; always give the
546
+ content a close control.
547
+ - Don't use `dismissible: false` without a way out in the content itself (a close or cancel
548
+ control) — the user would otherwise be stuck.
549
+ - Don't nest a `bf-modal` inside another modal's content by hand; open the second one through
550
+ `BfModalService` so it stacks and the depth scale is correct.
551
+ - Don't wrap a sheet modal's content in a `bf-page`: it brings the status-bar band back (see
552
+ Presentations).
553
+
554
+ ## Migration notes
555
+
556
+ ### 0.3.0
557
+
558
+ **Behaviour:** a navigation to another URL path closes it with no result, also a
559
+ `dismissible: false` one; new `closeOnNavigation` option (default true). Migration: pass
560
+ `closeOnNavigation: false` to keep it open (see Migration 19).
561
+
562
+ **Behaviour:** named by the first heading: without `ariaLabel`, the panel is named by the first
563
+ heading in its content that is not `aria-hidden` (such as the `bf-header` title) through
564
+ `aria-labelledby`, once the content has rendered; without a heading, by `provideBfLabels()`'s
565
+ `modal` ("Modal"). An explicit `ariaLabel` always wins.
566
+
567
+ New `injector` option: labels resolve from it per call (default the root injector) and it becomes
568
+ the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
569
+ other providers reach the modal.
570
+
571
+ A heading that renders after the overlay opened (behind `@if`, a `resource()`) still names it: the
572
+ panel is watched until its first heading appears.
573
+
574
+ JSDoc of `modal.component.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
575
+ behaviour change.
576
+
577
+ JSDoc of `modal.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
578
+ `@default` on the option properties the code applies; `@param` docs. No behaviour change.
579
+
580
+ **Behaviour:** the page-sheet card (`presentation: 'sheet'`) starts 10 px below the top safe
581
+ area (`top-[calc(var(--spacing-safe-top)+10px)]`, UIKit's page-sheet inset; was `top-3`), so it
582
+ sits below the status bar and the parent page peeks above it, and it sets its `BfPageContext`'s
583
+ `safeAreaTop` to false, so its content's header adds no second status-bar band (Migration 29). A
584
+ fullscreen modal is unchanged.
585
+
586
+ **Behaviour:** its content is no `main` landmark: `bf-modal` sets its `BfPageContext.mainLandmark`
587
+ to false (content in a dialog is not the page's main content; the page behind stays `main`, inert
588
+ while the modal is open). Android web back closes it where `CloseWatcher` exists (Migration 35).
589
+
590
+ ### 0.2.0
591
+
592
+ New `BfModalService.open(component, { data, presentation, dismissible, ariaLabel })` in
593
+ `@bitforgehq/ui/mobile`, a page-level modal on the overlay core (`position: 'fullscreen'`, back
594
+ priority 100, no backdrop dismiss). `presentation: 'sheet'` (default) renders `bf-modal` as a card
595
+ inset 12 px from the top (`bg-bg rounded-t-card shadow-floating`) and, in iOS mode only, counts as
596
+ a modal, driving `--bf-modal-depth` on every registered `bf-stack-outlet` root (see
597
+ `### stack-outlet`) so the page behind scales down (Material mode leaves it unscaled);
598
+ `'fullscreen'` renders it edge to edge (`bg-bg`, no radius or shadow) and does not count towards
599
+ that depth. iOS mode slides the frame up on enter and down on exit; Material mode fades it up 16 px
600
+ on enter and down on exit (new `fadeUp`/`fadeDown` helpers in `overlay-animations.ts`). It returns
601
+ the `BfOverlayRef<T>` the content also injects (with the caller's `data` as `BF_OVERLAY_DATA`) and
602
+ closes with a result through; unlike `bf-sheet` there is no drag-to-dismiss, so the content needs
603
+ its own close control. `dismissible: false` (default true) ignores `Escape` and swallows the
604
+ hardware back button while open instead of closing or falling through to the page behind. Like
605
+ `bf-page`, `bf-modal` provides a fresh `BfPageContext` so the content's own `bf-header`
606
+ (`largeTitle="false"`, typically a close `bf-icon-button` in `start`) and `bf-content` work
607
+ inside it. `provideBfShell()` now also provides `BfSheetService` and `BfModalService`.
608
+
609
+ Fixed (behaviour change): a `presentation: 'sheet'` modal ends above the soft keyboard. While the
610
+ keyboard overlays the card's bottom edge (iOS, a mobile browser), `bf-modal` gets an inline
611
+ `padding-bottom` of the covered height, so the content's `bf-content` ends at the keyboard's top
612
+ edge and its keyboard assist reveals the focused control there; it goes away with the keyboard. A
613
+ `fullscreen` modal, and Android, where the WebView resizes, are unchanged.