@bitforgehq/ui 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +3665 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -0
  4. package/ai/MANIFEST.index.json +383 -0
  5. package/ai/MANIFEST.json +7528 -0
  6. package/ai/REFERENCE.index.json +1461 -0
  7. package/ai/REFERENCE.json +7213 -0
  8. package/ai/UI_GUIDE.md +1159 -0
  9. package/ai/components/action-bar.md +209 -0
  10. package/ai/components/action-sheet.md +526 -0
  11. package/ai/components/alert-dialog.md +509 -0
  12. package/ai/components/alert.md +376 -0
  13. package/ai/components/avatar.md +146 -0
  14. package/ai/components/back-button.md +286 -0
  15. package/ai/components/badge.md +171 -0
  16. package/ai/components/button.md +431 -0
  17. package/ai/components/card.md +303 -0
  18. package/ai/components/checkbox.md +418 -0
  19. package/ai/components/chip-group.md +205 -0
  20. package/ai/components/chip.md +300 -0
  21. package/ai/components/content.md +474 -0
  22. package/ai/components/desktop-toast.md +420 -0
  23. package/ai/components/dialog.md +962 -0
  24. package/ai/components/empty-state.md +146 -0
  25. package/ai/components/field.md +319 -0
  26. package/ai/components/header.md +809 -0
  27. package/ai/components/icon-button.md +221 -0
  28. package/ai/components/icon.md +176 -0
  29. package/ai/components/input.md +455 -0
  30. package/ai/components/list-item.md +326 -0
  31. package/ai/components/list.md +178 -0
  32. package/ai/components/menu-button.md +377 -0
  33. package/ai/components/menu.md +661 -0
  34. package/ai/components/modal.md +613 -0
  35. package/ai/components/number-input.md +423 -0
  36. package/ai/components/page.md +299 -0
  37. package/ai/components/progress.md +215 -0
  38. package/ai/components/radio-group.md +432 -0
  39. package/ai/components/refresher.md +377 -0
  40. package/ai/components/segmented.md +517 -0
  41. package/ai/components/select.md +510 -0
  42. package/ai/components/sheet.md +845 -0
  43. package/ai/components/spinner.md +171 -0
  44. package/ai/components/stack-outlet.md +661 -0
  45. package/ai/components/tab-bar.md +427 -0
  46. package/ai/components/tab.md +295 -0
  47. package/ai/components/tabs.md +408 -0
  48. package/ai/components/textarea.md +371 -0
  49. package/ai/components/toast.md +522 -0
  50. package/ai/components/toggle.md +387 -0
  51. package/ai/reference/bf-action-directive.md +21 -0
  52. package/ai/reference/bf-action-sheet-action-role.md +27 -0
  53. package/ai/reference/bf-action-sheet-action.md +28 -0
  54. package/ai/reference/bf-action-sheet-options.md +28 -0
  55. package/ai/reference/bf-action-sheet-service.md +33 -0
  56. package/ai/reference/bf-alert-dialog-button-role.md +27 -0
  57. package/ai/reference/bf-alert-dialog-button.md +27 -0
  58. package/ai/reference/bf-alert-dialog-options.md +29 -0
  59. package/ai/reference/bf-alert-dialog-service.md +34 -0
  60. package/ai/reference/bf-alert-variant.md +28 -0
  61. package/ai/reference/bf-autocapitalize.md +30 -0
  62. package/ai/reference/bf-avatar-size.md +27 -0
  63. package/ai/reference/bf-avatar-variant.md +28 -0
  64. package/ai/reference/bf-back-at-root.md +28 -0
  65. package/ai/reference/bf-back-button-service.md +40 -0
  66. package/ai/reference/bf-badge-variant.md +29 -0
  67. package/ai/reference/bf-button-size.md +29 -0
  68. package/ai/reference/bf-button-type.md +28 -0
  69. package/ai/reference/bf-button-variant.md +29 -0
  70. package/ai/reference/bf-card-padding.md +26 -0
  71. package/ai/reference/bf-card-variant.md +28 -0
  72. package/ai/reference/bf-chip-group-mode.md +27 -0
  73. package/ai/reference/bf-chip-variant.md +26 -0
  74. package/ai/reference/bf-compare-with.md +24 -0
  75. package/ai/reference/bf-component-overlay-ref.md +33 -0
  76. package/ai/reference/bf-confirm-button-role.md +26 -0
  77. package/ai/reference/bf-confirm-button.md +27 -0
  78. package/ai/reference/bf-confirm-options.md +31 -0
  79. package/ai/reference/bf-confirm-size.md +26 -0
  80. package/ai/reference/bf-content-scroll-event.md +21 -0
  81. package/ai/reference/bf-defaults-options.md +32 -0
  82. package/ai/reference/bf-defaults.md +28 -0
  83. package/ai/reference/bf-desktop-toast-config.md +28 -0
  84. package/ai/reference/bf-desktop-toast-harness.md +33 -0
  85. package/ai/reference/bf-desktop-toast-options.md +28 -0
  86. package/ai/reference/bf-desktop-toast-position.md +28 -0
  87. package/ai/reference/bf-desktop-toast-service.md +42 -0
  88. package/ai/reference/bf-dialog-config.md +44 -0
  89. package/ai/reference/bf-dialog-harness.md +52 -0
  90. package/ai/reference/bf-dialog-ref.md +44 -0
  91. package/ai/reference/bf-dialog-role.md +26 -0
  92. package/ai/reference/bf-dialog-service.md +55 -0
  93. package/ai/reference/bf-dialog-size.md +31 -0
  94. package/ai/reference/bf-dialog-template-context.md +32 -0
  95. package/ai/reference/bf-empty-state-heading-level.md +26 -0
  96. package/ai/reference/bf-enter-key-hint.md +31 -0
  97. package/ai/reference/bf-field-control-class.md +32 -0
  98. package/ai/reference/bf-field-control-state.md +28 -0
  99. package/ai/reference/bf-field-size.md +29 -0
  100. package/ai/reference/bf-haptics-impact-style.md +27 -0
  101. package/ai/reference/bf-haptics-notification-type.md +27 -0
  102. package/ai/reference/bf-haptics-service.md +41 -0
  103. package/ai/reference/bf-harness-filters.md +29 -0
  104. package/ai/reference/bf-horizontal-scroll-directive.md +129 -0
  105. package/ai/reference/bf-icon-button-variant.md +28 -0
  106. package/ai/reference/bf-icon-size.md +27 -0
  107. package/ai/reference/bf-input-autocapitalize.md +36 -0
  108. package/ai/reference/bf-input-enter-key-hint.md +38 -0
  109. package/ai/reference/bf-input-input-mode.md +38 -0
  110. package/ai/reference/bf-input-mode.md +32 -0
  111. package/ai/reference/bf-input-type.md +35 -0
  112. package/ai/reference/bf-jsdom-shim-options.md +22 -0
  113. package/ai/reference/bf-keyboard-assist-behavior.md +26 -0
  114. package/ai/reference/bf-keyboard-assist-directive.md +185 -0
  115. package/ai/reference/bf-keyboard-phase.md +30 -0
  116. package/ai/reference/bf-keyboard-service.md +50 -0
  117. package/ai/reference/bf-labels-overrides.md +26 -0
  118. package/ai/reference/bf-labels.md +44 -0
  119. package/ai/reference/bf-menu-button-harness.md +40 -0
  120. package/ai/reference/bf-menu-button-variant.md +27 -0
  121. package/ai/reference/bf-menu-divider-directive.md +13 -0
  122. package/ai/reference/bf-menu-group-directive.md +14 -0
  123. package/ai/reference/bf-menu-harness.md +55 -0
  124. package/ai/reference/bf-menu-item-checkbox-directive.md +31 -0
  125. package/ai/reference/bf-menu-item-directive.md +31 -0
  126. package/ai/reference/bf-menu-item-harness.md +52 -0
  127. package/ai/reference/bf-menu-item-radio-directive.md +29 -0
  128. package/ai/reference/bf-menu-placement.md +29 -0
  129. package/ai/reference/bf-menu-trigger-directive.md +46 -0
  130. package/ai/reference/bf-modal-options.md +30 -0
  131. package/ai/reference/bf-modal-presentation.md +26 -0
  132. package/ai/reference/bf-modal-service.md +38 -0
  133. package/ai/reference/bf-nav-controller.md +59 -0
  134. package/ai/reference/bf-nav-direction.md +23 -0
  135. package/ai/reference/bf-nav-target.md +21 -0
  136. package/ai/reference/bf-number-input-mode.md +28 -0
  137. package/ai/reference/bf-number-input-step.md +25 -0
  138. package/ai/reference/bf-overlay-auto-focus.md +29 -0
  139. package/ai/reference/bf-overlay-controller.md +22 -0
  140. package/ai/reference/bf-overlay-host.md +36 -0
  141. package/ai/reference/bf-overlay-options.md +53 -0
  142. package/ai/reference/bf-overlay-position.md +30 -0
  143. package/ai/reference/bf-overlay-ref.md +53 -0
  144. package/ai/reference/bf-overlay-role.md +27 -0
  145. package/ai/reference/bf-overlay-service.md +53 -0
  146. package/ai/reference/bf-overlay-stack-options.md +24 -0
  147. package/ai/reference/bf-overlay-stack.md +81 -0
  148. package/ai/reference/bf-overlay-template-context.md +24 -0
  149. package/ai/reference/bf-overscroll-stretch-directive.md +163 -0
  150. package/ai/reference/bf-page-context.md +24 -0
  151. package/ai/reference/bf-platform-service.md +53 -0
  152. package/ai/reference/bf-platform.md +28 -0
  153. package/ai/reference/bf-progress-size.md +26 -0
  154. package/ai/reference/bf-progress-tone.md +28 -0
  155. package/ai/reference/bf-radio-group-harness.md +49 -0
  156. package/ai/reference/bf-radio-group-orientation.md +26 -0
  157. package/ai/reference/bf-radio-option.md +30 -0
  158. package/ai/reference/bf-refresher-refresh-event.md +22 -0
  159. package/ai/reference/bf-segment-option.md +29 -0
  160. package/ai/reference/bf-segmented-harness.md +48 -0
  161. package/ai/reference/bf-segmented-size.md +29 -0
  162. package/ai/reference/bf-select-harness.md +70 -0
  163. package/ai/reference/bf-select-option.md +29 -0
  164. package/ai/reference/bf-sheet-auto-focus.md +29 -0
  165. package/ai/reference/bf-sheet-options.md +32 -0
  166. package/ai/reference/bf-sheet-ref.md +45 -0
  167. package/ai/reference/bf-sheet-service.md +34 -0
  168. package/ai/reference/bf-shell-config-mode.md +28 -0
  169. package/ai/reference/bf-shell-config.md +38 -0
  170. package/ai/reference/bf-shell-dynamic-type-limit.md +21 -0
  171. package/ai/reference/bf-shell-dynamic-type.md +25 -0
  172. package/ai/reference/bf-shell-mode.md +28 -0
  173. package/ai/reference/bf-shell-testing-options.md +26 -0
  174. package/ai/reference/bf-size.md +45 -0
  175. package/ai/reference/bf-spinner-size.md +27 -0
  176. package/ai/reference/bf-stack-change.md +22 -0
  177. package/ai/reference/bf-stack-reuse-strategy.md +32 -0
  178. package/ai/reference/bf-stack-transition-event.md +26 -0
  179. package/ai/reference/bf-stack-transition.md +26 -0
  180. package/ai/reference/bf-stacked-overlay.md +23 -0
  181. package/ai/reference/bf-system-bars-style.md +38 -0
  182. package/ai/reference/bf-tab-bar-controller.md +51 -0
  183. package/ai/reference/bf-tab-bar-host.md +52 -0
  184. package/ai/reference/bf-tab-context.md +32 -0
  185. package/ai/reference/bf-tab-links.md +37 -0
  186. package/ai/reference/bf-testing-options.md +27 -0
  187. package/ai/reference/bf-text-scale-service.md +35 -0
  188. package/ai/reference/bf-theme.md +27 -0
  189. package/ai/reference/bf-toast-action.md +25 -0
  190. package/ai/reference/bf-toast-controller.md +17 -0
  191. package/ai/reference/bf-toast-dismiss-reason.md +29 -0
  192. package/ai/reference/bf-toast-options.md +29 -0
  193. package/ai/reference/bf-toast-position.md +26 -0
  194. package/ai/reference/bf-toast-ref.md +41 -0
  195. package/ai/reference/bf-toast-service.md +39 -0
  196. package/ai/reference/bf-toggle-label-position.md +26 -0
  197. package/ai/reference/bf-transition-kind.md +28 -0
  198. package/ai/reference/bf-validation-message.md +31 -0
  199. package/ai/reference/bf-validation-messages.md +33 -0
  200. package/ai/reference/bf-view-event.md +28 -0
  201. package/ai/reference/bf-view-lifecycle.md +41 -0
  202. package/ai/reference/bf-view-state.md +28 -0
  203. package/ai/reference/bf_default_labels.md +21 -0
  204. package/ai/reference/bf_default_values.md +21 -0
  205. package/ai/reference/bf_defaults.md +21 -0
  206. package/ai/reference/bf_desktop_toast_defaults.md +21 -0
  207. package/ai/reference/bf_dialog_data.md +17 -0
  208. package/ai/reference/bf_labels.md +21 -0
  209. package/ai/reference/bf_overlay_data.md +19 -0
  210. package/ai/reference/bf_overlay_host.md +21 -0
  211. package/ai/reference/bf_sheet_default_snap_points.md +17 -0
  212. package/ai/reference/bf_shell_config.md +21 -0
  213. package/ai/reference/bf_shell_defaults.md +21 -0
  214. package/ai/reference/bf_tab_bar.md +21 -0
  215. package/ai/reference/bf_tab_bar_controller.md +21 -0
  216. package/ai/reference/bf_tab_context.md +21 -0
  217. package/ai/reference/bf_tab_links.md +21 -0
  218. package/ai/reference/bf_theme_storage.md +19 -0
  219. package/ai/reference/create-transition.md +36 -0
  220. package/ai/reference/duration_fast.md +17 -0
  221. package/ai/reference/duration_md.md +17 -0
  222. package/ai/reference/duration_overlay.md +17 -0
  223. package/ai/reference/duration_push.md +17 -0
  224. package/ai/reference/fade-down.md +26 -0
  225. package/ai/reference/fade-in.md +25 -0
  226. package/ai/reference/fade-out.md +25 -0
  227. package/ai/reference/fade-up.md +26 -0
  228. package/ai/reference/inject-shell-mode.md +23 -0
  229. package/ai/reference/install-bf-jsdom-shims.md +31 -0
  230. package/ai/reference/ios_duration.md +17 -0
  231. package/ai/reference/ios_easing.md +17 -0
  232. package/ai/reference/is-text-entry.md +25 -0
  233. package/ai/reference/max_complete_ms.md +17 -0
  234. package/ai/reference/md-transition.md +31 -0
  235. package/ai/reference/md_duration.md +17 -0
  236. package/ai/reference/md_easing.md +17 -0
  237. package/ai/reference/overlay_duration.md +21 -0
  238. package/ai/reference/overlay_easing.md +19 -0
  239. package/ai/reference/overlay_reduced_duration.md +17 -0
  240. package/ai/reference/pop-frames-at.md +25 -0
  241. package/ai/reference/prefers-reduced-motion.md +25 -0
  242. package/ai/reference/provide-bf-defaults.md +33 -0
  243. package/ai/reference/provide-bf-desktop-toast.md +28 -0
  244. package/ai/reference/provide-bf-labels.md +38 -0
  245. package/ai/reference/provide-bf-shell-testing.md +34 -0
  246. package/ai/reference/provide-bf-shell.md +41 -0
  247. package/ai/reference/provide-bf-testing.md +33 -0
  248. package/ai/reference/reduced-transition.md +28 -0
  249. package/ai/reference/reduced_duration.md +17 -0
  250. package/ai/reference/resolve-shell-mode.md +33 -0
  251. package/ai/reference/scale-in.md +25 -0
  252. package/ai/reference/settle-duration.md +25 -0
  253. package/ai/reference/settle-pop.md +33 -0
  254. package/ai/reference/settle_easing.md +17 -0
  255. package/ai/reference/should-complete-pop.md +26 -0
  256. package/ai/reference/slide-down.md +26 -0
  257. package/ai/reference/slide-up.md +26 -0
  258. package/ai/reference/with-bf-shell-routing.md +19 -0
  259. package/bin/bf-ui.js +81 -0
  260. package/fesm2022/bitforgehq-ui-icons.mjs +230 -0
  261. package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -0
  262. package/fesm2022/bitforgehq-ui-mobile.mjs +5530 -0
  263. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -0
  264. package/fesm2022/bitforgehq-ui-number-input.mjs +680 -0
  265. package/fesm2022/bitforgehq-ui-number-input.mjs.map +1 -0
  266. package/fesm2022/bitforgehq-ui-overlay.mjs +2675 -0
  267. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -0
  268. package/fesm2022/bitforgehq-ui-testing.mjs +402 -0
  269. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -0
  270. package/fesm2022/bitforgehq-ui.mjs +7765 -0
  271. package/fesm2022/bitforgehq-ui.mjs.map +1 -0
  272. package/llms-full.txt +25394 -0
  273. package/llms.txt +329 -0
  274. package/package.json +94 -0
  275. package/theme.css +253 -0
  276. package/types/bitforgehq-ui-icons.d.ts +50 -0
  277. package/types/bitforgehq-ui-mobile.d.ts +2914 -0
  278. package/types/bitforgehq-ui-number-input.d.ts +282 -0
  279. package/types/bitforgehq-ui-overlay.d.ts +1514 -0
  280. package/types/bitforgehq-ui-testing.d.ts +495 -0
  281. package/types/bitforgehq-ui.d.ts +3499 -0
@@ -0,0 +1,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.