@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,661 @@
1
+ # bf-stack-outlet
2
+
3
+ Router outlet keeping a live page stack with slide transitions and iOS swipe back
4
+
5
+ `BfStackOutletComponent` from `@bitforgehq/ui/mobile` · Navigation · since 0.2.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, inject } from '@angular/core';
11
+ import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
12
+ import {
13
+ BfContentComponent,
14
+ BfHeaderComponent,
15
+ BfNavController,
16
+ BfPageComponent,
17
+ } from '@bitforgehq/ui/mobile';
18
+
19
+ /** `bf-stack-outlet` showcase root: pushes a two-level flow that pops with the back button or a swipe. */
20
+ @Component({
21
+ selector: 'app-stack-outlet-showcase',
22
+ imports: [
23
+ BfPageComponent,
24
+ BfHeaderComponent,
25
+ BfContentComponent,
26
+ BfListComponent,
27
+ BfListItemComponent,
28
+ ],
29
+ template: `
30
+ <bf-page data-showcase="stack-outlet">
31
+ <bf-header title="Stack outlet" [largeTitle]="false" />
32
+ <bf-content>
33
+ <div class="flex flex-col gap-4 p-4">
34
+ <p class="text-caption text-text-muted">
35
+ Push two pages, then pop them with the back button or, in iPhone mode, by dragging from
36
+ the left edge.
37
+ </p>
38
+ <bf-list [inset]="false">
39
+ <bf-list-item
40
+ title="Level 1"
41
+ [interactive]="true"
42
+ [chevron]="true"
43
+ (clicked)="push()"
44
+ />
45
+ </bf-list>
46
+ </div>
47
+ </bf-content>
48
+ </bf-page>
49
+ `,
50
+ })
51
+ export class StackOutletShowcasePage {
52
+ private readonly nav = inject(BfNavController);
53
+
54
+ push(): void {
55
+ void this.nav.forward('/showcase/stack-outlet/1');
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Overview
61
+
62
+ A router outlet that keeps a stack of live pages: a push slides the new page in and parks the
63
+ previous one (still in the DOM, so scroll and state survive), a pop slides the top page out and
64
+ destroys it, and on iOS a finger's edge swipe scrubs the pop (touch only: a mouse or pen drag
65
+ never arms it). `provideBfShell({ maxLivePages })` bounds the live pages of each stack: the
66
+ deepest ones are destroyed and created again, scroll restored, on a pop back to them. Replaces
67
+ `<router-outlet>` in a mobile app.
68
+ Pages receive route params, query params and route data as inputs, bound by the outlet itself.
69
+ Inside `bf-tabs` it keeps one stack per tab and switches between them without a transition.
70
+ Outlets nest: an outlet inside a page (the tabs page) keeps its stacks while an outer outlet
71
+ parks that page, and `BfNavController` follows the innermost outlet whose page is shown.
72
+
73
+ The router outlet of a mobile app. A plain `<router-outlet>` destroys the page you leave, so going
74
+ back rebuilds it: the list is scrolled to the top again, loaded data is fetched again, and there is
75
+ nothing left to slide back to. `bf-stack-outlet` keeps a stack of live pages instead. A push slides
76
+ the new page in over the current one, which stays in the DOM (hidden and inert) with its scroll
77
+ position and state. A pop slides the top page out, destroys it and shows the page below exactly as
78
+ it was. On iOS the pop can be scrubbed with a swipe from the left edge, like a native navigation
79
+ controller. The stack follows the browser history, so the Android back button, `Location.back()`
80
+ and the browser's own back button all pop.
81
+
82
+ ## When to use
83
+
84
+ Use `bf-stack-outlet` in place of `router-outlet` in the Capacitor shell, so pushed pages stay alive and slide like native navigation. Use [bf-tabs](https://bitforge-ui.web.app/components/tabs) instead for one stack per tab, and a plain `router-outlet` on a desktop web app.
85
+
86
+ ## Setup
87
+
88
+ Replace `<router-outlet>` with `<bf-stack-outlet />` and add the shell providers. `provideBfShell()`
89
+ provides the configuration and the stack's route reuse strategy (`BfStackReuseStrategy`: a change of
90
+ path params such as `/items/4 → /items/5` is a new page), and starts the platform services.
91
+ `BfNavController` and the overlay services (`BfSheetService`, `BfModalService`, `BfToastService`,
92
+ `BfActionSheetService`, `BfAlertDialogService`) are provided in root and tree-shakable: an app
93
+ that never injects an overlay service bundles no overlay code. Put `provideBfShell()` in the
94
+ application providers; in a lazy route's `providers` the services still come from the root
95
+ injector.
96
+ `withBfShellRouting()` sets `canceledNavigationResolution: 'computed'`, so a back navigation that a
97
+ guard refuses restores the history position and the stack stays in sync with it.
98
+ The stack outlet binds route state to its pages' inputs itself; keep `withComponentInputBinding()`
99
+ so nested `<router-outlet>`s inside pages bind the same way.
100
+
101
+ ```ts
102
+ import { type ApplicationConfig, Component } from '@angular/core';
103
+ import { type Routes, provideRouter, withComponentInputBinding } from '@angular/router';
104
+ import { BfStackOutletComponent, provideBfShell, withBfShellRouting } from '@bitforgehq/ui/mobile';
105
+
106
+ /** The app's routes; every navigation to a new page pushes it onto the stack. */
107
+ export const routes: Routes = [];
108
+
109
+ /** The shell providers next to the router. */
110
+ export const appConfig: ApplicationConfig = {
111
+ providers: [
112
+ provideRouter(routes, withComponentInputBinding(), withBfShellRouting()),
113
+ provideBfShell({ mode: 'auto', swipeBack: true, deferTransitionStart: true }),
114
+ ],
115
+ };
116
+
117
+ /**
118
+ * The shell setup: the providers, and the stack outlet in place of router-outlet.
119
+ *
120
+ * @preview none
121
+ */
122
+ @Component({
123
+ selector: 'app-example-stack-outlet-setup',
124
+ imports: [BfStackOutletComponent],
125
+ template: `<bf-stack-outlet />`,
126
+ })
127
+ export class StackOutletSetupExampleComponent {}
128
+ ```
129
+
130
+ Navigate with `routerLink` or the router as usual; every navigation to a new page pushes. Use
131
+ `BfNavController` for explicit directions: `forward(url)` pushes, `back()` pops through the real
132
+ history and `root(url)` replaces the whole stack. `nav.canGoBack()` and `nav.depth()` follow the
133
+ deepest outlet whose page is shown (see Nested outlets).
134
+
135
+ ## Pages read route state through inputs
136
+
137
+ A parked page keeps the `ActivatedRoute` it was created with, and after a pop the router hands the
138
+ outlet a new one: the observables of the injected route no longer emit. Take route params, query
139
+ params and route data (including resolved data) through inputs instead. The outlet binds them
140
+ itself, with or without `withComponentInputBinding()`, and re-binds them to the new route after a
141
+ pop. It binds like the router's default: query params, then path params, then data, and an input
142
+ with no matching key is set to `undefined`. It does not read the options of
143
+ `withComponentInputBinding()` (`queryParams: false`, `unmatchedInputBehavior`) and does not bind
144
+ the experimental route `resources`.
145
+
146
+ ```ts
147
+ import { Component, input } from '@angular/core';
148
+ import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
149
+
150
+ /**
151
+ * A page that reads its route param as an input.
152
+ *
153
+ * The `tasks/:id` route's `id` param arrives as the page's `id` input, including when the stack
154
+ * re-shows the page.
155
+ *
156
+ * @preview none
157
+ */
158
+ @Component({
159
+ selector: 'app-example-stack-outlet-inputs',
160
+ imports: [BfContentComponent, BfHeaderComponent, BfPageComponent],
161
+ template: `
162
+ <bf-page>
163
+ <bf-header [title]="'Task ' + id()" [largeTitle]="false" />
164
+ <bf-content>
165
+ <p class="p-4">Details of task {{ id() }}.</p>
166
+ </bf-content>
167
+ </bf-page>
168
+ `,
169
+ })
170
+ export class StackOutletInputsExampleComponent {
171
+ /** From the `tasks/:id` route param. */
172
+ readonly id = input.required<string>();
173
+ }
174
+ ```
175
+
176
+ ## Nested child routes
177
+
178
+ Child routes rendered by a `<router-outlet>` inside a page (tabs inside a detail page, for
179
+ example) do not push: switching between `/items/4` and `/items/4/notes` changes the child, not the
180
+ stack. Popping back to such a page lands on the child it last showed. Popping the page itself —
181
+ the back button, the swipe or `nav.back()` — goes back past the entries its child navigations
182
+ (and query-only changes) added, in one history step, to the entry before the page was pushed.
183
+
184
+ ```ts
185
+ import { Component } from '@angular/core';
186
+ import { RouterLink, RouterOutlet } from '@angular/router';
187
+ import { BfContentComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
188
+
189
+ /**
190
+ * A page with child routes of its own.
191
+ *
192
+ * The child routes render in a plain `router-outlet` inside the page: switching between them
193
+ * does not push a new page.
194
+ *
195
+ * @preview none
196
+ */
197
+ @Component({
198
+ selector: 'app-example-stack-outlet-child-routes',
199
+ imports: [BfContentComponent, BfPageComponent, RouterLink, RouterOutlet],
200
+ template: `
201
+ <bf-page>
202
+ <bf-content>
203
+ <nav class="flex gap-4 p-4">
204
+ <a routerLink="./">Overview</a>
205
+ <a routerLink="notes">Notes</a>
206
+ </nav>
207
+ <router-outlet />
208
+ </bf-content>
209
+ </bf-page>
210
+ `,
211
+ })
212
+ export class StackOutletChildRoutesExampleComponent {}
213
+ ```
214
+
215
+ ## Swipe-back and horizontal scrollers
216
+
217
+ In iOS mode a finger drag that starts within 50 px of the left edge scrubs the pop; releasing it
218
+ with a flick or past half the width completes it, anything else slides the page back. Only touch
219
+ arms it (`pointerType` `touch`): a mouse drag or a text selection from the edge, and a pen, never
220
+ pop the page, also on a desktop web page with the mode forced to `ios`. Turn the gesture off for
221
+ one outlet with `[swipeGesture]="false"`, or for the app with `provideBfShell({ swipeBack: false })`.
222
+
223
+ A horizontal scroller inside a page (a chips row, a carousel) needs nothing: `overflow-x-auto`
224
+ makes it a scroll container, and `touch-action` is intersected only up to a touch's nearest scroll
225
+ container (it is not inherited), so the outlet host's `touch-action: pan-y` never reaches it and
226
+ the row pans on its own. The host's value only keeps horizontal drags as pointer events where the
227
+ host itself is the nearest scroll container (the header), for the swipe-back. A drag that starts
228
+ in the 50 px edge zone is the swipe-back even on a scroller; anywhere else the scroller keeps its
229
+ own scrolling. `bfHorizontalScroll` is deprecated for that reason.
230
+
231
+ ```ts
232
+ import { Component } from '@angular/core';
233
+ import { BfChipComponent } from '@bitforgehq/ui';
234
+ import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
235
+
236
+ /**
237
+ * A stacked page with a horizontal chips row.
238
+ *
239
+ * The row scrolls sideways under a finger with `overflow-x-auto` alone; only a drag that starts in
240
+ * the 50 px left edge is the swipe-back.
241
+ *
242
+ * @preview none
243
+ */
244
+ @Component({
245
+ selector: 'app-example-stack-outlet-scroller',
246
+ imports: [BfChipComponent, BfContentComponent, BfHeaderComponent, BfPageComponent],
247
+ template: `
248
+ <bf-page>
249
+ <bf-header title="Tasks" />
250
+ <bf-content>
251
+ <div class="flex gap-2 overflow-x-auto px-4 py-2">
252
+ @for (filter of filters; track filter) {
253
+ <bf-chip class="shrink-0">{{ filter }}</bf-chip>
254
+ }
255
+ </div>
256
+ </bf-content>
257
+ </bf-page>
258
+ `,
259
+ })
260
+ export class StackOutletScrollerExampleComponent {
261
+ protected readonly filters = ['All', 'Open', 'Done', 'Mine', 'Starred', 'Archived'];
262
+ }
263
+ ```
264
+
265
+ ## Bounded stacks
266
+
267
+ Every pushed page stays alive until it is popped, so a long flow (a folder tree, a feed that
268
+ pushes page after page) keeps every page in memory. `provideBfShell({ maxLivePages: n })` bounds
269
+ it: when a stack holds more than `n` pages, the outlet destroys the components of the deepest
270
+ ones and keeps only a record of each (its route, URL, history position and the `bf-content`
271
+ scroll position). The top `n` pages stay alive. A pop back to such a page creates it again from
272
+ the route the router re-activates, in a new frame, restores its scroll after the first render and
273
+ runs the usual pop transition and lifecycle (`bfViewWillEnter`, `bfViewDidEnter`). The default
274
+ is `Infinity`: every page stays alive, as before.
275
+
276
+ Deep pages come back fresh: whatever lived only in the destroyed component (a form's typed text,
277
+ a local signal, loaded data kept in the page) is gone, and the stacks of outlets nested inside it
278
+ start over at their first page. Keep such state in a service, or keep `maxLivePages` above the
279
+ depth your flows reach. With `maxLivePages: 1` the page under the top one is always a record, so
280
+ the edge swipe-back does not arm (there is nothing to show under the finger); the back button and
281
+ `nav.back()` still pop.
282
+
283
+ ```ts
284
+ import { type ApplicationConfig, Component } from '@angular/core';
285
+ import { type Routes, provideRouter, withComponentInputBinding } from '@angular/router';
286
+ import { BfStackOutletComponent, provideBfShell, withBfShellRouting } from '@bitforgehq/ui/mobile';
287
+
288
+ /** The app's routes, including a flow that can push many pages (a folder tree, a feed). */
289
+ export const routes: Routes = [];
290
+
291
+ /** At most 10 pages of each stack stay alive; deeper ones come back fresh on a pop. */
292
+ export const appConfig: ApplicationConfig = {
293
+ providers: [
294
+ provideRouter(routes, withComponentInputBinding(), withBfShellRouting()),
295
+ provideBfShell({ maxLivePages: 10 }),
296
+ ],
297
+ };
298
+
299
+ /**
300
+ * A stack that bounds its memory.
301
+ *
302
+ * With `maxLivePages`, the outlet destroys the deepest pages beyond the limit and creates them
303
+ * again, scrolled where they were, when the user pops back to them.
304
+ *
305
+ * @preview none
306
+ */
307
+ @Component({
308
+ selector: 'app-example-stack-outlet-bounded',
309
+ imports: [BfStackOutletComponent],
310
+ template: `<bf-stack-outlet />`,
311
+ })
312
+ export class StackOutletBoundedExampleComponent {}
313
+ ```
314
+
315
+ ## Material mode
316
+
317
+ `mode: 'md'` (and `mode: 'auto'` on Android and the web) replaces the slide with Material's
318
+ 220 ms fade-up: the new page fades in while rising 16 px, a pop is the reverse. There is no
319
+ parallax, no dim layer and no edge gesture. `[animated]="false"` switches pages without any
320
+ animation in either mode; the lifecycle events and `transitionStarted` / `transitionEnded` still fire.
321
+
322
+ ```ts
323
+ import { Component, signal } from '@angular/core';
324
+ import { type BfStackChange, BfStackOutletComponent } from '@bitforgehq/ui/mobile';
325
+
326
+ /**
327
+ * An outlet without transitions that reports its depth.
328
+ *
329
+ * `[animated]="false"` swaps pages without the push animation; `stackChanged` reports the depth.
330
+ *
331
+ * @preview none
332
+ */
333
+ @Component({
334
+ selector: 'app-example-stack-outlet-material',
335
+ imports: [BfStackOutletComponent],
336
+ template: `<bf-stack-outlet [animated]="false" (stackChanged)="onChange($event)" />`,
337
+ })
338
+ export class StackOutletMaterialExampleComponent {
339
+ protected readonly depth = signal(1);
340
+
341
+ protected onChange(change: BfStackChange): void {
342
+ this.depth.set(change.depth);
343
+ }
344
+ }
345
+ ```
346
+
347
+ ## One stack per tab
348
+
349
+ Inside `bf-tabs` the outlet keeps one stack per tab, keyed by the URL segment after the tabs route
350
+ (`BF_TAB_CONTEXT`). A navigation onto another tab's segment parks the whole current stack and shows
351
+ the other tab's stack — its page for that URL, with any pages above it dropped — or starts it with a
352
+ new page. A switch has no transition and no swipe-back; `stackChanged` reports the active stack's
353
+ `depth` with its `tabKey`, `BfNavController.depth` follows the active stack and pops stay within it.
354
+ `lastUrlOf(tabKey)` returns the URL of a tab's top page (where `bf-tabs` navigates on a tab switch)
355
+ , `popToRoot()` goes back in history to the active stack's first page in one step (false when that
356
+ position is unknown), and `scrollActiveContentToTop()` scrolls the active page's `bf-content` to
357
+ the top.
358
+
359
+ ## Nested outlets
360
+
361
+ A page can host a `bf-stack-outlet` of its own: the usual case is a root outlet whose `tabs` page
362
+ holds the outlet inside `bf-tabs`, next to root-level pages such as `/gallery` or `/login`. The
363
+ outlets share one `BfNavController`: `depth`, `canGoBack` and `back()` follow the **deepest active
364
+ outlet**, the innermost one whose page is shown. While you move inside the tabs that is the tab's
365
+ stack; once the root outlet pushes `/gallery` above the tabs page, it is the root outlet.
366
+
367
+ Pushing above the tabs page parks it with its outlet and every tab stack intact. Popping back —
368
+ the back button, the swipe, the browser's back or `nav.back()` — shows the tabs page exactly as it
369
+ was left, including the detail page on top of the active tab. `nav.root('/login')` replaces the
370
+ root stack, and the tabs page with all its stacks is destroyed. The Android back button pops the
371
+ deepest active outlet, and only when every shown outlet is at its first page moves the app to the
372
+ background (`provideBfShell({ backAtRoot: 'exit' })` finishes it instead). An edge
373
+ swipe pops the innermost outlet that can pop. The pages inside a parked page get their lifecycle
374
+ events too: the active tab page gets `bfViewWillLeave`/`bfViewDidLeave` when `/gallery` is pushed
375
+ above the tabs page, and `bfViewWillEnter`/`bfViewDidEnter` when it comes back.
376
+
377
+ ```ts
378
+ import { Component, inject } from '@angular/core';
379
+ import { type Routes } from '@angular/router';
380
+ import { BfButtonComponent, BfTabBarComponent, BfTabComponent } from '@bitforgehq/ui';
381
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
382
+ import {
383
+ BfContentComponent,
384
+ BfHeaderComponent,
385
+ BfNavController,
386
+ BfPageComponent,
387
+ BfStackOutletComponent,
388
+ BfTabsComponent,
389
+ } from '@bitforgehq/ui/mobile';
390
+ import { Sun, User } from 'lucide';
391
+
392
+ /** The Today tab's page. */
393
+ @Component({
394
+ selector: 'app-today-page',
395
+ imports: [BfContentComponent, BfHeaderComponent, BfPageComponent],
396
+ template: `
397
+ <bf-page>
398
+ <bf-header title="Today" />
399
+ <bf-content><p class="p-4">Three tasks due.</p></bf-content>
400
+ </bf-page>
401
+ `,
402
+ })
403
+ export class TodayPage {}
404
+
405
+ /** The Me tab's page: it pushes onto the root outlet, above the tabs page. */
406
+ @Component({
407
+ selector: 'app-me-page',
408
+ imports: [BfButtonComponent, BfContentComponent, BfHeaderComponent, BfPageComponent],
409
+ template: `
410
+ <bf-page>
411
+ <bf-header title="Me" />
412
+ <bf-content>
413
+ <bf-button class="m-4" (clicked)="openGallery()">Open the gallery</bf-button>
414
+ </bf-content>
415
+ </bf-page>
416
+ `,
417
+ })
418
+ export class MePage {
419
+ private readonly nav = inject(BfNavController);
420
+
421
+ openGallery(): void {
422
+ void this.nav.forward('/gallery');
423
+ }
424
+ }
425
+
426
+ /** The tabs page: the outlet nested in `bf-tabs`, one stack per tab. */
427
+ @Component({
428
+ selector: 'app-tabs-page',
429
+ imports: [
430
+ BfIconComponent,
431
+ BfStackOutletComponent,
432
+ BfTabBarComponent,
433
+ BfTabComponent,
434
+ BfTabsComponent,
435
+ ],
436
+ template: `
437
+ <bf-tabs tabsPath="tabs">
438
+ <bf-stack-outlet />
439
+ <bf-tab-bar>
440
+ <bf-tab id="today" label="Today"><bf-icon [icon]="sun" /></bf-tab>
441
+ <bf-tab id="me" label="Me"><bf-icon [icon]="user" /></bf-tab>
442
+ </bf-tab-bar>
443
+ </bf-tabs>
444
+ `,
445
+ })
446
+ export class TabsPage {
447
+ protected readonly sun = Sun;
448
+ protected readonly user = User;
449
+ }
450
+
451
+ /** The app's routes: the tabs page and a root-level gallery. */
452
+ export const routes: Routes = [
453
+ {
454
+ path: 'tabs',
455
+ component: TabsPage,
456
+ children: [
457
+ { path: 'today', component: TodayPage },
458
+ { path: 'me', component: MePage },
459
+ { path: '', redirectTo: 'today', pathMatch: 'full' },
460
+ ],
461
+ },
462
+ // A root-level page, pushed above the tabs page; it loads in its own chunk.
463
+ { path: 'gallery', loadComponent: () => import('./gallery.page').then((m) => m.GalleryPage) },
464
+ { path: '', redirectTo: 'tabs', pathMatch: 'full' },
465
+ ];
466
+
467
+ /**
468
+ * Nested outlets: a root outlet whose tabs page holds its own outlet.
469
+ *
470
+ * The app component holds the root outlet; the tabs page nests one inside `bf-tabs`, and the Me
471
+ * page pushes `/gallery` onto the root outlet, above the tabs page.
472
+ *
473
+ * @preview none
474
+ */
475
+ @Component({
476
+ selector: 'app-example-stack-outlet-nested-outlets',
477
+ imports: [BfStackOutletComponent],
478
+ template: `<bf-stack-outlet />`,
479
+ })
480
+ export class StackOutletNestedOutletsExampleComponent {}
481
+ ```
482
+
483
+ ## Desktop
484
+
485
+ `bf-stack-outlet` is part of the Capacitor shell (`provideBfShell()`). A desktop web app uses a plain `router-outlet`: pages replace each other and the browser keeps the history. Inside the shell, the browser's back and forward buttons and their shortcuts pop and push, because the stack follows the history. The edge swipe-back answers touch only, so a mouse drag or a text selection that starts at the left edge never pops a page.
486
+
487
+ ## Theming
488
+
489
+ - Page frame: every page sits in a frame with the `bg` background, so a sliding page never shows
490
+ the page under it through transparent areas.
491
+ - Dim layer: `scrim` (black in both modes) at 15 % (`bg-scrim/15`), faded in over the page under a push.
492
+ - Edge shadow of the sliding page: `shadow-floating`.
493
+ - The host is `display: block; height: 100%`; give its container a height (the app shell or
494
+ `h-dvh`).
495
+
496
+ ## Accessibility
497
+
498
+ Only the top page is exposed: parked pages are `visibility: hidden`, `inert` and
499
+ `aria-hidden="true"`, so neither the keyboard nor a screen reader can reach them, and their change
500
+ detection is paused. The shown page's `bf-content` is the document's one `main` landmark
501
+ (`role="main"`), so a screen reader's landmark jump lands on the page content. During a transition only the entering page is interactive. On push, focus
502
+ moves to the entering page's title — the element marked `data-bf-page-title` (the `bf-header` title
503
+ once it exists), made focusable with `tabindex="-1"` — or to the page frame when there is none. On
504
+ pop, focus returns to the element that had it when the page was left (the row that was tapped),
505
+ with `preventScroll` so the restored scroll position does not jump. Under
506
+ `prefers-reduced-motion: reduce` push and pop become a 150 ms cross-fade with no movement.
507
+
508
+ ## Inputs
509
+
510
+ | Name | Type | Default | Description |
511
+ | -------------- | --------- | ----------- | ------------------------------------------------------------------------------------ |
512
+ | `name` | `string` | `'primary'` | Outlet name, as on `router-outlet`. |
513
+ | `swipeGesture` | `boolean` | `true` | Enables the iOS edge swipe-back for this outlet (ignored in Material mode). |
514
+ | `animated` | `boolean` | `true` | Animates push and pop; false switches pages instantly (lifecycle events still fire). |
515
+
516
+ ## Outputs
517
+
518
+ | Name | Payload | Description |
519
+ | ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
520
+ | `stackChanged` | `BfStackChange` | Emitted with the new depth after every change of the stack. |
521
+ | `transitionStarted` | `BfStackTransitionEvent` | Emitted when a transition between two pages starts (also the start of a swipe-back). |
522
+ | `transitionEnded` | `BfStackTransitionEvent` | Emitted when a transition between two pages has settled. |
523
+ | `stackChange` | `BfStackChange` | Emitted with the new depth after every change of the stack, together with `stackChanged`. **Deprecated** (0.3.0 use stackChanged; removed in 0.4.0) |
524
+ | `transitionStart` | `BfStackTransitionEvent` | Emitted when a transition starts, together with `transitionStarted`. **Deprecated** (0.3.0 use transitionStarted; removed in 0.4.0) |
525
+ | `transitionEnd` | `BfStackTransitionEvent` | Emitted when a transition has settled, together with `transitionEnded`. **Deprecated** (0.3.0 use transitionEnded; removed in 0.4.0) |
526
+
527
+ ## Methods
528
+
529
+ ### `lastUrlOf(tabKey: string): string | null`
530
+
531
+ The URL of the top page of a tab's stack (inside `bf-tabs`), or null when that tab has no
532
+ stack yet. `bf-tabs` navigates there on a tab switch, so the tab comes back as it was left.
533
+
534
+ | Parameter | Type | Description |
535
+ | --------- | -------- | ----------------------------------------------------- |
536
+ | `tabKey` | `string` | The key of the tab, the URL segment after `tabsPath`. |
537
+
538
+ ### `popToRoot(): boolean`
539
+
540
+ Pops the active stack back to its first page in one history step (`Location.historyGo(-n)`
541
+ to the entry before the second page was pushed), keeping that page alive. Returns false, and
542
+ does nothing, when the stack has one page or the history position is unknown.
543
+
544
+ ### `scrollActiveContentToTop(behavior?: ScrollBehavior): void`
545
+
546
+ Scrolls the active page's `bf-content` back to the top (a re-selected tab).
547
+
548
+ | Parameter | Type | Description |
549
+ | ----------- | ---------------- | ------------------------------------ |
550
+ | `behavior?` | `ScrollBehavior` | `smooth` (the default) or `instant`. |
551
+
552
+ ## Notes
553
+
554
+ ### Do
555
+
556
+ - Use one `bf-stack-outlet` per navigation stack, in place of `<router-outlet>`.
557
+ - Take route params as inputs, and load data in `bfViewDidEnter` or from the inputs.
558
+ - Put horizontal scrollers inside pages as plain `overflow-x-auto` rows; they pan on their own.
559
+ - Set `maxLivePages` for flows that push without bound, and keep their state in services.
560
+
561
+ ### Don't
562
+
563
+ - Don't `inject(ActivatedRoute)` in a routed page to read params; it goes stale after a pop.
564
+ - Don't enable WKWebView's `allowsBackForwardNavigationGestures`; it competes with the edge swipe.
565
+ - Don't navigate imperatively to a page that is already in the stack to go back to it; call
566
+ `nav.back()` (a new navigation always pushes).
567
+
568
+ ## Migration notes
569
+
570
+ ### 0.3.0
571
+
572
+ **Deprecated:** `stackChange` → `stackChanged`, `transitionStart` → `transitionStarted` and
573
+ `transitionEnd` → `transitionEnded`. The new outputs emit the same `BfStackChange` and
574
+ `BfStackTransitionEvent` objects at the same moments; the old names still emit them right after
575
+ until 0.4.0 (see Migration 23).
576
+
577
+ JSDoc of `stack-outlet.component.ts` for the API reference: `@summary`/`@since` on the exported
578
+ symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
579
+ leaves them out. No behaviour change.
580
+
581
+ Setup in `docs.md`: `provideBfShell()` provides the configuration and the reuse strategy and starts
582
+ the platform services; `BfNavController` and the overlay services are root-provided and
583
+ tree-shakable.
584
+
585
+ The `router.events` subscription says in a comment why it stays: the stack pairs each navigation
586
+ id's start with its own end. No behaviour change.
587
+
588
+ **Behaviour:** the iOS edge swipe-back arms only for `pointerType` `touch`: a mouse drag or a
589
+ text selection from the left edge, and a pen, never pop the page (a desktop web page with the mode
590
+ forced to `ios`). See Migration 39.
591
+
592
+ New `maxLivePages` (through `provideBfShell()`, default `Infinity`): above it, the outlet destroys
593
+ the components of the deepest pages of a stack and keeps a record of each (route, key, history
594
+ position, navigation id, URL and the `bf-content` scroll position); the top `maxLivePages` pages
595
+ stay alive. A pop back to such a page creates it again from the re-activated route in a new frame,
596
+ restores its scroll after the first render and runs the pop transition and lifecycle. Deep pages
597
+ come back fresh: nested outlets inside a destroyed page lose their stacks. With `maxLivePages: 1`
598
+ the swipe-back does not arm (the page below is a record). The deprecated `stackChange`,
599
+ `transitionStart` and `transitionEnd` keep emitting after their replacements.
600
+
601
+ The host's `touch-action: pan-y` comment and the docs follow the Pointer Events model: page
602
+ scrollers, horizontal rows included, keep their own panning (see `### horizontal-scroll`).
603
+
604
+ ### 0.2.0
605
+
606
+ New `bf-stack-outlet`: a router outlet that keeps a stack of live pages instead of destroying the
607
+ one you leave, implementing `RouterOutletContract` in place of `<router-outlet>`. A push slides
608
+ the new page in and parks the previous one (`visibility: hidden`, `inert`, `aria-hidden`, detached
609
+ change detector, scroll and state kept); a pop slides the top page out, destroys it and restores
610
+ the page below with focus returned (`preventScroll`) to whatever had it. Push vs. pop is
611
+ classified from the router's navigation trigger and the history positions the router records
612
+ (`computed` resolution), so `replaceUrl`, redirects, refused guards, `history.go(-n)` and browser
613
+ forward resolve correctly. A pop always goes back to the history entry before the page was pushed,
614
+ in one step, even when the page added entries of its own (query-only or nested child
615
+ navigations); a swipe whose history step does not pop the outlet slides the page back. Transitions follow `BfShellConfig.mode`: iOS is a 500 ms
616
+ slide-and-parallax under a dimming layer with an edge-swipe-back (50 px left edge, Pointer Events,
617
+ scrubs the pop's `currentTime`, completes on velocity or half-width, `Location.back()` on
618
+ completion); Material is a 220 ms fade-up with no gesture; `prefers-reduced-motion` reduces both to
619
+ a 150 ms cross-fade. `stackChange`, `transitionStart` and `transitionEnd` outputs report depth and
620
+ transition state. `BfNavController` (root, `providedIn: 'root'`) exposes `forward()`, `back()`,
621
+ `root()`, `canGoBack` and `depth` (see `### nav controller`; `back()` pops the top page in one
622
+ history step); `withBfShellRouting()` sets
623
+ `canceledNavigationResolution: 'computed'` so a refused back navigation stays in sync with the
624
+ outlet. The outlet binds route params, query params and route data to its pages' inputs itself
625
+ (`bindInputs`), so a parked page's `ActivatedRoute` staying stale after a pop is never a problem.
626
+ Every primary outlet registers with `BfNavController` on init (`registerOutlet(outlet, level)`,
627
+ `level` = number of stack outlets around it) and unregisters on destroy; it no longer registers
628
+ a hardware back handler of its own.
629
+
630
+ Multi-stack: inside `bf-tabs` (optional `BF_TAB_CONTEXT`) the outlet keeps one stack per tab key
631
+ (`Map<tabKey, StackEntry[]>`). A navigation onto another tab key is a `switch`: the whole current
632
+ stack is parked (every page hidden, `inert`, change detection detached), and the target stack is
633
+ restored — its page for the URL re-activated (attached, visible, focus to its title) with any
634
+ pages above it dropped — or started with a new page; no transition and no gesture. A `root`
635
+ navigation into another tab replaces only that tab's stack. `depth`/`canGoBack` reflect the active
636
+ stack, pops stay within it, and `stackChange` now emits `{ depth, tabKey }` (`tabKey` null outside
637
+ `bf-tabs`). New public methods `lastUrlOf(tabKey)` (URL of a tab's top page), `popToRoot()` (goes back in
638
+ history to the active stack's first page in one step; false when its position is unknown) and
639
+ `scrollActiveContentToTop(behavior?)` (through the active page's `BfPageContext` scroller, which
640
+ the outermost `bf-page` now registers with the outlet).
641
+
642
+ Nested outlets: an outlet inside a page of another outlet (the outlet in `bf-tabs` on a tabs page
643
+ hosted by the root outlet) keeps its stacks while the outer outlet parks that page. Each page gets
644
+ a per-page host token that tells the outlets inside it whether the page is shown; when the router
645
+ deactivates the inner outlet because the outer one pushed a page above its host (e.g.
646
+ `/tabs/today/task/1` → `/gallery`), the inner outlet suspends instead of destroying its stacks,
647
+ and when the outer outlet pops back it resumes the same page (same DOM nodes, every tab stack
648
+ intact) without a transition. A history step that lands on another page of the inner stack pops
649
+ within it as usual. The page shown by the inner outlet follows its host page's lifecycle: it gets
650
+ `willLeave`/`didLeave` when the host is parked and `willEnter`/`didEnter` when it comes back
651
+ (forwarded only on a real state change, so no duplicate events). An edge swipe is claimed by the
652
+ innermost outlet that can pop — also while that outlet is mid-transition, where the swipe is
653
+ ignored — so nested outlets never pop together.
654
+
655
+ Modal depth: the outlet's host now carries a scale rule keyed to `--bf-modal-depth` (written by
656
+ `BfOverlayStack` on every registered outlet root while a page-sheet `bf-modal` is open — see
657
+ `### modal`): `transform: scale(calc(1 - var(--bf-modal-depth, 0) * 0.08))
658
+ translateY(calc(var(--bf-modal-depth, 0) * 8px))`, `border-radius: calc(var(--bf-modal-depth, 0) *
659
+ 12px)`, a 260 ms transition on both (skipped under `prefers-reduced-motion`), `transform-origin:
660
+ top center`. At depth 0 the rule is a no-op; each stacked modal scales the page stack down another
661
+ 8 % and rounds its corners another 12 px.