@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,809 @@
|
|
|
1
|
+
# bf-header
|
|
2
|
+
|
|
3
|
+
Page header toolbar with a large title that collapses into the toolbar on scroll
|
|
4
|
+
|
|
5
|
+
`BfHeaderComponent` from `@bitforgehq/ui/mobile` · Navigation · since 0.2.0
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, signal } from '@angular/core';
|
|
11
|
+
import {
|
|
12
|
+
BfAvatarComponent,
|
|
13
|
+
BfListComponent,
|
|
14
|
+
BfListItemComponent,
|
|
15
|
+
BfToggleComponent,
|
|
16
|
+
} from '@bitforgehq/ui';
|
|
17
|
+
import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
|
|
18
|
+
import { formatTodaySubtitle } from '../../today-date';
|
|
19
|
+
|
|
20
|
+
/** `bf-header` showcase: a large title collapsing over a long list, with a translucent toggle. */
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'app-header-showcase',
|
|
23
|
+
imports: [
|
|
24
|
+
BfPageComponent,
|
|
25
|
+
BfHeaderComponent,
|
|
26
|
+
BfContentComponent,
|
|
27
|
+
BfAvatarComponent,
|
|
28
|
+
BfListComponent,
|
|
29
|
+
BfListItemComponent,
|
|
30
|
+
BfToggleComponent,
|
|
31
|
+
],
|
|
32
|
+
template: `
|
|
33
|
+
<bf-page data-showcase="header">
|
|
34
|
+
<bf-header title="Header" [subtitle]="subtitle" [translucent]="translucent()">
|
|
35
|
+
<bf-avatar bfSlot="end" initials="LM" size="sm" variant="ink" />
|
|
36
|
+
</bf-header>
|
|
37
|
+
<bf-content>
|
|
38
|
+
<div class="flex flex-col gap-4 px-4 pt-2 pb-6">
|
|
39
|
+
<p class="text-caption text-text-muted">
|
|
40
|
+
Scroll the list: the large title collapses into the toolbar. Translucent lets the list
|
|
41
|
+
scroll under a blurred header.
|
|
42
|
+
</p>
|
|
43
|
+
<bf-list [inset]="false">
|
|
44
|
+
<bf-list-item>
|
|
45
|
+
<bf-toggle label="Translucent" labelPosition="start" [(checked)]="translucent" />
|
|
46
|
+
</bf-list-item>
|
|
47
|
+
@for (row of rows; track row) {
|
|
48
|
+
<bf-list-item [title]="'Row ' + row" subtitle="Scroll to collapse the title" />
|
|
49
|
+
}
|
|
50
|
+
</bf-list>
|
|
51
|
+
</div>
|
|
52
|
+
</bf-content>
|
|
53
|
+
</bf-page>
|
|
54
|
+
`,
|
|
55
|
+
})
|
|
56
|
+
export class HeaderShowcasePage {
|
|
57
|
+
protected readonly translucent = signal(false);
|
|
58
|
+
protected readonly rows = Array.from({ length: 30 }, (_, index) => index + 1);
|
|
59
|
+
protected readonly subtitle = formatTodaySubtitle();
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Overview
|
|
64
|
+
|
|
65
|
+
The page header: a persistent bar (`start` | title | `end`) and, with `largeTitle` (the
|
|
66
|
+
default), the Meadow Sage large title below it that collapses into the bar's title as the
|
|
67
|
+
page's `bf-content` scrolls. The bar follows the shell mode (`provideBfShell({ mode })`, the
|
|
68
|
+
platform's for `'auto'`): iOS, at least 44 px with a semibold title centred on the bar whatever
|
|
69
|
+
the slots hold (symmetric insets of the wider slot's width plus 8 px, measured with a
|
|
70
|
+
`ResizeObserver`); Material, at least 64 px with a regular title leading after the `start` slot.
|
|
71
|
+
The bar grows with its content at large text sizes: the title wraps to at most two lines (the
|
|
72
|
+
bar is never shorter than the measured title, `--bf-header-title-height`), the `start` slot may
|
|
73
|
+
shrink (a back button drops its text before its chevron; in iOS mode capped by
|
|
74
|
+
`--bf-header-start-max` so the centred title keeps up to half the bar) and the `end` slot never
|
|
75
|
+
does. At
|
|
76
|
+
compact heights (`(max-height: 500px)`, phone landscape) it renders no large title: the title is
|
|
77
|
+
the bar's `h1`, the bar is 44 px in both modes and nothing collapses. Once content scrolls under
|
|
78
|
+
it (the large title fully collapsed; without one, `scrollTop > 0`, overscroll excluded) the
|
|
79
|
+
host gets `data-bf-scrolled`: iOS turns translucent with a blur and a 1 px hairline, Material
|
|
80
|
+
takes a tonal tint. Reads its sibling scroller through the page's `BfPageContext` (set by
|
|
81
|
+
`bf-content`) and, from a passive scroll listener, writes `--bf-header-progress` (0…1, over the
|
|
82
|
+
large title block's rendered height, bounded to 44–200 px (a taller title collapses faster), of the scroll position clamped to the
|
|
83
|
+
scrollable range, so overscroll never moves it, and 0 on a page that cannot scroll that far) on
|
|
84
|
+
its host and the large title block's height (its natural height, measured once and on resize,
|
|
85
|
+
times `1 − progress`), so at progress 1 the header is exactly the toolbar; scrolling schedules
|
|
86
|
+
no Angular change detection. Solid, it also writes the collapsed height as
|
|
87
|
+
`--bf-header-collapse` on the scroller, which `bf-content` adds to its bottom padding: the
|
|
88
|
+
scroller grows by what the header gives up, so its scroll range stays the same through the
|
|
89
|
+
collapse and the short-page rule reads one stable value. When a scroll ends part-way
|
|
90
|
+
(`scrollend`, or 120 ms without a `scroll` event) with nothing pressed on the scroller, it snaps
|
|
91
|
+
the scroller to fully expanded or fully collapsed, whichever is nearer, like UIKit. Solid
|
|
92
|
+
(default), it sits in the page's flow above the content; `translucent`, it is drawn over the
|
|
93
|
+
content and `bf-content` pads its scroller by the header's height (`--bf-header-height`).
|
|
94
|
+
|
|
95
|
+
The page header: a persistent bar with `start`/`end` slots and, by default, the Meadow Sage large
|
|
96
|
+
title below it that collapses into the bar's title as the page's `bf-content` scrolls. The bar
|
|
97
|
+
follows the shell mode — a bar of at least 44 px with a centred title in iOS mode, at least 64 px
|
|
98
|
+
with a leading title in Material mode — grows with large text, and changes its surface once
|
|
99
|
+
content scrolls under it. It reads its
|
|
100
|
+
sibling scroller through the page's `BfPageContext` (set by `bf-content`) and writes
|
|
101
|
+
`--bf-header-progress` and `data-bf-scrolled` on its own host from a passive scroll listener, so
|
|
102
|
+
the collapse is pure CSS and scrolling never schedules Angular change detection.
|
|
103
|
+
|
|
104
|
+
## When to use
|
|
105
|
+
|
|
106
|
+
Use `bf-header` at the top of every `bf-page` in the Capacitor shell: the bar with its start and end actions and the collapsing large title. Use a plain `h1` and a toolbar row of [bf-button](https://bitforge-ui.web.app/components/button)s instead on a desktop web page, and [bf-back-button](https://bitforge-ui.web.app/components/back-button) in its `start` slot on a pushed page.
|
|
107
|
+
|
|
108
|
+
## Basic usage
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Component } from '@angular/core';
|
|
112
|
+
import { BfAvatarComponent, BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
|
|
113
|
+
import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A large-title header with a subtitle and an avatar.
|
|
117
|
+
*
|
|
118
|
+
* @preview none
|
|
119
|
+
*/
|
|
120
|
+
@Component({
|
|
121
|
+
selector: 'app-example-header-basic',
|
|
122
|
+
imports: [
|
|
123
|
+
BfAvatarComponent,
|
|
124
|
+
BfContentComponent,
|
|
125
|
+
BfHeaderComponent,
|
|
126
|
+
BfListComponent,
|
|
127
|
+
BfListItemComponent,
|
|
128
|
+
BfPageComponent,
|
|
129
|
+
],
|
|
130
|
+
template: `
|
|
131
|
+
<bf-page>
|
|
132
|
+
<bf-header title="Today" subtitle="Wed, 23 Sep">
|
|
133
|
+
<bf-avatar bfSlot="end" initials="LM" size="sm" />
|
|
134
|
+
</bf-header>
|
|
135
|
+
<bf-content>
|
|
136
|
+
<bf-list>
|
|
137
|
+
<bf-list-item title="Review the Atlas deck" />
|
|
138
|
+
<bf-list-item title="Call Bloom" />
|
|
139
|
+
</bf-list>
|
|
140
|
+
</bf-content>
|
|
141
|
+
</bf-page>
|
|
142
|
+
`,
|
|
143
|
+
})
|
|
144
|
+
export class HeaderBasicExampleComponent {}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## iOS and Material bars
|
|
148
|
+
|
|
149
|
+
The mode is the shell's (`provideBfShell({ mode })`, the platform's for `'auto'`: iOS on iOS,
|
|
150
|
+
Material elsewhere), the same one the stack outlet uses for its transitions; the host carries it
|
|
151
|
+
as `data-bf-mode`.
|
|
152
|
+
|
|
153
|
+
| | iOS mode | Material mode |
|
|
154
|
+
| -------------- | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
155
|
+
| Bar height | at least 44 px (`min-h-11`), grows | at least 64 px (`min-h-16`), grows |
|
|
156
|
+
| Bar title | `text-heading`, semibold, centred on the bar | `text-heading`, regular, leading after `start` (16 px from the edge without one) |
|
|
157
|
+
| Scrolled under | `bg-bg/85`, `backdrop-blur-xl`, 1 px hairline | `surface-muted` tint, no blur, no hairline |
|
|
158
|
+
| Large title | left-aligned, collapses on scroll | the same |
|
|
159
|
+
|
|
160
|
+
The bar title sizes are the closest steps of the Meadow Sage type scale: UIKit's 17 pt semibold
|
|
161
|
+
and Material's 22 dp regular title both map to the 18 px `text-heading`, so the modes differ in
|
|
162
|
+
weight and alignment rather than size.
|
|
163
|
+
|
|
164
|
+
In iOS mode the title is centred on the bar itself, not in the space left between the slots: the
|
|
165
|
+
header measures both slot containers (a `ResizeObserver`) and gives the title symmetric insets of
|
|
166
|
+
the wider slot's width plus 8 px (`--bf-header-title-inset` on the host, measured from the bar's
|
|
167
|
+
edges), so a back button on one side and an avatar on the other never push it off centre. A title
|
|
168
|
+
that does not fit wraps to at most two lines (see [Large text and compact heights](#large-text-and-compact-heights)).
|
|
169
|
+
|
|
170
|
+
## Detail pages
|
|
171
|
+
|
|
172
|
+
Turn off `largeTitle` for a pushed page and put `bf-back-button` in `start`: the toolbar title is
|
|
173
|
+
the page's only heading, so nothing collapses.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Component } from '@angular/core';
|
|
177
|
+
import { BfIconButtonComponent } from '@bitforgehq/ui';
|
|
178
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
179
|
+
import {
|
|
180
|
+
BfBackButtonComponent,
|
|
181
|
+
BfContentComponent,
|
|
182
|
+
BfHeaderComponent,
|
|
183
|
+
BfPageComponent,
|
|
184
|
+
} from '@bitforgehq/ui/mobile';
|
|
185
|
+
import { Share } from 'lucide';
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A detail page's header: a back button, a compact title and a share action.
|
|
189
|
+
*
|
|
190
|
+
* @preview none
|
|
191
|
+
*/
|
|
192
|
+
@Component({
|
|
193
|
+
selector: 'app-example-header-detail',
|
|
194
|
+
imports: [
|
|
195
|
+
BfBackButtonComponent,
|
|
196
|
+
BfContentComponent,
|
|
197
|
+
BfHeaderComponent,
|
|
198
|
+
BfIconButtonComponent,
|
|
199
|
+
BfIconComponent,
|
|
200
|
+
BfPageComponent,
|
|
201
|
+
],
|
|
202
|
+
template: `
|
|
203
|
+
<bf-page>
|
|
204
|
+
<bf-header title="Task" [largeTitle]="false">
|
|
205
|
+
<bf-back-button bfSlot="start" />
|
|
206
|
+
<bf-icon-button bfSlot="end" variant="text" label="Share">
|
|
207
|
+
<bf-icon [icon]="shareIcon" />
|
|
208
|
+
</bf-icon-button>
|
|
209
|
+
</bf-header>
|
|
210
|
+
<bf-content>
|
|
211
|
+
<p class="text-body p-4">Review the Atlas deck before the call.</p>
|
|
212
|
+
</bf-content>
|
|
213
|
+
</bf-page>
|
|
214
|
+
`,
|
|
215
|
+
})
|
|
216
|
+
export class HeaderDetailExampleComponent {
|
|
217
|
+
protected readonly shareIcon = Share;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Subtitle eyebrow
|
|
222
|
+
|
|
223
|
+
`subtitle` renders as a small caption above the large title (e.g. a date); it is dropped along
|
|
224
|
+
with the large title block when `largeTitle` is false.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { Component } from '@angular/core';
|
|
228
|
+
import { BfHeaderComponent } from '@bitforgehq/ui/mobile';
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A header with a subtitle eyebrow above the title.
|
|
232
|
+
*
|
|
233
|
+
* @preview none
|
|
234
|
+
*/
|
|
235
|
+
@Component({
|
|
236
|
+
selector: 'app-example-header-subtitle',
|
|
237
|
+
imports: [BfHeaderComponent],
|
|
238
|
+
template: `<bf-header title="Today" subtitle="Wed, 23 Sep" />`,
|
|
239
|
+
})
|
|
240
|
+
export class HeaderSubtitleExampleComponent {}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Start and end slots
|
|
244
|
+
|
|
245
|
+
`start` (usually `bf-back-button`, or nothing on a root page) and `end` (actions, an avatar)
|
|
246
|
+
are `bfSlot="start"` / `bfSlot="end"` content, projected into the bar on either side of the
|
|
247
|
+
title:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { Component } from '@angular/core';
|
|
251
|
+
import { BfIconButtonComponent } from '@bitforgehq/ui';
|
|
252
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
253
|
+
import { BfHeaderComponent } from '@bitforgehq/ui/mobile';
|
|
254
|
+
import { Plus } from 'lucide';
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A header with an action in its end slot.
|
|
258
|
+
*
|
|
259
|
+
* @preview none
|
|
260
|
+
*/
|
|
261
|
+
@Component({
|
|
262
|
+
selector: 'app-example-header-slots',
|
|
263
|
+
imports: [BfHeaderComponent, BfIconButtonComponent, BfIconComponent],
|
|
264
|
+
template: `
|
|
265
|
+
<bf-header title="Inbox">
|
|
266
|
+
<bf-icon-button bfSlot="end" variant="text" label="Compose">
|
|
267
|
+
<bf-icon [icon]="plusIcon" />
|
|
268
|
+
</bf-icon-button>
|
|
269
|
+
</bf-header>
|
|
270
|
+
`,
|
|
271
|
+
})
|
|
272
|
+
export class HeaderSlotsExampleComponent {
|
|
273
|
+
protected readonly plusIcon = Plus;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The slots follow each platform's bar conventions:
|
|
278
|
+
|
|
279
|
+
- **Both modes**: one leading control in `start` — the back button, or a menu or close control.
|
|
280
|
+
- **iOS**: text buttons (`bf-button variant="text"`) for state actions such as Done, Edit and
|
|
281
|
+
Cancel; plain glyph icon buttons (`bf-icon-button variant="text"`) otherwise; at most two
|
|
282
|
+
trailing actions in `end`.
|
|
283
|
+
- **Material**: icon buttons only (`bf-icon-button variant="text"`, 24 px glyphs), at most three
|
|
284
|
+
trailing actions; move the rest into an overflow menu.
|
|
285
|
+
|
|
286
|
+
Pick per mode with `injectShellMode()` from `@bitforgehq/ui/mobile` (`'ios'` or `'md'`, the same
|
|
287
|
+
resolution the header uses; `resolveShellMode(config, platform)` is the pure form):
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { Component } from '@angular/core';
|
|
291
|
+
import { BfButtonComponent, BfIconButtonComponent } from '@bitforgehq/ui';
|
|
292
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
293
|
+
import { BfBackButtonComponent, BfHeaderComponent, injectShellMode } from '@bitforgehq/ui/mobile';
|
|
294
|
+
import { Pencil } from 'lucide';
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A header action that follows the shell mode.
|
|
298
|
+
*
|
|
299
|
+
* `injectShellMode()` reads the resolved mode once: iOS shows the state action as text, Material
|
|
300
|
+
* as an icon button.
|
|
301
|
+
*
|
|
302
|
+
* @preview none
|
|
303
|
+
*/
|
|
304
|
+
@Component({
|
|
305
|
+
selector: 'app-example-header-mode',
|
|
306
|
+
imports: [
|
|
307
|
+
BfBackButtonComponent,
|
|
308
|
+
BfButtonComponent,
|
|
309
|
+
BfHeaderComponent,
|
|
310
|
+
BfIconButtonComponent,
|
|
311
|
+
BfIconComponent,
|
|
312
|
+
],
|
|
313
|
+
template: `
|
|
314
|
+
<bf-header title="Tasks" [largeTitle]="false">
|
|
315
|
+
<bf-back-button bfSlot="start" />
|
|
316
|
+
@if (ios) {
|
|
317
|
+
<bf-button bfSlot="end" variant="text" size="sm">Edit</bf-button>
|
|
318
|
+
} @else {
|
|
319
|
+
<bf-icon-button bfSlot="end" variant="text" label="Edit">
|
|
320
|
+
<bf-icon [icon]="pencilIcon" />
|
|
321
|
+
</bf-icon-button>
|
|
322
|
+
}
|
|
323
|
+
</bf-header>
|
|
324
|
+
`,
|
|
325
|
+
})
|
|
326
|
+
export class HeaderModeExampleComponent {
|
|
327
|
+
protected readonly ios = injectShellMode() === 'ios';
|
|
328
|
+
protected readonly pencilIcon = Pencil;
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { Component } from '@angular/core';
|
|
334
|
+
import { BfButtonComponent, BfIconButtonComponent } from '@bitforgehq/ui';
|
|
335
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
336
|
+
import { BfBackButtonComponent, BfHeaderComponent } from '@bitforgehq/ui/mobile';
|
|
337
|
+
import { Pencil } from 'lucide';
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The same action in the two modes.
|
|
341
|
+
*
|
|
342
|
+
* iOS mode: a state action as text. Material mode: the same action as an icon button.
|
|
343
|
+
*
|
|
344
|
+
* @preview none
|
|
345
|
+
*/
|
|
346
|
+
@Component({
|
|
347
|
+
selector: 'app-example-header-modes',
|
|
348
|
+
imports: [
|
|
349
|
+
BfBackButtonComponent,
|
|
350
|
+
BfButtonComponent,
|
|
351
|
+
BfHeaderComponent,
|
|
352
|
+
BfIconButtonComponent,
|
|
353
|
+
BfIconComponent,
|
|
354
|
+
],
|
|
355
|
+
template: `
|
|
356
|
+
<!-- iOS mode: a state action as text -->
|
|
357
|
+
<bf-header title="Tasks" [largeTitle]="false">
|
|
358
|
+
<bf-back-button bfSlot="start" />
|
|
359
|
+
<bf-button bfSlot="end" variant="text" size="sm">Edit</bf-button>
|
|
360
|
+
</bf-header>
|
|
361
|
+
|
|
362
|
+
<!-- Material mode: the same action as an icon button -->
|
|
363
|
+
<bf-header title="Tasks" [largeTitle]="false">
|
|
364
|
+
<bf-back-button bfSlot="start" />
|
|
365
|
+
<bf-icon-button bfSlot="end" variant="text" label="Edit">
|
|
366
|
+
<bf-icon [icon]="pencilIcon" />
|
|
367
|
+
</bf-icon-button>
|
|
368
|
+
</bf-header>
|
|
369
|
+
`,
|
|
370
|
+
})
|
|
371
|
+
export class HeaderModesExampleComponent {
|
|
372
|
+
protected readonly pencilIcon = Pencil;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Custom title content
|
|
377
|
+
|
|
378
|
+
Leave `title` empty to project your own title markup instead of plain text — it lands in
|
|
379
|
+
whichever `h1` is active (the large title's when `largeTitle` is true, the toolbar's otherwise):
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { Component, signal } from '@angular/core';
|
|
383
|
+
import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
|
|
384
|
+
import { BfHeaderComponent } from '@bitforgehq/ui/mobile';
|
|
385
|
+
|
|
386
|
+
/** A workspace picker in the title's place. */
|
|
387
|
+
@Component({
|
|
388
|
+
selector: 'app-workspace-switcher',
|
|
389
|
+
imports: [BfSelectComponent],
|
|
390
|
+
template: `
|
|
391
|
+
<bf-select size="sm" ariaLabel="Workspace" [options]="workspaces" [(value)]="workspace" />
|
|
392
|
+
`,
|
|
393
|
+
})
|
|
394
|
+
export class WorkspaceSwitcherComponent {
|
|
395
|
+
protected readonly workspaces: BfSelectOption[] = [
|
|
396
|
+
{ value: 'studio', label: 'Studio' },
|
|
397
|
+
{ value: 'home', label: 'Home' },
|
|
398
|
+
];
|
|
399
|
+
protected readonly workspace = signal<string | null>('studio');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A header with custom title content.
|
|
404
|
+
*
|
|
405
|
+
* Without a `title`, the default slot takes the title's place.
|
|
406
|
+
*
|
|
407
|
+
* @preview none
|
|
408
|
+
*/
|
|
409
|
+
@Component({
|
|
410
|
+
selector: 'app-example-header-custom-title',
|
|
411
|
+
imports: [BfHeaderComponent, WorkspaceSwitcherComponent],
|
|
412
|
+
template: `
|
|
413
|
+
<bf-header>
|
|
414
|
+
<app-workspace-switcher />
|
|
415
|
+
</bf-header>
|
|
416
|
+
`,
|
|
417
|
+
})
|
|
418
|
+
export class HeaderCustomTitleExampleComponent {}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## The scroll-collapsing large title
|
|
422
|
+
|
|
423
|
+
With `largeTitle` (the default), the large title collapses into the toolbar as the page's
|
|
424
|
+
`bf-content` scrolls: the header attaches a passive listener to the scroller registered in
|
|
425
|
+
`BfPageContext` and maps the scroll over the large title's own rendered height (the block with its
|
|
426
|
+
eyebrow and bottom padding, about 70 px at the default text size; at least 44 px, and 56 px until
|
|
427
|
+
it is measured) to `--bf-header-progress` (0 expanded, 1 collapsed), so the collapse ends exactly
|
|
428
|
+
when the title has scrolled away. The range is at most 200 px: a taller title (three lines at the
|
|
429
|
+
largest text sizes) collapses its whole height over 200 px of scroll, so a little faster than it
|
|
430
|
+
scrolls. Inside the collapsing clip the large title fades (`1 − progress`) and rises 8 px
|
|
431
|
+
(`progress × 8px`), so it reads as sliding under the bar rather than being cut off; the bar's title
|
|
432
|
+
fades in (`progress`); and the large title block's height shrinks from its natural height to 0 —
|
|
433
|
+
so at progress 1 the header is exactly the bar (plus the top safe area). All of it follows the
|
|
434
|
+
scroll position directly, with no transition. The natural height is measured
|
|
435
|
+
once the header renders and again only when it resizes (a `ResizeObserver`), never per scroll
|
|
436
|
+
frame, and scrolling schedules no Angular change detection. Used outside a `bf-page` (no
|
|
437
|
+
`BfPageContext` in the injector, e.g. a docs demo), the header simply never collapses.
|
|
438
|
+
|
|
439
|
+
The collapse behaves like a UIKit large title:
|
|
440
|
+
|
|
441
|
+
- **Overscroll never moves it.** The progress is computed from the scroll position clamped to the
|
|
442
|
+
scrollable range (`0` to `scrollHeight − clientHeight`), so a rubber-band past the top or the
|
|
443
|
+
bottom of the page (the negative or too-large `scrollTop` WebKit reports while bouncing) leaves
|
|
444
|
+
the header where it was.
|
|
445
|
+
- **The scroll range never changes under it.** A solid header shrinks by the large title's
|
|
446
|
+
collapsed height, which makes `bf-content`'s scroller that much taller. The header writes the
|
|
447
|
+
same amount as `--bf-header-collapse` on the scroller (`largeHeight × progress` in px, `0px` at
|
|
448
|
+
rest, removed on destroy), and `bf-content` adds it to its bottom padding, so the page gains
|
|
449
|
+
exactly the scroll it would otherwise lose and `scrollHeight − clientHeight` stays the rest
|
|
450
|
+
range through the whole collapse. A translucent header is drawn over the scroller, which never
|
|
451
|
+
resizes, so it keeps the compensation at `0px`. While the soft keyboard is up,
|
|
452
|
+
`bfKeyboardAssist`'s inline bottom padding replaces the compensation (no snap runs then).
|
|
453
|
+
- **Short pages never collapse.** A page whose rest range is under the collapse range (in iOS mode even a
|
|
454
|
+
page with nothing to scroll overflows by 1 px, so WebKit rubber-bands it) keeps its large title
|
|
455
|
+
expanded — it could never collapse fully, so it must not stop half-way. A page with a longer
|
|
456
|
+
rest range collapses fully and stays collapsed, even when the content itself (without the
|
|
457
|
+
compensation) scrolls less than the collapse range once the header has shrunk.
|
|
458
|
+
- **It snaps on scroll end.** When a scroll ends part-way (`scrollend` where the engine fires it,
|
|
459
|
+
otherwise 120 ms without a `scroll` event) and no finger or pointer is down on the scroller, the
|
|
460
|
+
header scrolls the page to fully expanded (`0`, below half-way) or fully collapsed (the collapse
|
|
461
|
+
range), with
|
|
462
|
+
a smooth scroll (instant under `prefers-reduced-motion`). It waits while a finger holds the page
|
|
463
|
+
and snaps once it lifts and the momentum has settled (a tap that did not scroll never snaps); its own scroll never triggers another
|
|
464
|
+
snap, and it does not snap while the soft keyboard is up (`bfKeyboardAssist` owns the scroll
|
|
465
|
+
then) or on a page that is not the visible one.
|
|
466
|
+
|
|
467
|
+
## Scroll-under appearance
|
|
468
|
+
|
|
469
|
+
Once content scrolls under the bar the host gets `data-bf-scrolled`, removed again when it
|
|
470
|
+
leaves. With a large title that is when the title has fully collapsed (the end of the collapse
|
|
471
|
+
range, with 0.5 px of tolerance for a fractional snap landing): like UIKit, the large title belongs
|
|
472
|
+
to the scroll-edge appearance, so the bar changes as soon as it is gone, including after a snap to
|
|
473
|
+
collapsed (a page too short to collapse never gets it). Without a large title (`largeTitle` false) it is as soon as the
|
|
474
|
+
page leaves its top (`scrollTop > 0`, read clamped so a rubber-band past the top does not count,
|
|
475
|
+
on pages of any length). The bar signals it the platform's way:
|
|
476
|
+
|
|
477
|
+
- **iOS**: opaque `bg-bg` at rest; scrolled under, translucent `bg-bg/85` with `backdrop-blur-xl`
|
|
478
|
+
and a 1 px `border` hairline along the bottom of the whole header (under the collapsed large
|
|
479
|
+
title block too). The hairline is the host's `::after`, so it never changes the header's height.
|
|
480
|
+
- **Material**: never a hairline or blur; scrolled under, the surface takes the `surface-muted`
|
|
481
|
+
tint, with a 150 ms colour transition (`motion-safe` only).
|
|
482
|
+
|
|
483
|
+
## Large text and compact heights
|
|
484
|
+
|
|
485
|
+
The header keeps its content at the largest text sizes (iOS Dynamic Type up to 235 %, Android
|
|
486
|
+
font scale 2.0):
|
|
487
|
+
|
|
488
|
+
- **The bar grows.** Its height is a minimum (`min-h-11` in iOS mode, `min-h-16` in Material
|
|
489
|
+
mode), so larger text and taller slot content make it taller instead of clipping. The iOS title
|
|
490
|
+
box is absolutely positioned to stay centred, so the header measures the title
|
|
491
|
+
(`--bf-header-title-height`, in the same `ResizeObserver` pass as the slot insets) and keeps the
|
|
492
|
+
bar at least that tall.
|
|
493
|
+
- **The bar title wraps** to at most two lines (`line-clamp-2`, breaking a long word anywhere)
|
|
494
|
+
instead of truncating; the iOS title centres each line.
|
|
495
|
+
- **The `start` slot may shrink, the `end` slot never does**, so actions stay whole. A
|
|
496
|
+
`bf-back-button` in `start` lets its text truncate and then disappear before the chevron, as
|
|
497
|
+
UIKit drops the back title when space is short. In iOS mode, where the centred title sits out of
|
|
498
|
+
the row's flow, the header caps the slot (`--bf-header-start-max`) only when the title does not
|
|
499
|
+
fit on one line beside the slots at their full width: the title may then claim up to half the
|
|
500
|
+
bar, and only a back button's text gives way. The cap never goes below the slot's other content
|
|
501
|
+
(two icon buttons stay whole) or one 44 px hit area, so a custom back label such as "Projects"
|
|
502
|
+
stays whole beside a short title and truncates only beside a long or large one. A title
|
|
503
|
+
projected as content (no `title` input) is not measured, so it gets no cap.
|
|
504
|
+
- **The large title is capped at 200 %** (80 px text on 88 px lines, `min(var(--text-display),
|
|
505
|
+
80px)`), in px because Dynamic Type scales the root and so every rem; it still wraps, and the
|
|
506
|
+
collapse follows its real height.
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { Component } from '@angular/core';
|
|
510
|
+
import { BfIconButtonComponent } from '@bitforgehq/ui';
|
|
511
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
512
|
+
import {
|
|
513
|
+
BfBackButtonComponent,
|
|
514
|
+
BfContentComponent,
|
|
515
|
+
BfHeaderComponent,
|
|
516
|
+
BfPageComponent,
|
|
517
|
+
} from '@bitforgehq/ui/mobile';
|
|
518
|
+
import { Share } from 'lucide';
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* A long title wraps to two lines and the bar grows with it; the back text gives way first.
|
|
522
|
+
*
|
|
523
|
+
* @preview none
|
|
524
|
+
*/
|
|
525
|
+
@Component({
|
|
526
|
+
selector: 'app-example-header-long-title',
|
|
527
|
+
imports: [
|
|
528
|
+
BfBackButtonComponent,
|
|
529
|
+
BfContentComponent,
|
|
530
|
+
BfHeaderComponent,
|
|
531
|
+
BfIconButtonComponent,
|
|
532
|
+
BfIconComponent,
|
|
533
|
+
BfPageComponent,
|
|
534
|
+
],
|
|
535
|
+
template: `
|
|
536
|
+
<bf-page>
|
|
537
|
+
<bf-header title="Prepare the quarterly review for the studio" [largeTitle]="false">
|
|
538
|
+
<bf-back-button bfSlot="start" label="Today" />
|
|
539
|
+
<bf-icon-button bfSlot="end" variant="text" label="Share">
|
|
540
|
+
<bf-icon [icon]="shareIcon" />
|
|
541
|
+
</bf-icon-button>
|
|
542
|
+
</bf-header>
|
|
543
|
+
<bf-content>
|
|
544
|
+
<p class="text-body p-4">Collect the numbers from every project before Friday.</p>
|
|
545
|
+
</bf-content>
|
|
546
|
+
</bf-page>
|
|
547
|
+
`,
|
|
548
|
+
})
|
|
549
|
+
export class HeaderLongTitleExampleComponent {
|
|
550
|
+
protected readonly shareIcon = Share;
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
At compact heights (`(max-height: 500px)`: phone landscape, split screen) there is no room for a
|
|
555
|
+
large title. The header then renders none: the title is the bar's `h1`, the bar is at least 44 px
|
|
556
|
+
in both modes, and nothing collapses or snaps. The header follows the media query live
|
|
557
|
+
(`matchMedia`, the listener removed with the header), so rotating back to portrait brings the
|
|
558
|
+
large title back. With `bf-tabs`, the tab bar slims down too, so at least 60 % of the height is
|
|
559
|
+
content.
|
|
560
|
+
|
|
561
|
+
## Translucent
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
import { Component } from '@angular/core';
|
|
565
|
+
import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
|
|
566
|
+
import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* A translucent header the content scrolls under.
|
|
570
|
+
*
|
|
571
|
+
* @preview none
|
|
572
|
+
*/
|
|
573
|
+
@Component({
|
|
574
|
+
selector: 'app-example-header-translucent',
|
|
575
|
+
imports: [
|
|
576
|
+
BfContentComponent,
|
|
577
|
+
BfHeaderComponent,
|
|
578
|
+
BfListComponent,
|
|
579
|
+
BfListItemComponent,
|
|
580
|
+
BfPageComponent,
|
|
581
|
+
],
|
|
582
|
+
template: `
|
|
583
|
+
<bf-page>
|
|
584
|
+
<bf-header title="Today" [translucent]="true" />
|
|
585
|
+
<bf-content>
|
|
586
|
+
<bf-list>
|
|
587
|
+
@for (row of rows; track row) {
|
|
588
|
+
<bf-list-item [title]="'Task ' + row" />
|
|
589
|
+
}
|
|
590
|
+
</bf-list>
|
|
591
|
+
</bf-content>
|
|
592
|
+
</bf-page>
|
|
593
|
+
`,
|
|
594
|
+
})
|
|
595
|
+
export class HeaderTranslucentExampleComponent {
|
|
596
|
+
protected readonly rows = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
By default (solid) the header sits in the page's column above `bf-content`, on an opaque `bg-bg`.
|
|
601
|
+
With `translucent` it is taken out of the flow instead — `absolute top-0 inset-x-0 z-(--bf-z-sticky)` over the
|
|
602
|
+
content, with the same per-mode scroll-under surface — and `bf-content` fills the whole page. The header
|
|
603
|
+
publishes its expanded height through `BfPageContext`, and `bf-content` writes it as
|
|
604
|
+
`--bf-header-height` on its scroller and pads its top by it (`pt-(--bf-header-height,0px)`, also
|
|
605
|
+
as `scroll-padding-top`), so the first row starts below the header and content scrolls under the
|
|
606
|
+
bar (under the blur in iOS mode). Without a `bf-page` (no `BfPageContext`) nothing pads the content, so use `translucent` only
|
|
607
|
+
inside one.
|
|
608
|
+
|
|
609
|
+
## Desktop
|
|
610
|
+
|
|
611
|
+
`bf-header` is part of the Capacitor shell (`provideBfShell()`), shaped like an iOS or Material app bar. On a desktop web page, give the page a plain `h1` and put its actions in a toolbar row of [bf-button](https://bitforge-ui.web.app/components/button)s and a [bf-menu-button](https://bitforge-ui.web.app/components/menu-button); the large title and the scroll-under surface are phone patterns. Under the shell, a browser window shorter than 500 px gets the compact bar (no large title), as a phone in landscape does.
|
|
612
|
+
|
|
613
|
+
## Theming
|
|
614
|
+
|
|
615
|
+
- Background: `bg` at rest in both modes; scrolled under, `bg/85` with `backdrop-blur-xl` (iOS) or
|
|
616
|
+
`surface-muted` (Material), on the host.
|
|
617
|
+
- Hairline: iOS only, 1 px `border` under the whole header while scrolled under.
|
|
618
|
+
- Type: large title `text-display`, capped at 80/88 px; bar title `text-heading` (semibold in iOS mode, regular in
|
|
619
|
+
Material mode); the `subtitle` eyebrow `text-caption`.
|
|
620
|
+
- Text: large title and bar title in `text`; the `subtitle` eyebrow in `text-muted`.
|
|
621
|
+
- Spacing: the large title block is inset by the 16 px screen gutter (`px-gutter`), so it lines up
|
|
622
|
+
with page content padded the same way. The toolbar is padded by the top safe area
|
|
623
|
+
(`pt-safe-top`) while the page's `BfPageContext.safeAreaTop` is true; a page-sheet `bf-modal`
|
|
624
|
+
sets it to false, since its card already sits below the status bar, so the header adds no
|
|
625
|
+
second status-bar band there.
|
|
626
|
+
- Variables it writes: `--bf-header-progress` (0…1), `--bf-header-title-inset` (px),
|
|
627
|
+
`--bf-header-title-height` (px) and, in iOS mode, `--bf-header-start-max` (px) on its host, `--bf-header-collapse` (px) on the page's scroller.
|
|
628
|
+
- Attributes it writes on its host: `data-bf-mode` (`ios` or `md`) and `data-bf-scrolled` (while
|
|
629
|
+
content is under the bar), usable for app-level styling.
|
|
630
|
+
|
|
631
|
+
## Accessibility
|
|
632
|
+
|
|
633
|
+
Exactly one `h1` is ever rendered — the large title's when `largeTitle` is true, the toolbar's
|
|
634
|
+
otherwise — and it carries `data-bf-page-title`, the element `bf-stack-outlet` focuses after a
|
|
635
|
+
push so a screen reader announces where navigation landed. With `largeTitle`, the toolbar shows a
|
|
636
|
+
second, `aria-hidden="true"` mirror of the title purely for the visual collapse; it carries no
|
|
637
|
+
semantics of its own. `prefers-reduced-motion` drops the large title's upward translate, the Material tint's colour
|
|
638
|
+
transition and makes the scroll-end snap instant; the fade and the height collapse follow the
|
|
639
|
+
scroll position directly (direct manipulation, not an animation). The centred iOS title box is
|
|
640
|
+
`pointer-events-none` so it never covers a slot; custom content projected into the toolbar `h1`
|
|
641
|
+
stays interactive. At the largest text sizes nothing in the bar clips (WCAG 1.4.4): the bar grows,
|
|
642
|
+
the title wraps and the back text gives way before any action.
|
|
643
|
+
|
|
644
|
+
### Keyboard
|
|
645
|
+
|
|
646
|
+
The header has no keys of its own: its slots set the order in which Tab reaches the page actions.
|
|
647
|
+
The title is not in the tab order; after a push, `bf-stack-outlet` focuses it with
|
|
648
|
+
`tabindex="-1"`.
|
|
649
|
+
|
|
650
|
+
| Key | Action |
|
|
651
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
652
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the start slot's controls, then the end slot's, in DOM order |
|
|
653
|
+
|
|
654
|
+
## Inputs
|
|
655
|
+
|
|
656
|
+
| Name | Type | Default | Description |
|
|
657
|
+
| ------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
658
|
+
| `title` | `string` | `''` | The page title, rendered in the toolbar and, with `largeTitle`, in the large title block. |
|
|
659
|
+
| `subtitle` | `string` | `''` | Eyebrow line above the large title (e.g. a date); rendered only with `largeTitle`. |
|
|
660
|
+
| `largeTitle` | `boolean` | `true` | Renders the collapsing large title block below the toolbar; never at compact heights (`(max-height: 500px)`), where the title is the bar's `h1`. |
|
|
661
|
+
| `translucent` | `boolean` | `false` | Draws the header over the content (`absolute`) instead of in the page's flow, so content scrolls under its scroll-under surface; `bf-content` pads its scroller by the header's height. |
|
|
662
|
+
|
|
663
|
+
## Slots
|
|
664
|
+
|
|
665
|
+
- `start`: Toolbar buttons at the start, such as `bf-back-button`.
|
|
666
|
+
- `default`: The toolbar title, when the `title` input is empty.
|
|
667
|
+
- `end`: Toolbar buttons at the end.
|
|
668
|
+
|
|
669
|
+
## Notes
|
|
670
|
+
|
|
671
|
+
### Do
|
|
672
|
+
|
|
673
|
+
- Give every page exactly one `bf-header` (or another element carrying `data-bf-page-title`) so
|
|
674
|
+
push focus has somewhere to land.
|
|
675
|
+
- Turn off `largeTitle` on pushed detail pages; keep it on tab roots.
|
|
676
|
+
- Put `bf-back-button` in `start` on any page that isn't a tab root.
|
|
677
|
+
- iOS: use text buttons for Done, Edit and Cancel and plain glyph icons otherwise, at most two in
|
|
678
|
+
`end`. Material: use icon buttons only, at most three in `end`.
|
|
679
|
+
|
|
680
|
+
### Don't
|
|
681
|
+
|
|
682
|
+
- Don't put a text button in a Material bar or a filled icon button in an iOS bar, and don't put
|
|
683
|
+
more than one control in `start`.
|
|
684
|
+
- Don't put more than one `h1` in a page's header — the toolbar mirror is `aria-hidden` on
|
|
685
|
+
purpose, not a second heading.
|
|
686
|
+
- Don't expect the collapse to run without a `bf-content` inside the same `bf-page`; the header
|
|
687
|
+
reads the scroller `bf-content` registers in `BfPageContext`.
|
|
688
|
+
|
|
689
|
+
## Migration notes
|
|
690
|
+
|
|
691
|
+
### 0.3.0
|
|
692
|
+
|
|
693
|
+
JSDoc of `header.component.ts` for the API reference: `@slot` docs for `start`, `default`, `end`. No
|
|
694
|
+
behaviour change.
|
|
695
|
+
|
|
696
|
+
The toolbar pads the top safe area (`pt-safe-top`) only while the page's
|
|
697
|
+
`BfPageContext.safeAreaTop()` is true; inside a page-sheet `bf-modal` (false) it adds none
|
|
698
|
+
(Migration 29).
|
|
699
|
+
|
|
700
|
+
**Behaviour:** large text (F-051). The bar has a minimum height instead of a fixed one
|
|
701
|
+
(`min-h-11` in iOS mode, `min-h-16` in Material mode) and grows with its content; the header
|
|
702
|
+
measures the bar title as `--bf-header-title-height` (the same `ResizeObserver` pass as the slot
|
|
703
|
+
insets) and keeps the bar at least that tall, since the centred iOS title box is absolutely
|
|
704
|
+
positioned. The bar title wraps to at most two lines (`line-clamp-2 wrap-anywhere`, centred in iOS
|
|
705
|
+
mode) instead of truncating. The `start` slot may shrink (`min-w-0 shrink`) and the `end` slot
|
|
706
|
+
never does (`shrink-0`), so actions stay whole. In iOS mode, where the centred title is taken out
|
|
707
|
+
of the row's flow, the `start` slot is capped by `--bf-header-start-max` only when the title does
|
|
708
|
+
not fit on one line beside the slots at their full width: the title may then claim up to half the
|
|
709
|
+
bar, and only a back button's text (`bf-back-button`'s `[data-bf-back-text]`) gives way. The cap
|
|
710
|
+
is never below the slot's other content or 44 px, so two start buttons stay whole, and the title
|
|
711
|
+
inset reads the slot's `scrollWidth` too. It is measured from the bar's width, a hidden one-line
|
|
712
|
+
copy of the title and the slot's full and minimum widths, so it never depends on the capped slot
|
|
713
|
+
itself. A projected title (no `title` input) gets no cap. The large title's font and line height are capped
|
|
714
|
+
at 200 % (`min(var(--text-display), 80px)`, `min(var(--text-display--line-height), 88px)`), in px
|
|
715
|
+
because iOS Dynamic Type scales the root.
|
|
716
|
+
|
|
717
|
+
**Behaviour:** the collapse range is the large title block's rendered height, bounded to 44–200 px
|
|
718
|
+
(was a fixed 56 px, while the iOS large title renders about 70 px; a block taller than 200 px
|
|
719
|
+
collapses its whole height over 200 px of scroll), re-read whenever it resizes; the snap goes to
|
|
720
|
+
it; `data-bf-scrolled` switches on at full collapse, as before, now at that height (with 0.5 px of
|
|
721
|
+
tolerance for a fractional snap landing). See Migration 31.
|
|
722
|
+
|
|
723
|
+
**Behaviour:** compact heights (F-054). Under `(max-height: 500px)` (phone landscape, split
|
|
724
|
+
screen; read with `matchMedia` and followed live, the listener removed on destroy) the header
|
|
725
|
+
renders no large title: the title is the bar's `h1`, the bar is at least 44 px in both modes and
|
|
726
|
+
nothing collapses or snaps.
|
|
727
|
+
|
|
728
|
+
**Behaviour:** without a large title (`largeTitle` false, or a compact height) the scroll never
|
|
729
|
+
moves `--bf-header-progress` and never snaps; a detail page used to snap to 0 or 56 px when a
|
|
730
|
+
scroll stopped in its first 56 px.
|
|
731
|
+
|
|
732
|
+
### 0.2.0
|
|
733
|
+
|
|
734
|
+
New `bf-header`: a 44 px toolbar (`pt-safe-top`, `start` | title | `end` slots, `bfSlot="start"` /
|
|
735
|
+
`bfSlot="end"`) and, with `largeTitle` (default `true`), a Meadow Sage large title
|
|
736
|
+
(`text-display`) below it with an optional `subtitle` eyebrow (`text-caption text-text-muted`)
|
|
737
|
+
above the title. Exactly one `h1` carries `data-bf-page-title`, the focus target `bf-stack-outlet`
|
|
738
|
+
moves to after a push — the large title's when `largeTitle` is true, the toolbar's otherwise; with
|
|
739
|
+
`largeTitle`, the toolbar shows a second, `aria-hidden="true"` `span` mirror of the title for the
|
|
740
|
+
visual collapse only. `title` empty projects the default-slot content into whichever `h1` is
|
|
741
|
+
active instead. Reads its sibling scroller from the page's `BfPageContext` (set by `bf-content`)
|
|
742
|
+
through an `effect`: once non-null it attaches a passive `scroll` listener that maps the first
|
|
743
|
+
56 px of scroll to `--bf-header-progress` (0…1, 3 decimals, written to the host only when it
|
|
744
|
+
changes) and detaches on scroller change or destroy. That variable drives the large title's
|
|
745
|
+
opacity and an 8 px upward translate, the toolbar mirror's fade-in and a `divider` hairline that
|
|
746
|
+
fades in with it; the same listener sets the large title block's height to its natural height (measured
|
|
747
|
+
after render and on `ResizeObserver` changes, never per frame) times `1 − progress`, so at
|
|
748
|
+
progress 1 the header is exactly the toolbar. Scrolling schedules no Angular change detection;
|
|
749
|
+
`prefers-reduced-motion` drops the translate. Solid (default), the header is in the page's flow on
|
|
750
|
+
`bg-bg`; `translucent` draws it over the content (`absolute inset-x-0 top-0 z-(--bf-z-sticky)`,
|
|
751
|
+
`bg-bg/85 backdrop-blur`) and publishes its expanded height in `BfPageContext.headerHeight`, which
|
|
752
|
+
`bf-content` pads its scroller by. The large title block is inset by the 16 px screen gutter
|
|
753
|
+
(`px-gutter`, was `px-5`), so it lines up with page content padded the same way.
|
|
754
|
+
|
|
755
|
+
Fixed: the large title no longer sits half-collapsed or collapses during a rubber-band. The
|
|
756
|
+
progress is computed from `scrollTop` clamped to `[0, scrollHeight − clientHeight]`, so overscroll
|
|
757
|
+
past either end never moves the header, and stays 0 on a page that cannot scroll the full 56 px
|
|
758
|
+
(such as a short iOS page, whose body overflows by only 1 px). When a scroll ends part-way
|
|
759
|
+
(`scrollend`, or 120 ms without a `scroll` event, whichever comes first) with no touch or pointer
|
|
760
|
+
down on the scroller (passive `touchstart`/`touchend`/`touchcancel` and
|
|
761
|
+
`pointerdown`/`pointerup`/`pointercancel` listeners), the header scrolls the scroller to 0 below
|
|
762
|
+
half-way, else to 56 px (`behavior: 'smooth'`, `'instant'` under `prefers-reduced-motion`). The
|
|
763
|
+
snap's own scroll never schedules another, and it is skipped while `BfKeyboardService.visible()`
|
|
764
|
+
is true, on a page hidden by `visibility` (`checkVisibility({ visibilityProperty: true })`) and
|
|
765
|
+
after destroy; its timer is cleared on scroller change and destroy. A mouse released outside the
|
|
766
|
+
scroller is heard on the `document` (`pointerup`/`pointercancel`, listened to only while a press
|
|
767
|
+
is tracked), and a press released without a scroll in between (a tap) schedules no snap.
|
|
768
|
+
|
|
769
|
+
Fixed: on a page whose rest scroll range lies between 56 px and 56 px plus the large title's
|
|
770
|
+
height, the header flipped between open and part-collapsed on every frame (measured on a search
|
|
771
|
+
page at 393 × 800: `scrollTop/max@progress` `40/72@0 → 35/35@0.714 → 40/72@0 …`): the collapse made
|
|
772
|
+
the scroller taller, the range fell under 56 and the short-page rule reset the progress. The header
|
|
773
|
+
now writes the collapsed height (`largeHeight × progress` in px, the exact height the block gave
|
|
774
|
+
up; `0px` at rest, when translucent or without a large title) as `--bf-header-collapse` on the
|
|
775
|
+
page's scroller together with `--bf-header-progress`, and removes it on destroy (or from a
|
|
776
|
+
replaced scroller); `bf-content` pads the scroller's bottom by it, so `scrollHeight − clientHeight`
|
|
777
|
+
stays the rest range and the short-page rule and the clamp read one stable value.
|
|
778
|
+
|
|
779
|
+
Fixed: the centred toolbar title sat in the space left between the `start` and `end` slots, so a
|
|
780
|
+
back button or an avatar pushed it off the bar's centre. It is now laid out over the whole toolbar
|
|
781
|
+
row (`absolute`, `pointer-events-none`, truncating) with symmetric insets: the header measures both
|
|
782
|
+
slot containers with its `ResizeObserver` and writes `--bf-header-title-inset` (the 8 px row
|
|
783
|
+
padding + the wider slot's width + 8 px; 16 px without slots) on its host, which the title box
|
|
784
|
+
uses for `left` and `right`.
|
|
785
|
+
|
|
786
|
+
Changed: the bar follows the shell mode (`provideBfShell({ mode })`, the platform's for `'auto'`,
|
|
787
|
+
resolved like `bf-stack-outlet`; the host carries `data-bf-mode`). iOS: a 44 px bar with the
|
|
788
|
+
title (`text-heading font-semibold`, the closest step to UIKit's 17 pt semibold) centred on it.
|
|
789
|
+
Material: a 64 px bar with the title (`text-heading font-normal`, 18 px, the closest step to
|
|
790
|
+
Material's 22 dp regular title) leading after the `start` slot, 16 px from the edge without one,
|
|
791
|
+
never centred. The large title stays left-aligned and collapses the same way in both modes.
|
|
792
|
+
|
|
793
|
+
Changed: scroll-under appearance. The scroll listener also sets `data-bf-scrolled` on the host
|
|
794
|
+
while content is under the bar: with a large title once it has fully collapsed (progress 1; like
|
|
795
|
+
UIKit, the large title is part of the scroll-edge bar, so a page too short to collapse never gets
|
|
796
|
+
it), without one (`largeTitle` false) while `scrollTop` (clamped, so a rubber-band past the top does
|
|
797
|
+
not count) is above 0, on any page length. iOS: opaque `bg-bg` at rest; scrolled under, `bg-bg/85` with `backdrop-blur-xl` and a 1 px
|
|
798
|
+
`border`-coloured hairline under the whole header (the host's `::after`, so the header's height
|
|
799
|
+
never changes; it replaces the `divider` hairline that faded in with the collapse). Material: no
|
|
800
|
+
hairline ever; scrolled under, a `surface-muted` tint with a 150 ms colour transition
|
|
801
|
+
(`motion-safe`) and no blur. `translucent` keeps its meaning (drawn over the content, which
|
|
802
|
+
`bf-content` pads) and now takes the same per-mode surface instead of a permanent
|
|
803
|
+
`bg-bg/85 backdrop-blur`.
|
|
804
|
+
|
|
805
|
+
Changed: the large title's exit is spelled in utilities instead of component CSS (the header has
|
|
806
|
+
no `styles` any more): inside the collapsing clip the block fades (`opacity: 1 − progress`) and
|
|
807
|
+
rises 8 px (`translateY(−8px × progress)`, `motion-safe` only, so reduced motion keeps the fade
|
|
808
|
+
and drops the rise), and the toolbar mirror fades in (`opacity: progress`), all read from
|
|
809
|
+
`--bf-header-progress`, never transitioned.
|