@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,962 @@
1
+ # bf-dialog
2
+
3
+ Desktop modal dialog with a title, scrolling body, footer actions and focus trap
4
+
5
+ `BfDialogComponent` from `@bitforgehq/ui/overlay` · Overlays · since 0.3.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, inject, signal } from '@angular/core';
11
+ import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
12
+ import {
13
+ BF_DIALOG_DATA,
14
+ BfDialogComponent,
15
+ BfDialogRef,
16
+ BfDialogService,
17
+ BfMenuButtonComponent,
18
+ BfMenuComponent,
19
+ BfMenuItemDirective,
20
+ } from '@bitforgehq/ui/overlay';
21
+
22
+ /** The project a demo dialog edits. */
23
+ export interface DemoProject {
24
+ name: string;
25
+ owner: string;
26
+ }
27
+
28
+ /** The form dialog of the demo: two fields, Cancel and Save. */
29
+ @Component({
30
+ selector: 'app-dialog-demo-edit',
31
+ imports: [BfDialogComponent, BfButtonComponent, BfInputComponent],
32
+ template: `
33
+ <bf-dialog title="Edit project">
34
+ <bf-input label="Name" [(value)]="name" />
35
+ <bf-input label="Owner" [(value)]="owner" />
36
+ <div bfSlot="footer">
37
+ <bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
38
+ <bf-button (clicked)="save()">Save</bf-button>
39
+ </div>
40
+ </bf-dialog>
41
+ `,
42
+ })
43
+ export class DialogDemoEditComponent {
44
+ protected readonly ref = inject<BfDialogRef<DemoProject>>(BfDialogRef);
45
+ private readonly project = inject<DemoProject>(BF_DIALOG_DATA);
46
+
47
+ protected readonly name = signal(this.project.name);
48
+ protected readonly owner = signal(this.project.owner);
49
+
50
+ protected save(): void {
51
+ this.ref.close({ name: this.name(), owner: this.owner() });
52
+ }
53
+ }
54
+
55
+ /** The sections of the long dialog: more text than fits, so its body scrolls. */
56
+ const RELEASE_NOTES: readonly { title: string; body: string }[] = [
57
+ { title: 'Boards', body: 'Columns keep their width when you collapse the sidebar.' },
58
+ { title: 'Search', body: 'Results group by project, and the newest match comes first.' },
59
+ { title: 'Invoices', body: 'Drafts save every few seconds, so a closed tab loses nothing.' },
60
+ { title: 'Calendar', body: 'Week view starts on the day your team settings name.' },
61
+ { title: 'Files', body: 'Uploads resume after a lost connection instead of starting over.' },
62
+ { title: 'Comments', body: 'Mentions notify once, even when you edit the comment later.' },
63
+ { title: 'Reports', body: 'Exports keep the filters and the column order you chose.' },
64
+ { title: 'Tasks', body: 'Recurring tasks skip public holidays when you ask them to.' },
65
+ { title: 'Team', body: 'Guests see only the projects they were invited to.' },
66
+ { title: 'Settings', body: 'The theme follows your system until you pick one yourself.' },
67
+ ];
68
+
69
+ /** A dialog whose body is taller than the viewport allows: the body scrolls, the page does not. */
70
+ @Component({
71
+ selector: 'app-dialog-demo-notes',
72
+ imports: [BfDialogComponent, BfButtonComponent],
73
+ template: `
74
+ <bf-dialog title="Release notes">
75
+ @for (note of notes; track note.title) {
76
+ <section class="flex flex-col gap-1">
77
+ <h3 class="text-label font-bold">{{ note.title }}</h3>
78
+ <p class="text-body text-text-secondary">{{ note.body }}</p>
79
+ <p class="text-body text-text-secondary">
80
+ Nothing changes for your existing data, and you can switch it back in the settings.
81
+ </p>
82
+ </section>
83
+ }
84
+ <div bfSlot="footer">
85
+ <bf-button (clicked)="ref.close(true)">Got it</bf-button>
86
+ </div>
87
+ </bf-dialog>
88
+ `,
89
+ })
90
+ export class DialogDemoNotesComponent {
91
+ protected readonly ref = inject<BfDialogRef<boolean>>(BfDialogRef);
92
+ protected readonly notes = RELEASE_NOTES;
93
+ }
94
+
95
+ /** Who can open a shared project link. */
96
+ export type DemoAccess = 'Only the team' | 'Anyone with the link';
97
+
98
+ /**
99
+ * A dialog with a menu button in its footer: `Escape` in the open menu closes the menu only, and
100
+ * a second `Escape` closes the dialog.
101
+ */
102
+ @Component({
103
+ selector: 'app-dialog-demo-share',
104
+ imports: [
105
+ BfDialogComponent,
106
+ BfButtonComponent,
107
+ BfMenuButtonComponent,
108
+ BfMenuComponent,
109
+ BfMenuItemDirective,
110
+ ],
111
+ template: `
112
+ <bf-dialog title="Share project">
113
+ <p class="text-body text-text-secondary">Link access: {{ access() }}</p>
114
+ <div bfSlot="footer">
115
+ <bf-menu-button data-testid="dialog-share-access" [menu]="accessMenu">
116
+ Link access
117
+ </bf-menu-button>
118
+ <bf-button (clicked)="ref.close(access())">Done</bf-button>
119
+ </div>
120
+ </bf-dialog>
121
+
122
+ <ng-template #accessMenu>
123
+ <bf-menu>
124
+ <button bfMenuItem (triggered)="access.set('Only the team')">Only the team</button>
125
+ <button bfMenuItem (triggered)="access.set('Anyone with the link')">
126
+ Anyone with the link
127
+ </button>
128
+ </bf-menu>
129
+ </ng-template>
130
+ `,
131
+ })
132
+ export class DialogDemoShareComponent {
133
+ protected readonly ref = inject<BfDialogRef<DemoAccess>>(BfDialogRef);
134
+ protected readonly access = signal<DemoAccess>('Only the team');
135
+ }
136
+
137
+ /**
138
+ * The bf-dialog overview.
139
+ *
140
+ * Live demo of `bf-dialog` and `BfDialogService`, rendered in place: "Edit project" opens a form
141
+ * dialog, "Delete…" a `confirm()` alert dialog, "Release notes" a dialog whose body scrolls and
142
+ * "Share…" a dialog with a menu button in its footer; the result line announces each outcome
143
+ * (the `confirm()` answer id included). The triggers and the result line carry `data-testid`s
144
+ * for the end-to-end suite.
145
+ */
146
+ @Component({
147
+ selector: 'app-example-dialog-overview',
148
+ imports: [BfButtonComponent],
149
+ template: `
150
+ <div class="flex flex-col gap-4">
151
+ <p class="text-heading">{{ project().name }}</p>
152
+ <p class="text-body text-text-secondary">Owner: {{ project().owner }}</p>
153
+ <div class="flex flex-wrap gap-2">
154
+ <bf-button data-testid="dialog-edit" (clicked)="edit()">Edit project</bf-button>
155
+ <bf-button data-testid="dialog-delete" variant="danger" (clicked)="remove()">
156
+ Delete…
157
+ </bf-button>
158
+ <bf-button data-testid="dialog-notes" variant="outline" (clicked)="readNotes()">
159
+ Release notes
160
+ </bf-button>
161
+ <bf-button data-testid="dialog-share" variant="tonal" (clicked)="share()">
162
+ Share…
163
+ </bf-button>
164
+ </div>
165
+ <p
166
+ data-status
167
+ data-testid="dialog-result"
168
+ class="text-caption text-text-muted"
169
+ aria-live="polite"
170
+ >
171
+ {{ result() }}
172
+ </p>
173
+ </div>
174
+ `,
175
+ })
176
+ export class DialogOverviewExampleComponent {
177
+ private readonly dialogs = inject(BfDialogService);
178
+
179
+ readonly project = signal<DemoProject>({ name: 'Atlas rebrand', owner: 'Mia Keller' });
180
+ readonly result = signal('');
181
+
182
+ async edit(): Promise<void> {
183
+ const ref = this.dialogs.open<DialogDemoEditComponent, DemoProject, DemoProject>(
184
+ DialogDemoEditComponent,
185
+ { data: this.project() },
186
+ );
187
+ const saved = await ref.afterClosed;
188
+ if (saved) {
189
+ this.project.set(saved);
190
+ this.result.set('Saved.');
191
+ } else {
192
+ this.result.set('Edit cancelled.');
193
+ }
194
+ }
195
+
196
+ async remove(): Promise<void> {
197
+ const answer = await this.dialogs.confirm({
198
+ title: `Delete ${this.project().name}?`,
199
+ message: 'Its tasks and files are deleted too. This cannot be undone.',
200
+ buttons: [
201
+ { id: 'cancel', label: 'Cancel', role: 'cancel' },
202
+ { id: 'delete', label: 'Delete', role: 'destructive' },
203
+ ],
204
+ });
205
+ this.result.set(
206
+ answer === 'delete'
207
+ ? 'Answer "delete": deleted (not really, this is a demo).'
208
+ : `Answer "${answer ?? 'none'}": kept.`,
209
+ );
210
+ }
211
+
212
+ async readNotes(): Promise<void> {
213
+ const read = await this.dialogs.open<DialogDemoNotesComponent, unknown, boolean>(
214
+ DialogDemoNotesComponent,
215
+ ).afterClosed;
216
+ this.result.set(read ? 'Release notes read.' : 'Release notes closed.');
217
+ }
218
+
219
+ async share(): Promise<void> {
220
+ const access = await this.dialogs.open<DialogDemoShareComponent, unknown, DemoAccess>(
221
+ DialogDemoShareComponent,
222
+ ).afterClosed;
223
+ this.result.set(access ? `Link access: ${access}.` : 'Sharing unchanged.');
224
+ }
225
+ }
226
+ ```
227
+
228
+ ## Overview
229
+
230
+ Layout of a modal dialog opened with `BfDialogService`: a title row, a scrolling body and a
231
+ footer of actions. The panel is labelled by the title, and the body becomes focusable only
232
+ while its content overflows, so keyboard users can scroll it (a region named by the title when
233
+ there is one).
234
+
235
+ A modal dialog for desktop and web apps, from `@bitforgehq/ui/overlay` (needs `@angular/cdk`, no
236
+ Capacitor). `BfDialogService.open()` renders your component or template centred over a backdrop
237
+ and returns a typed `BfDialogRef`; `bf-dialog` inside the content lays out the title, the
238
+ scrolling body and the footer actions. `confirm()` opens an alert dialog in one call.
239
+
240
+ ## When to use
241
+
242
+ Use `BfDialogService` for a task or a decision that needs the user's full attention before the page goes on: a form, a detail view, a destructive confirmation (`confirm()`). Use [bf-menu](https://bitforge-ui.web.app/components/menu) instead for a short list of actions, [BfDesktopToastService](https://bitforge-ui.web.app/api/bf-desktop-toast-service) for a status that needs no answer, and [bf-alert](https://bitforge-ui.web.app/components/alert) for information that fits in the page. In the Capacitor shell, use [BfModalService](https://bitforge-ui.web.app/api/bf-modal-service) or [BfAlertDialogService](https://bitforge-ui.web.app/api/bf-alert-dialog-service) instead.
243
+
244
+ ## Basic usage
245
+
246
+ Write the dialog as a component whose template is a `bf-dialog`. It injects `BfDialogRef` to
247
+ close itself and `BF_DIALOG_DATA` for its input. Put the actions in `bfSlot="footer"`.
248
+
249
+ ```ts
250
+ import { Component, inject, signal } from '@angular/core';
251
+ import { BfButtonComponent, BfInputComponent } from '@bitforgehq/ui';
252
+ import {
253
+ BF_DIALOG_DATA,
254
+ BfDialogComponent,
255
+ BfDialogRef,
256
+ BfDialogService,
257
+ } from '@bitforgehq/ui/overlay';
258
+
259
+ /** The dialog: a `bf-dialog` that closes itself with the new name. */
260
+ @Component({
261
+ selector: 'app-rename-dialog',
262
+ imports: [BfDialogComponent, BfButtonComponent, BfInputComponent],
263
+ template: `
264
+ <bf-dialog title="Rename project">
265
+ <bf-input label="Name" [(value)]="name" />
266
+ <div bfSlot="footer">
267
+ <bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
268
+ <bf-button (clicked)="ref.close(name())">Save</bf-button>
269
+ </div>
270
+ </bf-dialog>
271
+ `,
272
+ })
273
+ export class RenameDialogComponent {
274
+ protected readonly ref = inject<BfDialogRef<string>>(BfDialogRef);
275
+ protected readonly name = signal(inject<{ name: string }>(BF_DIALOG_DATA).name);
276
+ }
277
+
278
+ /**
279
+ * A dialog that renames a project.
280
+ *
281
+ * `open()` passes the current name as data and awaits the result: the new name, or `undefined`
282
+ * when the dialog is dismissed.
283
+ */
284
+ @Component({
285
+ selector: 'app-example-dialog-basic',
286
+ imports: [BfButtonComponent],
287
+ template: `
288
+ <div class="flex flex-col items-start gap-3">
289
+ <p class="text-heading">{{ project().name }}</p>
290
+ <bf-button (clicked)="rename()">Rename…</bf-button>
291
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
292
+ </div>
293
+ `,
294
+ })
295
+ export class DialogBasicExampleComponent {
296
+ private readonly dialogs = inject(BfDialogService);
297
+ protected readonly project = signal({ name: 'Atlas rebrand' });
298
+ protected readonly result = signal('');
299
+
300
+ protected async rename(): Promise<void> {
301
+ const ref = this.dialogs.open<RenameDialogComponent, { name: string }, string>(
302
+ RenameDialogComponent,
303
+ { data: { name: this.project().name } },
304
+ );
305
+ const newName = await ref.afterClosed; // undefined when dismissed
306
+ if (newName) {
307
+ this.project.set({ name: newName });
308
+ }
309
+ this.result.set(newName ? `Renamed to ${newName}.` : 'Not renamed.');
310
+ }
311
+ }
312
+ ```
313
+
314
+ ## Sizes
315
+
316
+ `size` sets the dialog's width on its own scale (not `BfSize`, which is a control-height
317
+ scale): `sm` 24 rem, `md` 32 rem (default), `lg` 48 rem, `xl` 64 rem. Each is capped at the
318
+ viewport width minus the gutter on both sides, and the height at the viewport height minus the
319
+ gutters; the body scrolls when the content is taller. `fullscreen` fills the viewport without a
320
+ card radius.
321
+
322
+ ```ts
323
+ import { Component, inject } from '@angular/core';
324
+ import { BfButtonComponent } from '@bitforgehq/ui';
325
+ import { BfDialogComponent, BfDialogRef, BfDialogService } from '@bitforgehq/ui/overlay';
326
+
327
+ /** A wide report. */
328
+ @Component({
329
+ selector: 'app-report-dialog',
330
+ imports: [BfDialogComponent, BfButtonComponent],
331
+ template: `
332
+ <bf-dialog title="Quarterly report">
333
+ <p>Revenue grew in every region; the details follow in the full report.</p>
334
+ <div bfSlot="footer"><bf-button (clicked)="ref.close()">Close</bf-button></div>
335
+ </bf-dialog>
336
+ `,
337
+ })
338
+ export class ReportDialogComponent {
339
+ protected readonly ref = inject(BfDialogRef);
340
+ }
341
+
342
+ /** A full-screen editor. */
343
+ @Component({
344
+ selector: 'app-editor-dialog',
345
+ imports: [BfDialogComponent, BfButtonComponent],
346
+ template: `
347
+ <bf-dialog title="Edit the brief">
348
+ <p>A full-screen dialog fills the viewport, without a card radius.</p>
349
+ <div bfSlot="footer"><bf-button (clicked)="ref.close()">Done</bf-button></div>
350
+ </bf-dialog>
351
+ `,
352
+ })
353
+ export class EditorDialogComponent {
354
+ protected readonly ref = inject(BfDialogRef);
355
+ }
356
+
357
+ /** A large dialog and a full-screen dialog. */
358
+ @Component({
359
+ selector: 'app-example-dialog-sizes',
360
+ imports: [BfButtonComponent],
361
+ template: `
362
+ <div class="flex flex-wrap gap-2">
363
+ <bf-button variant="outline" (clicked)="dialogs.open(report, { size: 'lg' })"
364
+ >Large</bf-button
365
+ >
366
+ <bf-button variant="outline" (clicked)="dialogs.open(editor, { size: 'fullscreen' })"
367
+ >Full screen</bf-button
368
+ >
369
+ </div>
370
+ `,
371
+ })
372
+ export class DialogSizesExampleComponent {
373
+ protected readonly dialogs = inject(BfDialogService);
374
+ protected readonly report = ReportDialogComponent;
375
+ protected readonly editor = EditorDialogComponent;
376
+ }
377
+ ```
378
+
379
+ ## Header, body and footer
380
+
381
+ `title` renders as the `h2` the dialog is labelled by. Anything with `bfSlot="header"` sits
382
+ after the title in the header row (a status chip, a step count). The default slot is the body;
383
+ `bfSlot="footer"` is an end-aligned row of actions that wraps on narrow screens. The close button
384
+ (×) at the top end closes with no result; hide it with `[closeButton]="false"` when the footer
385
+ has a cancel action.
386
+
387
+ ```ts
388
+ import { Component, inject, signal } from '@angular/core';
389
+ import { BfBadgeComponent, BfButtonComponent } from '@bitforgehq/ui';
390
+ import { BfDialogComponent, BfDialogRef, BfDialogService } from '@bitforgehq/ui/overlay';
391
+
392
+ /** A dialog with a header badge, a body and footer actions, and no close button. */
393
+ @Component({
394
+ selector: 'app-invite-dialog',
395
+ imports: [BfBadgeComponent, BfButtonComponent, BfDialogComponent],
396
+ template: `
397
+ <bf-dialog title="Invite people" [closeButton]="false">
398
+ <bf-badge bfSlot="header">Step 2 of 3</bf-badge>
399
+ <p>Add the people who work on this project.</p>
400
+ <div bfSlot="footer">
401
+ <bf-button variant="outline" (clicked)="ref.close()">Back</bf-button>
402
+ <bf-button (clicked)="ref.close('next')">Next</bf-button>
403
+ </div>
404
+ </bf-dialog>
405
+ `,
406
+ })
407
+ export class InviteDialogComponent {
408
+ protected readonly ref = inject<BfDialogRef<string>>(BfDialogRef);
409
+ }
410
+
411
+ /**
412
+ * The header, body and footer of a dialog.
413
+ *
414
+ * The title and a step badge in the header, the body text, then Back and Next in the footer.
415
+ */
416
+ @Component({
417
+ selector: 'app-example-dialog-sections',
418
+ imports: [BfButtonComponent],
419
+ template: `
420
+ <div class="flex flex-col items-start gap-3">
421
+ <bf-button (clicked)="invite()">Invite people</bf-button>
422
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
423
+ </div>
424
+ `,
425
+ })
426
+ export class DialogSectionsExampleComponent {
427
+ private readonly dialogs = inject(BfDialogService);
428
+ protected readonly result = signal('');
429
+
430
+ protected async invite(): Promise<void> {
431
+ const step = await this.dialogs.open<InviteDialogComponent, unknown, string>(
432
+ InviteDialogComponent,
433
+ ).afterClosed;
434
+ this.result.set(step === 'next' ? 'On to step 3.' : 'Back to step 1.');
435
+ }
436
+ }
437
+ ```
438
+
439
+ ## Template dialogs
440
+
441
+ A template opens with the same options. Its context is `{ $implicit: ref, data }`.
442
+
443
+ ```ts
444
+ import { Component, type TemplateRef, inject, viewChild } from '@angular/core';
445
+ import { BfButtonComponent } from '@bitforgehq/ui';
446
+ import {
447
+ BfDialogComponent,
448
+ BfDialogService,
449
+ type BfDialogTemplateContext,
450
+ } from '@bitforgehq/ui/overlay';
451
+
452
+ /**
453
+ * A dialog from a template.
454
+ *
455
+ * The `ng-template` receives `{ $implicit: ref, data }`; `open()` takes the same options as for a
456
+ * component.
457
+ */
458
+ @Component({
459
+ selector: 'app-example-dialog-template',
460
+ imports: [BfButtonComponent, BfDialogComponent],
461
+ template: `
462
+ <bf-button variant="outline" (clicked)="open()">Keyboard shortcuts</bf-button>
463
+
464
+ <ng-template #shortcuts let-ref let-data="data">
465
+ <bf-dialog title="Keyboard shortcuts">
466
+ <p>{{ data.hint }}</p>
467
+ <div bfSlot="footer"><bf-button (clicked)="ref.close()">Got it</bf-button></div>
468
+ </bf-dialog>
469
+ </ng-template>
470
+ `,
471
+ })
472
+ export class DialogTemplateExampleComponent {
473
+ private readonly dialogs = inject(BfDialogService);
474
+ readonly shortcuts =
475
+ viewChild.required<TemplateRef<BfDialogTemplateContext<{ hint: string }>>>('shortcuts');
476
+
477
+ open(): void {
478
+ this.dialogs.open(this.shortcuts(), { size: 'sm', data: { hint: 'Press ? anywhere.' } });
479
+ }
480
+ }
481
+ ```
482
+
483
+ ## Confirmations
484
+
485
+ `confirm()` opens an alert dialog with a title, an optional message and a row of buttons, and
486
+ resolves with the pressed button's `id` (its `label` when it has no id). Buttons render in order,
487
+ right-aligned: a `cancel`-role button is `outline`, a `destructive` one `danger`, the first plain
488
+ one `primary`, further plain ones `tonal`. Without `buttons` it shows Cancel and OK from
489
+ `provideBfLabels()` (ids `cancel` and `ok`). `size` is `sm` (default) or `md`.
490
+
491
+ ```ts
492
+ import { Component, inject, signal } from '@angular/core';
493
+ import { BfButtonComponent } from '@bitforgehq/ui';
494
+ import { BfDialogService } from '@bitforgehq/ui/overlay';
495
+
496
+ /**
497
+ * A destructive confirmation.
498
+ *
499
+ * `confirm()` resolves with the pressed button's id: `delete`, `cancel`, or the cancel answer on
500
+ * `Escape`.
501
+ */
502
+ @Component({
503
+ selector: 'app-example-dialog-confirm',
504
+ imports: [BfButtonComponent],
505
+ template: `
506
+ <div class="flex flex-col items-start gap-3">
507
+ <bf-button variant="danger" (clicked)="remove()">Delete project…</bf-button>
508
+ <p aria-live="polite" class="text-caption text-text-muted">{{ result() }}</p>
509
+ </div>
510
+ `,
511
+ })
512
+ export class DialogConfirmExampleComponent {
513
+ private readonly dialogs = inject(BfDialogService);
514
+ protected readonly result = signal('');
515
+
516
+ protected async remove(): Promise<void> {
517
+ const answer = await this.dialogs.confirm({
518
+ title: 'Delete project?',
519
+ message: 'Its tasks and files are deleted too. This cannot be undone.',
520
+ buttons: [
521
+ { id: 'cancel', label: 'Cancel', role: 'cancel' },
522
+ { id: 'delete', label: 'Delete', role: 'destructive' },
523
+ ],
524
+ });
525
+ this.result.set(answer === 'delete' ? 'Deleted (not really).' : 'Kept.');
526
+ }
527
+ }
528
+ ```
529
+
530
+ ## Results
531
+
532
+ `BfDialogRef<R, C>` carries the result type `R` and the content type `C`. `close(result)`
533
+ resolves `afterClosed` with it; `Escape`, a backdrop click, the close button and a navigation
534
+ resolve `undefined`. `closed()` turns true once the dialog is gone, and `componentInstance` is the
535
+ content component (`null` for a template).
536
+
537
+ ```ts
538
+ import { Component, inject, signal } from '@angular/core';
539
+ import { BfButtonComponent, BfCheckboxComponent } from '@bitforgehq/ui';
540
+ import {
541
+ BF_DIALOG_DATA,
542
+ BfDialogComponent,
543
+ BfDialogRef,
544
+ BfDialogService,
545
+ } from '@bitforgehq/ui/overlay';
546
+
547
+ /** The filters a list shows. */
548
+ export interface Filters {
549
+ open: boolean;
550
+ mine: boolean;
551
+ }
552
+
553
+ /** A filter dialog that returns the edited filters and can highlight one of them. */
554
+ @Component({
555
+ selector: 'app-filter-dialog',
556
+ imports: [BfButtonComponent, BfCheckboxComponent, BfDialogComponent],
557
+ template: `
558
+ <bf-dialog title="Filters">
559
+ <div class="flex flex-col gap-2">
560
+ <bf-checkbox
561
+ label="Open tasks only"
562
+ [class]="highlighted() === 'status' ? 'bg-primary-soft rounded-field' : ''"
563
+ [(checked)]="open"
564
+ />
565
+ <bf-checkbox label="Assigned to me" [(checked)]="mine" />
566
+ </div>
567
+ <div bfSlot="footer">
568
+ <bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
569
+ <bf-button (clicked)="ref.close({ open: open(), mine: mine() })">Apply</bf-button>
570
+ </div>
571
+ </bf-dialog>
572
+ `,
573
+ })
574
+ export class FilterDialogComponent {
575
+ protected readonly ref = inject<BfDialogRef<Filters>>(BfDialogRef);
576
+ private readonly current = inject<Filters>(BF_DIALOG_DATA);
577
+ protected readonly open = signal(this.current.open);
578
+ protected readonly mine = signal(this.current.mine);
579
+ protected readonly highlighted = signal('');
580
+
581
+ highlight(field: string): void {
582
+ this.highlighted.set(field);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * A typed dialog result.
588
+ *
589
+ * `BfDialogRef<Filters, FilterDialogComponent>` types both the result and `componentInstance`;
590
+ * a dismissed dialog keeps the current filters.
591
+ */
592
+ @Component({
593
+ selector: 'app-example-dialog-results',
594
+ imports: [BfButtonComponent],
595
+ template: `
596
+ <div class="flex flex-col items-start gap-3">
597
+ <bf-button variant="outline" (clicked)="edit()">Filters…</bf-button>
598
+ <p class="text-caption text-text-muted">
599
+ {{ filters().open ? 'Open' : 'All' }} tasks · {{ filters().mine ? 'mine' : 'everyone' }}
600
+ </p>
601
+ </div>
602
+ `,
603
+ })
604
+ export class DialogResultsExampleComponent {
605
+ private readonly dialogs = inject(BfDialogService);
606
+ protected readonly filters = signal<Filters>({ open: true, mine: false });
607
+
608
+ protected async edit(): Promise<void> {
609
+ const current = this.filters();
610
+ const ref = this.dialogs.open<FilterDialogComponent, Filters, Filters>(FilterDialogComponent, {
611
+ data: current,
612
+ });
613
+ ref.componentInstance?.highlight('status');
614
+ this.filters.set((await ref.afterClosed) ?? current);
615
+ }
616
+ }
617
+ ```
618
+
619
+ ## Focus and dismissal
620
+
621
+ Focus moves to the element marked `autofocus` (or `cdkFocusInitial`) — itself when it is
622
+ focusable (a control, or an element with a `tabindex`, also `-1` for a region to start reading
623
+ at), else the first control inside it — else the first field or button in the body, never the ×,
624
+ which comes last in the tab order. `autoFocus: 'panel'` focuses the dialog itself, so a screen
625
+ reader starts at the top of a long text. `Escape` closes the dialog unless
626
+ `dismissOnEscape: false`; a backdrop click closes a `dialog` unless `dismissOnBackdrop: false`,
627
+ and never an `alertdialog`. In a browser with `CloseWatcher` (Chrome on Android, a PWA), Android
628
+ back closes the dialog the same way as `Escape` and leaves the URL alone; with
629
+ `dismissOnEscape: false` the dialog swallows it and stays open. A desktop browser's Back button is
630
+ no close request: it navigates, and the navigation closes the dialog. On close, focus returns to the element that opened it
631
+ (`restoreFocus: false` to skip); when that element is gone, to the topmost dialog still open,
632
+ else it stays on the page body. After a destructive `confirm()` that removes the opener (a
633
+ deleted row), move focus yourself to a logical place, such as the next row or the list's
634
+ heading. An `alertdialog` you open yourself (`open(…, { role: 'alertdialog' })`) needs
635
+ `ariaDescribedBy` pointing at its message; `confirm()` wires it for you. The service is a root
636
+ singleton: pass
637
+ `injector: inject(Injector)` so a feature-level `provideBfLabels()` names the close button
638
+ (`close`) and the fallback (`dialog`).
639
+
640
+ ```ts
641
+ import {
642
+ Component,
643
+ ElementRef,
644
+ Injector,
645
+ afterNextRender,
646
+ inject,
647
+ signal,
648
+ viewChildren,
649
+ } from '@angular/core';
650
+ import { BfButtonComponent } from '@bitforgehq/ui';
651
+ import { BfDialogComponent, BfDialogRef, BfDialogService } from '@bitforgehq/ui/overlay';
652
+
653
+ /** Focuses the native button inside a `bf-button` host (a type guard, not an assertion). */
654
+ function focusButtonIn(host: unknown): void {
655
+ if (host instanceof HTMLElement) {
656
+ host.querySelector('button')?.focus();
657
+ }
658
+ }
659
+
660
+ /** Terms that must be answered: an alert dialog described by its text. */
661
+ @Component({
662
+ selector: 'app-terms-dialog',
663
+ imports: [BfButtonComponent, BfDialogComponent],
664
+ template: `
665
+ <bf-dialog title="Updated terms" [closeButton]="false">
666
+ <p id="terms-text">We changed how long we keep deleted projects: 30 days instead of 7.</p>
667
+ <div bfSlot="footer"><bf-button (clicked)="ref.close(true)">Accept</bf-button></div>
668
+ </bf-dialog>
669
+ `,
670
+ })
671
+ export class TermsDialogComponent {
672
+ protected readonly ref = inject<BfDialogRef<boolean>>(BfDialogRef);
673
+ }
674
+
675
+ /**
676
+ * Initial focus, dismissal and focus after a deletion.
677
+ *
678
+ * The terms open as an `alertdialog` that starts reading at the panel and ignores `Escape`. Each
679
+ * row's Delete confirms first; the deleted row opened the dialog, so focus moves to the next row.
680
+ */
681
+ @Component({
682
+ selector: 'app-example-dialog-focus',
683
+ imports: [BfButtonComponent],
684
+ template: `
685
+ <div class="flex flex-col items-start gap-3">
686
+ <bf-button variant="outline" (clicked)="showTerms()">Show the terms</bf-button>
687
+ <ul class="flex flex-col gap-2 self-stretch">
688
+ @for (row of rows(); track row) {
689
+ <li class="flex items-center justify-between gap-3">
690
+ <span class="text-body">{{ row }}</span>
691
+ <bf-button #rowButton variant="text" size="sm" (clicked)="remove(row)"
692
+ >Delete {{ row }}</bf-button
693
+ >
694
+ </li>
695
+ }
696
+ </ul>
697
+ </div>
698
+ `,
699
+ })
700
+ export class DialogFocusExampleComponent {
701
+ private readonly dialogs = inject(BfDialogService);
702
+ private readonly injector = inject(Injector);
703
+ private readonly rowButtons = viewChildren('rowButton', {
704
+ read: ElementRef,
705
+ });
706
+ protected readonly rows = signal(['Brief', 'Moodboard', 'Deck']);
707
+
708
+ protected showTerms(): void {
709
+ this.dialogs.open(TermsDialogComponent, {
710
+ role: 'alertdialog',
711
+ ariaDescribedBy: 'terms-text',
712
+ autoFocus: 'panel',
713
+ dismissOnEscape: false,
714
+ injector: this.injector,
715
+ });
716
+ }
717
+
718
+ protected async remove(row: string): Promise<void> {
719
+ const answer = await this.dialogs.confirm({
720
+ title: `Delete ${row}?`,
721
+ buttons: [
722
+ { id: 'cancel', label: 'Cancel', role: 'cancel' },
723
+ { id: 'delete', label: 'Delete', role: 'destructive' },
724
+ ],
725
+ });
726
+ if (answer === 'delete') {
727
+ // After a destructive confirmation, focus the next row: the deleted one opened the dialog.
728
+ const next = this.rows().indexOf(row);
729
+ this.rows.update((rows) => rows.filter((r) => r !== row));
730
+ afterNextRender(
731
+ () => {
732
+ const buttons = this.rowButtons();
733
+ const target = buttons[next] ?? buttons.at(-1);
734
+ focusButtonIn(target?.nativeElement);
735
+ },
736
+ { injector: this.injector },
737
+ );
738
+ }
739
+ }
740
+ }
741
+ ```
742
+
743
+ ## Navigation
744
+
745
+ A navigation to another URL path closes the dialog: a router navigation (a link inside it, a
746
+ redirect after a save, a deep link) and the browser's back and forward buttons. `afterClosed`
747
+ resolves `undefined`; `confirm()` resolves the cancel button's answer, else `undefined`. A query-
748
+ or fragment-only change (a tab kept in `?tab=`) leaves it open. It listens through `Location` from
749
+ `@angular/common`, so it works with or without `@angular/router`. A `confirm()` opened from a
750
+ `canDeactivate` guard stays open: the path it records is the one the guard runs on. Focus
751
+ returns to the opener when it is still in the page, as on any close. Pass
752
+ `closeOnNavigation: false` for a dialog that outlives its page, such as one that walks through
753
+ several routes.
754
+
755
+ ```ts
756
+ import { Component, inject } from '@angular/core';
757
+ import { BfButtonComponent } from '@bitforgehq/ui';
758
+ import { BfDialogComponent, BfDialogRef, BfDialogService } from '@bitforgehq/ui/overlay';
759
+
760
+ /** A setup wizard that walks through several routes, so it outlives a navigation. */
761
+ @Component({
762
+ selector: 'app-setup-wizard',
763
+ imports: [BfButtonComponent, BfDialogComponent],
764
+ template: `
765
+ <bf-dialog title="Set up your workspace">
766
+ <p>This dialog stays open while the page behind it changes.</p>
767
+ <div bfSlot="footer"><bf-button (clicked)="ref.close()">Finish</bf-button></div>
768
+ </bf-dialog>
769
+ `,
770
+ })
771
+ export class SetupWizardComponent {
772
+ protected readonly ref = inject(BfDialogRef);
773
+ }
774
+
775
+ /** A dialog that stays open across navigations. */
776
+ @Component({
777
+ selector: 'app-example-dialog-navigation',
778
+ imports: [BfButtonComponent],
779
+ template: `<bf-button variant="outline" (clicked)="open()">Start the setup</bf-button>`,
780
+ })
781
+ export class DialogNavigationExampleComponent {
782
+ private readonly dialogs = inject(BfDialogService);
783
+
784
+ protected open(): void {
785
+ this.dialogs.open(SetupWizardComponent, { closeOnNavigation: false });
786
+ }
787
+ }
788
+ ```
789
+
790
+ A matrix-parameter change (`/projects;view=grid`) is a path change and closes it. A
791
+ `history.pushState()` made outside Angular's `Location` is not seen and does not. A dialog opened
792
+ before the first navigation ends (from the root component or an `APP_INITIALIZER`) closes when
793
+ the start URL redirects (`''` to `'projects'`): open it after the first `NavigationEnd`, or pass
794
+ `closeOnNavigation: false`.
795
+
796
+ A route `canDeactivate` guard protects a dialog's unsaved input only against in-app navigations:
797
+ the dialog stays open while the guard's `confirm()` asks. The browser's back and forward buttons
798
+ change the URL before the guard runs, so the dialog has already closed when "Stay" is answered. A
799
+ dialog that must keep its draft passes `closeOnNavigation: false` or saves the draft as it
800
+ changes.
801
+
802
+ ```ts
803
+ import { Component, inject, signal } from '@angular/core';
804
+ import { type CanDeactivateFn } from '@angular/router';
805
+ import { BfTextareaComponent } from '@bitforgehq/ui';
806
+ import { BfDialogService } from '@bitforgehq/ui/overlay';
807
+
808
+ /** Asks before the editor's unsaved changes are lost; `true` lets the navigation go on. */
809
+ export const unsavedChanges: CanDeactivateFn<DialogGuardExampleComponent> = async (page) =>
810
+ !page.dirty() ||
811
+ (await inject(BfDialogService).confirm({
812
+ title: 'Discard changes?',
813
+ buttons: [
814
+ { id: 'stay', label: 'Stay', role: 'cancel' },
815
+ { id: 'discard', label: 'Discard', role: 'destructive' },
816
+ ],
817
+ })) === 'discard';
818
+
819
+ /**
820
+ * A route guard that asks before unsaved changes are lost.
821
+ *
822
+ * `unsavedChanges` goes on the editor's route:
823
+ * `{ path: 'editor', component: DialogGuardExampleComponent, canDeactivate: [unsavedChanges] }`.
824
+ * The `confirm()` it opens stays open while the guard waits for the answer.
825
+ *
826
+ * @preview none
827
+ */
828
+ @Component({
829
+ selector: 'app-example-dialog-guard',
830
+ imports: [BfTextareaComponent],
831
+ template: `<bf-textarea label="Draft" [value]="draft()" (valueChange)="edit($event)" />`,
832
+ })
833
+ export class DialogGuardExampleComponent {
834
+ readonly draft = signal('');
835
+ readonly dirty = signal(false);
836
+
837
+ protected edit(text: string): void {
838
+ this.draft.set(text);
839
+ this.dirty.set(true);
840
+ }
841
+ }
842
+ ```
843
+
844
+ ## Desktop
845
+
846
+ Built for desktop: a centred card over a backdrop, at `sm` 24 rem to `xl` 64 rem wide, or `fullscreen`. By default a backdrop click closes a `dialog` and leaves an `alertdialog` open; `dismissOnBackdrop` overrides either. The page behind is inert and does not scroll, so the mouse wheel scrolls only the dialog body. The close button (×) is a `text` icon button: hover tints it, and its ring shows on keyboard focus only. The footer buttons follow `provideBfDefaults()` like any `bf-button`.
847
+
848
+ ## Theming
849
+
850
+ - Card: `bg-surface`, `text-text`, `rounded-card` (`--radius-card`), `shadow-floating`; widths
851
+ 24/32/48/64 rem capped at `100vw - 2 × --spacing-gutter`, height capped at
852
+ `100dvh - 2 × --spacing-gutter`. `fullscreen` has no radius or shadow.
853
+ - Title: `text-heading`; body padding `px-6 py-4`, header `px-6 pt-6`, footer `px-6 pb-6`
854
+ with `gap-2` between actions.
855
+ - Close button: a `text` `bf-icon-button` (`sm`) with a lucide `X`.
856
+ - Backdrop: `bf-overlay-backdrop` from the overlay core (a black scrim: 40 % in light mode, 60 %
857
+ in dark).
858
+ - Confirm message: `text-body text-text-secondary`.
859
+
860
+ ## Accessibility
861
+
862
+ The overlay panel is `role="dialog"` (or `alertdialog`) with `aria-modal="true"` and
863
+ a name: `ariaLabel` when given; else `aria-labelledby` pointing at the `bf-dialog` title; else
864
+ the first heading in the content; else the `dialog` label ("Dialog"). It never points
865
+ `aria-labelledby` at a missing element, and its ids come from the library's shared counter, so
866
+ they never repeat one in the page. `confirm()` is an `alertdialog` whose
867
+ `aria-describedby` points at the message, with initial focus on the cancel button (the least
868
+ destructive action), else the first button.
869
+
870
+ On open, focus moves into the dialog; Tab and Shift+Tab cycle inside it (the CDK focus trap), and
871
+ a press on the backdrop never moves it out (a `confirm()` stays open with focus on its button); on
872
+ close it returns to the opener (to the topmost dialog still open when the opener is gone). While it is open the page behind it is `inert` (every other child
873
+ of `<body>`; under `provideBfShell()` the page stack) and the document does not scroll. Nested
874
+ dialogs stack: `Escape` closes only the top one. When the body overflows, it becomes focusable
875
+ (`tabindex="0"`, a `role="region"` labelled by the title when there is one), so keyboard users
876
+ can scroll it with the arrow keys. Under
877
+ `prefers-reduced-motion` the dialog cross-fades instead of scaling.
878
+
879
+ ### Keyboard
880
+
881
+ | Key | Action |
882
+ | ------------------------------------------------ | ------------------------------------------------------------------------------ |
883
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Next / previous focusable element, wrapping inside the dialog |
884
+ | <kbd>Escape</kbd> | Closes the dialog (a confirmation: answers with its cancel button, or nothing) |
885
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Activates the focused button |
886
+
887
+ ## Inputs
888
+
889
+ | Name | Type | Default | Description |
890
+ | ------------- | --------- | ------- | ---------------------------------------------------------------------------------------- |
891
+ | `title` | `string` | `''` | Heading of the dialog, rendered as the `h2` the dialog panel is labelled by. |
892
+ | `closeButton` | `boolean` | `true` | Shows a close button (×) at the top end that closes the dialog with no result. |
893
+ | `closeLabel` | `string` | `''` | Accessible name of the close button; empty uses `provideBfLabels()`'s `close` ("Close"). |
894
+
895
+ ## Slots
896
+
897
+ - `header`: More header content after the title, such as a status badge.
898
+ - `default`: The dialog body.
899
+ - `footer`: The actions, aligned at the end.
900
+
901
+ ## Notes
902
+
903
+ ### Do
904
+
905
+ - Give every dialog a `title`, and word the primary action as the verb ("Save", "Delete").
906
+ - Put the cancel action first in the footer and the primary action last.
907
+ - Use `confirm()` for a yes/no decision and `role: 'destructive'` on the action that loses data.
908
+ - Pass `injector: inject(Injector)` when the opener sits under its own `provideBfLabels()`.
909
+
910
+ ### Don't
911
+
912
+ - Don't open a dialog for information that fits inline (a `bf-alert`) or a transient status (a
913
+ toast).
914
+ - Don't nest more than two dialogs; finish or close the first one instead.
915
+ - Don't make a form dialog an `alertdialog` just to block the backdrop; pass
916
+ `dismissOnBackdrop: false`.
917
+ - Don't use `/mobile` sheets or modals on desktop pages; they are phone presentations.
918
+
919
+ ## Migration notes
920
+
921
+ ### 0.3.0
922
+
923
+ New exported `BfConfirmSize` (`'sm' | 'md'`), the type of `BfConfirmOptions.size`,
924
+ `BfDialogRole` (`'dialog' | 'alertdialog'`), the type of `BfDialogConfig.role`, and
925
+ `BfConfirmButtonRole` (`'cancel' | 'destructive'`), the type of `BfConfirmButton.role`;
926
+ `BfDialogConfig` `autoFocus` takes the new `BfOverlayAutoFocus`.
927
+
928
+ New `bf-dialog` and `BfDialogService` in `@bitforgehq/ui/overlay`: a centred modal dialog for
929
+ desktop and web apps with no Capacitor dependency. `open(component | template, config)` returns
930
+ a typed `BfDialogRef<R, C>` (`afterClosed`, `closed()`, `componentInstance`, `close(result)`);
931
+ the content injects `BfDialogRef` and `BF_DIALOG_DATA`, a template gets `{ $implicit: ref, data }`.
932
+ `BfDialogConfig`: `data`, `size` (`sm` 24, `md` 32 (default), `lg` 48, `xl` 64 rem, capped at the
933
+ viewport minus the gutters, or `fullscreen`), `role` (`dialog`/`alertdialog`), `ariaLabel`,
934
+ `ariaDescribedBy`, `dismissOnBackdrop` (default true for `dialog`, false for `alertdialog`),
935
+ `dismissOnEscape`, `autoFocus`, `restoreFocus`, `panelClass`, `injector`. `bf-dialog` lays out a
936
+ `title` (the `h2` the panel is labelled by), `[bfSlot=header]`, a scrolling body (focusable only
937
+ while it overflows, then a region named by the title when there is one) and an end-aligned
938
+ `[bfSlot=footer]`, with a close button (`closeButton`, `closeLabel`, default the `close` label)
939
+ last in the tab order. `confirm({ title, message?, buttons?, size?, injector? })` opens an alert
940
+ dialog and resolves with the pressed button's `id ?? label` (default buttons Cancel and OK, ids
941
+ `cancel` and `ok`); focus starts on the cancel button, `Escape` answers with it or is ignored
942
+ without one. A navigation to another URL path closes a dialog with no result and a `confirm()`
943
+ with the cancel button's answer (else `undefined`); `closeOnNavigation: false` (on `BfDialogConfig`
944
+ and `confirm()`) keeps it open. Title and message ids come from the library's shared id counter,
945
+ so they never repeat an id of a `bf-dialog` rendered in the page. Focus is trapped and restored,
946
+ the page behind is `inert` and document scroll is locked (APG dialog (modal) and alert dialog
947
+ patterns).
948
+
949
+ JSDoc of `dialog-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols;
950
+ `@param` docs. No behaviour change.
951
+
952
+ JSDoc of `dialog.component.ts` for the API reference: `@slot` docs for `header`, `default`,
953
+ `footer`. No behaviour change.
954
+
955
+ JSDoc of `dialog.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
956
+ `@default` on the option properties the code applies; `@param` docs. No behaviour change.
957
+
958
+ The `bf-dialog` host carries `data-bf-dialog-id` (its title id, also without a rendered title), so
959
+ `BfDialogHarness` finds the overlay panel the card sits in, also under a stacked dialog.
960
+
961
+ The docs say that Android back in a browser with `CloseWatcher` closes the dialog like `Escape`
962
+ (and that `dismissOnEscape: false` swallows it); see `### overlay core` and Migration 35.