@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.
- package/CHANGELOG.md +3665 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/ai/MANIFEST.index.json +383 -0
- package/ai/MANIFEST.json +7528 -0
- package/ai/REFERENCE.index.json +1461 -0
- package/ai/REFERENCE.json +7213 -0
- package/ai/UI_GUIDE.md +1159 -0
- package/ai/components/action-bar.md +209 -0
- package/ai/components/action-sheet.md +526 -0
- package/ai/components/alert-dialog.md +509 -0
- package/ai/components/alert.md +376 -0
- package/ai/components/avatar.md +146 -0
- package/ai/components/back-button.md +286 -0
- package/ai/components/badge.md +171 -0
- package/ai/components/button.md +431 -0
- package/ai/components/card.md +303 -0
- package/ai/components/checkbox.md +418 -0
- package/ai/components/chip-group.md +205 -0
- package/ai/components/chip.md +300 -0
- package/ai/components/content.md +474 -0
- package/ai/components/desktop-toast.md +420 -0
- package/ai/components/dialog.md +962 -0
- package/ai/components/empty-state.md +146 -0
- package/ai/components/field.md +319 -0
- package/ai/components/header.md +809 -0
- package/ai/components/icon-button.md +221 -0
- package/ai/components/icon.md +176 -0
- package/ai/components/input.md +455 -0
- package/ai/components/list-item.md +326 -0
- package/ai/components/list.md +178 -0
- package/ai/components/menu-button.md +377 -0
- package/ai/components/menu.md +661 -0
- package/ai/components/modal.md +613 -0
- package/ai/components/number-input.md +423 -0
- package/ai/components/page.md +299 -0
- package/ai/components/progress.md +215 -0
- package/ai/components/radio-group.md +432 -0
- package/ai/components/refresher.md +377 -0
- package/ai/components/segmented.md +517 -0
- package/ai/components/select.md +510 -0
- package/ai/components/sheet.md +845 -0
- package/ai/components/spinner.md +171 -0
- package/ai/components/stack-outlet.md +661 -0
- package/ai/components/tab-bar.md +427 -0
- package/ai/components/tab.md +295 -0
- package/ai/components/tabs.md +408 -0
- package/ai/components/textarea.md +371 -0
- package/ai/components/toast.md +522 -0
- package/ai/components/toggle.md +387 -0
- package/ai/reference/bf-action-directive.md +21 -0
- package/ai/reference/bf-action-sheet-action-role.md +27 -0
- package/ai/reference/bf-action-sheet-action.md +28 -0
- package/ai/reference/bf-action-sheet-options.md +28 -0
- package/ai/reference/bf-action-sheet-service.md +33 -0
- package/ai/reference/bf-alert-dialog-button-role.md +27 -0
- package/ai/reference/bf-alert-dialog-button.md +27 -0
- package/ai/reference/bf-alert-dialog-options.md +29 -0
- package/ai/reference/bf-alert-dialog-service.md +34 -0
- package/ai/reference/bf-alert-variant.md +28 -0
- package/ai/reference/bf-autocapitalize.md +30 -0
- package/ai/reference/bf-avatar-size.md +27 -0
- package/ai/reference/bf-avatar-variant.md +28 -0
- package/ai/reference/bf-back-at-root.md +28 -0
- package/ai/reference/bf-back-button-service.md +40 -0
- package/ai/reference/bf-badge-variant.md +29 -0
- package/ai/reference/bf-button-size.md +29 -0
- package/ai/reference/bf-button-type.md +28 -0
- package/ai/reference/bf-button-variant.md +29 -0
- package/ai/reference/bf-card-padding.md +26 -0
- package/ai/reference/bf-card-variant.md +28 -0
- package/ai/reference/bf-chip-group-mode.md +27 -0
- package/ai/reference/bf-chip-variant.md +26 -0
- package/ai/reference/bf-compare-with.md +24 -0
- package/ai/reference/bf-component-overlay-ref.md +33 -0
- package/ai/reference/bf-confirm-button-role.md +26 -0
- package/ai/reference/bf-confirm-button.md +27 -0
- package/ai/reference/bf-confirm-options.md +31 -0
- package/ai/reference/bf-confirm-size.md +26 -0
- package/ai/reference/bf-content-scroll-event.md +21 -0
- package/ai/reference/bf-defaults-options.md +32 -0
- package/ai/reference/bf-defaults.md +28 -0
- package/ai/reference/bf-desktop-toast-config.md +28 -0
- package/ai/reference/bf-desktop-toast-harness.md +33 -0
- package/ai/reference/bf-desktop-toast-options.md +28 -0
- package/ai/reference/bf-desktop-toast-position.md +28 -0
- package/ai/reference/bf-desktop-toast-service.md +42 -0
- package/ai/reference/bf-dialog-config.md +44 -0
- package/ai/reference/bf-dialog-harness.md +52 -0
- package/ai/reference/bf-dialog-ref.md +44 -0
- package/ai/reference/bf-dialog-role.md +26 -0
- package/ai/reference/bf-dialog-service.md +55 -0
- package/ai/reference/bf-dialog-size.md +31 -0
- package/ai/reference/bf-dialog-template-context.md +32 -0
- package/ai/reference/bf-empty-state-heading-level.md +26 -0
- package/ai/reference/bf-enter-key-hint.md +31 -0
- package/ai/reference/bf-field-control-class.md +32 -0
- package/ai/reference/bf-field-control-state.md +28 -0
- package/ai/reference/bf-field-size.md +29 -0
- package/ai/reference/bf-haptics-impact-style.md +27 -0
- package/ai/reference/bf-haptics-notification-type.md +27 -0
- package/ai/reference/bf-haptics-service.md +41 -0
- package/ai/reference/bf-harness-filters.md +29 -0
- package/ai/reference/bf-horizontal-scroll-directive.md +129 -0
- package/ai/reference/bf-icon-button-variant.md +28 -0
- package/ai/reference/bf-icon-size.md +27 -0
- package/ai/reference/bf-input-autocapitalize.md +36 -0
- package/ai/reference/bf-input-enter-key-hint.md +38 -0
- package/ai/reference/bf-input-input-mode.md +38 -0
- package/ai/reference/bf-input-mode.md +32 -0
- package/ai/reference/bf-input-type.md +35 -0
- package/ai/reference/bf-jsdom-shim-options.md +22 -0
- package/ai/reference/bf-keyboard-assist-behavior.md +26 -0
- package/ai/reference/bf-keyboard-assist-directive.md +185 -0
- package/ai/reference/bf-keyboard-phase.md +30 -0
- package/ai/reference/bf-keyboard-service.md +50 -0
- package/ai/reference/bf-labels-overrides.md +26 -0
- package/ai/reference/bf-labels.md +44 -0
- package/ai/reference/bf-menu-button-harness.md +40 -0
- package/ai/reference/bf-menu-button-variant.md +27 -0
- package/ai/reference/bf-menu-divider-directive.md +13 -0
- package/ai/reference/bf-menu-group-directive.md +14 -0
- package/ai/reference/bf-menu-harness.md +55 -0
- package/ai/reference/bf-menu-item-checkbox-directive.md +31 -0
- package/ai/reference/bf-menu-item-directive.md +31 -0
- package/ai/reference/bf-menu-item-harness.md +52 -0
- package/ai/reference/bf-menu-item-radio-directive.md +29 -0
- package/ai/reference/bf-menu-placement.md +29 -0
- package/ai/reference/bf-menu-trigger-directive.md +46 -0
- package/ai/reference/bf-modal-options.md +30 -0
- package/ai/reference/bf-modal-presentation.md +26 -0
- package/ai/reference/bf-modal-service.md +38 -0
- package/ai/reference/bf-nav-controller.md +59 -0
- package/ai/reference/bf-nav-direction.md +23 -0
- package/ai/reference/bf-nav-target.md +21 -0
- package/ai/reference/bf-number-input-mode.md +28 -0
- package/ai/reference/bf-number-input-step.md +25 -0
- package/ai/reference/bf-overlay-auto-focus.md +29 -0
- package/ai/reference/bf-overlay-controller.md +22 -0
- package/ai/reference/bf-overlay-host.md +36 -0
- package/ai/reference/bf-overlay-options.md +53 -0
- package/ai/reference/bf-overlay-position.md +30 -0
- package/ai/reference/bf-overlay-ref.md +53 -0
- package/ai/reference/bf-overlay-role.md +27 -0
- package/ai/reference/bf-overlay-service.md +53 -0
- package/ai/reference/bf-overlay-stack-options.md +24 -0
- package/ai/reference/bf-overlay-stack.md +81 -0
- package/ai/reference/bf-overlay-template-context.md +24 -0
- package/ai/reference/bf-overscroll-stretch-directive.md +163 -0
- package/ai/reference/bf-page-context.md +24 -0
- package/ai/reference/bf-platform-service.md +53 -0
- package/ai/reference/bf-platform.md +28 -0
- package/ai/reference/bf-progress-size.md +26 -0
- package/ai/reference/bf-progress-tone.md +28 -0
- package/ai/reference/bf-radio-group-harness.md +49 -0
- package/ai/reference/bf-radio-group-orientation.md +26 -0
- package/ai/reference/bf-radio-option.md +30 -0
- package/ai/reference/bf-refresher-refresh-event.md +22 -0
- package/ai/reference/bf-segment-option.md +29 -0
- package/ai/reference/bf-segmented-harness.md +48 -0
- package/ai/reference/bf-segmented-size.md +29 -0
- package/ai/reference/bf-select-harness.md +70 -0
- package/ai/reference/bf-select-option.md +29 -0
- package/ai/reference/bf-sheet-auto-focus.md +29 -0
- package/ai/reference/bf-sheet-options.md +32 -0
- package/ai/reference/bf-sheet-ref.md +45 -0
- package/ai/reference/bf-sheet-service.md +34 -0
- package/ai/reference/bf-shell-config-mode.md +28 -0
- package/ai/reference/bf-shell-config.md +38 -0
- package/ai/reference/bf-shell-dynamic-type-limit.md +21 -0
- package/ai/reference/bf-shell-dynamic-type.md +25 -0
- package/ai/reference/bf-shell-mode.md +28 -0
- package/ai/reference/bf-shell-testing-options.md +26 -0
- package/ai/reference/bf-size.md +45 -0
- package/ai/reference/bf-spinner-size.md +27 -0
- package/ai/reference/bf-stack-change.md +22 -0
- package/ai/reference/bf-stack-reuse-strategy.md +32 -0
- package/ai/reference/bf-stack-transition-event.md +26 -0
- package/ai/reference/bf-stack-transition.md +26 -0
- package/ai/reference/bf-stacked-overlay.md +23 -0
- package/ai/reference/bf-system-bars-style.md +38 -0
- package/ai/reference/bf-tab-bar-controller.md +51 -0
- package/ai/reference/bf-tab-bar-host.md +52 -0
- package/ai/reference/bf-tab-context.md +32 -0
- package/ai/reference/bf-tab-links.md +37 -0
- package/ai/reference/bf-testing-options.md +27 -0
- package/ai/reference/bf-text-scale-service.md +35 -0
- package/ai/reference/bf-theme.md +27 -0
- package/ai/reference/bf-toast-action.md +25 -0
- package/ai/reference/bf-toast-controller.md +17 -0
- package/ai/reference/bf-toast-dismiss-reason.md +29 -0
- package/ai/reference/bf-toast-options.md +29 -0
- package/ai/reference/bf-toast-position.md +26 -0
- package/ai/reference/bf-toast-ref.md +41 -0
- package/ai/reference/bf-toast-service.md +39 -0
- package/ai/reference/bf-toggle-label-position.md +26 -0
- package/ai/reference/bf-transition-kind.md +28 -0
- package/ai/reference/bf-validation-message.md +31 -0
- package/ai/reference/bf-validation-messages.md +33 -0
- package/ai/reference/bf-view-event.md +28 -0
- package/ai/reference/bf-view-lifecycle.md +41 -0
- package/ai/reference/bf-view-state.md +28 -0
- package/ai/reference/bf_default_labels.md +21 -0
- package/ai/reference/bf_default_values.md +21 -0
- package/ai/reference/bf_defaults.md +21 -0
- package/ai/reference/bf_desktop_toast_defaults.md +21 -0
- package/ai/reference/bf_dialog_data.md +17 -0
- package/ai/reference/bf_labels.md +21 -0
- package/ai/reference/bf_overlay_data.md +19 -0
- package/ai/reference/bf_overlay_host.md +21 -0
- package/ai/reference/bf_sheet_default_snap_points.md +17 -0
- package/ai/reference/bf_shell_config.md +21 -0
- package/ai/reference/bf_shell_defaults.md +21 -0
- package/ai/reference/bf_tab_bar.md +21 -0
- package/ai/reference/bf_tab_bar_controller.md +21 -0
- package/ai/reference/bf_tab_context.md +21 -0
- package/ai/reference/bf_tab_links.md +21 -0
- package/ai/reference/bf_theme_storage.md +19 -0
- package/ai/reference/create-transition.md +36 -0
- package/ai/reference/duration_fast.md +17 -0
- package/ai/reference/duration_md.md +17 -0
- package/ai/reference/duration_overlay.md +17 -0
- package/ai/reference/duration_push.md +17 -0
- package/ai/reference/fade-down.md +26 -0
- package/ai/reference/fade-in.md +25 -0
- package/ai/reference/fade-out.md +25 -0
- package/ai/reference/fade-up.md +26 -0
- package/ai/reference/inject-shell-mode.md +23 -0
- package/ai/reference/install-bf-jsdom-shims.md +31 -0
- package/ai/reference/ios_duration.md +17 -0
- package/ai/reference/ios_easing.md +17 -0
- package/ai/reference/is-text-entry.md +25 -0
- package/ai/reference/max_complete_ms.md +17 -0
- package/ai/reference/md-transition.md +31 -0
- package/ai/reference/md_duration.md +17 -0
- package/ai/reference/md_easing.md +17 -0
- package/ai/reference/overlay_duration.md +21 -0
- package/ai/reference/overlay_easing.md +19 -0
- package/ai/reference/overlay_reduced_duration.md +17 -0
- package/ai/reference/pop-frames-at.md +25 -0
- package/ai/reference/prefers-reduced-motion.md +25 -0
- package/ai/reference/provide-bf-defaults.md +33 -0
- package/ai/reference/provide-bf-desktop-toast.md +28 -0
- package/ai/reference/provide-bf-labels.md +38 -0
- package/ai/reference/provide-bf-shell-testing.md +34 -0
- package/ai/reference/provide-bf-shell.md +41 -0
- package/ai/reference/provide-bf-testing.md +33 -0
- package/ai/reference/reduced-transition.md +28 -0
- package/ai/reference/reduced_duration.md +17 -0
- package/ai/reference/resolve-shell-mode.md +33 -0
- package/ai/reference/scale-in.md +25 -0
- package/ai/reference/settle-duration.md +25 -0
- package/ai/reference/settle-pop.md +33 -0
- package/ai/reference/settle_easing.md +17 -0
- package/ai/reference/should-complete-pop.md +26 -0
- package/ai/reference/slide-down.md +26 -0
- package/ai/reference/slide-up.md +26 -0
- package/ai/reference/with-bf-shell-routing.md +19 -0
- package/bin/bf-ui.js +81 -0
- package/fesm2022/bitforgehq-ui-icons.mjs +230 -0
- package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs +5530 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs +680 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs +2675 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-testing.mjs +402 -0
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui.mjs +7765 -0
- package/fesm2022/bitforgehq-ui.mjs.map +1 -0
- package/llms-full.txt +25394 -0
- package/llms.txt +329 -0
- package/package.json +94 -0
- package/theme.css +253 -0
- package/types/bitforgehq-ui-icons.d.ts +50 -0
- package/types/bitforgehq-ui-mobile.d.ts +2914 -0
- package/types/bitforgehq-ui-number-input.d.ts +282 -0
- package/types/bitforgehq-ui-overlay.d.ts +1514 -0
- package/types/bitforgehq-ui-testing.d.ts +495 -0
- 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.
|