@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,377 @@
1
+ # bf-refresher
2
+
3
+ Pull-to-refresh indicator with haptic feedback, native iOS bounce and an Android-style pull
4
+
5
+ `BfRefresherComponent` from `@bitforgehq/ui/mobile` · Navigation · since 0.2.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
11
+ import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
12
+ import {
13
+ BfContentComponent,
14
+ BfHeaderComponent,
15
+ BfPageComponent,
16
+ BfRefresherComponent,
17
+ } from '@bitforgehq/ui/mobile';
18
+
19
+ /** How long the showcase's fake reload takes, so the spinner is visible. */
20
+ export const SHOWCASE_REFRESH_MS = 800;
21
+
22
+ /** `bf-refresher` showcase: pull the list down to "reload", which adds a row at the top. */
23
+ @Component({
24
+ selector: 'app-refresher-showcase',
25
+ imports: [
26
+ BfPageComponent,
27
+ BfHeaderComponent,
28
+ BfContentComponent,
29
+ BfRefresherComponent,
30
+ BfListComponent,
31
+ BfListItemComponent,
32
+ ],
33
+ template: `
34
+ <bf-page data-showcase="refresher">
35
+ <bf-header title="Refresher" [largeTitle]="false" />
36
+ <bf-content>
37
+ <bf-refresher (refreshRequested)="onRefresh($event.complete)" />
38
+ <div class="flex flex-col gap-4 p-4">
39
+ <p class="text-caption text-text-muted">
40
+ Pull the list down past the disc's threshold and let go: it spins, and a new row arrives
41
+ at the top.
42
+ </p>
43
+ <bf-list [inset]="false">
44
+ @for (row of rows(); track row) {
45
+ <bf-list-item [title]="row" />
46
+ }
47
+ </bf-list>
48
+ </div>
49
+ </bf-content>
50
+ </bf-page>
51
+ `,
52
+ })
53
+ export class RefresherShowcasePage {
54
+ private readonly destroyRef = inject(DestroyRef);
55
+
56
+ protected readonly rows = signal<readonly string[]>(['Message 3', 'Message 2', 'Message 1']);
57
+
58
+ /** The pending fake-reload timer, so it can be cancelled if the page is destroyed mid-refresh. */
59
+ private reloadTimeoutId: ReturnType<typeof setTimeout> | undefined;
60
+
61
+ constructor() {
62
+ this.destroyRef.onDestroy(() => {
63
+ if (this.reloadTimeoutId !== undefined) {
64
+ clearTimeout(this.reloadTimeoutId);
65
+ }
66
+ });
67
+ }
68
+
69
+ async onRefresh(complete: () => void): Promise<void> {
70
+ try {
71
+ await new Promise<void>((resolve) => {
72
+ this.reloadTimeoutId = setTimeout(resolve, SHOWCASE_REFRESH_MS);
73
+ });
74
+ this.rows.update((rows) => [`Message ${rows.length + 1}`, ...rows]);
75
+ } finally {
76
+ this.reloadTimeoutId = undefined;
77
+ complete();
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Overview
84
+
85
+ Pull-to-refresh, placed as the first child of `bf-content`. It tracks Pointer Events on the
86
+ page's scroller (read from `BfPageContext`, or its nearest scrolling ancestor when used without one)
87
+ only while the scroller is at `scrollTop === 0`: the disc resists the pull (half the pointer's
88
+ travel, capped) from a resting position above the clipped content to a 64 px threshold, ticks
89
+ `BfHapticsService.impact('light')` once on crossing it, and on release past the threshold
90
+ parks at the top with a spinner and emits `refreshRequested` — call `complete()` on the event to animate
91
+ the disc away and reset. A press by itself changes nothing: only once a pointermove clears an
92
+ 8 px downward slop (out-vertical'ing any horizontal travel) does it claim the gesture and
93
+ capture the pointer on the scroller, so a plain click on the content underneath (a row, a
94
+ button) still reaches it. For touch, a non-passive `touchmove` guard (attached only while a
95
+ press is tracked) keeps the browser from taking a downward drag at the top as a pan, which
96
+ would cancel the pointer stream; upward drags still scroll the list. The disc itself is
97
+ `aria-hidden`; a permanent, visually hidden `role="status"` region announces
98
+ `provideBfLabels()`'s `refreshing` ("Refreshing"). `prefers-reduced-motion` still follows the
99
+ pointer 1:1 during the pull (direct manipulation is not motion); only the settle, park and `complete()` animations
100
+ become instant.
101
+
102
+ That pull is the Material (and mouse/pen) mode. A touch in iOS mode (`BfPlatformService.mode()`)
103
+ is the bounce mode instead: the refresher claims nothing (no capture, no `preventDefault`, no
104
+ guard), the scroller rubber-bands natively, and while the finger is down a negative `scrollTop`
105
+ shows the disc inside the gap the bounce opens, at its resting −48 px, with opacity and scale
106
+ (0.6 → 1) following `−scrollTop / 64` once it passes 8 px; one light haptic at 64 px. Lifting the
107
+ finger at 64 px or more parks the disc and emits `refreshRequested`, like `UIRefreshControl`; a bounce
108
+ with no finger down (a fling's overshoot) never shows it. The host carries `data-bf-refresher`
109
+ while enabled, so `bfOverscrollStretch` leaves the top edge to it.
110
+
111
+ Pull-to-refresh. `bf-refresher` goes inside `bf-content`, as its first child: it tracks Pointer
112
+ Events on the page's scroller and, only while the scroller is at the very top, resists a downward
113
+ pull into a 40 px disc above the content. A plain press changes nothing — only once a move clears
114
+ an 8 px downward slop does it claim the gesture, and only then does it capture the pointer on the
115
+ scroller (so the gesture keeps tracking even if the pointer strays outside the scroller mid-drag)
116
+ and the disc becomes visible. Pull past the 64 px threshold and release, and it parks the disc at
117
+ the top with a spinner and emits `refreshRequested` — call `complete()` on the event once the reload is
118
+ done to animate the disc away and reset.
119
+
120
+ ## When to use
121
+
122
+ Use `bf-refresher` as the first child of a [bf-content](https://bitforge-ui.web.app/components/content) whose content the user reloads on a phone: an inbox, a feed. Use a Refresh [bf-button](https://bitforge-ui.web.app/components/button) instead on desktop, and add one next to the refresher for keyboard and switch-control users.
123
+
124
+ ## Basic usage
125
+
126
+ ```ts
127
+ import { Component, signal } from '@angular/core';
128
+ import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
129
+ import {
130
+ BfContentComponent,
131
+ BfHeaderComponent,
132
+ BfPageComponent,
133
+ BfRefresherComponent,
134
+ } from '@bitforgehq/ui/mobile';
135
+
136
+ /**
137
+ * Pull to refresh.
138
+ *
139
+ * `refreshRequested` hands over `complete`; call it once the reload has finished.
140
+ *
141
+ * @preview none
142
+ */
143
+ @Component({
144
+ selector: 'app-example-refresher-basic',
145
+ imports: [
146
+ BfContentComponent,
147
+ BfHeaderComponent,
148
+ BfListComponent,
149
+ BfListItemComponent,
150
+ BfPageComponent,
151
+ BfRefresherComponent,
152
+ ],
153
+ template: `
154
+ <bf-page>
155
+ <bf-header title="Inbox" />
156
+ <bf-content>
157
+ <bf-refresher (refreshRequested)="onRefresh($event.complete)" />
158
+ <bf-list>
159
+ @for (message of messages(); track message) {
160
+ <bf-list-item [title]="message" />
161
+ }
162
+ </bf-list>
163
+ </bf-content>
164
+ </bf-page>
165
+ `,
166
+ })
167
+ export class RefresherBasicExampleComponent {
168
+ protected readonly messages = signal(['Welcome']);
169
+
170
+ async onRefresh(complete: () => void): Promise<void> {
171
+ await this.reload();
172
+ complete();
173
+ }
174
+
175
+ /** Stands in for the inbox state's reload. */
176
+ private async reload(): Promise<void> {
177
+ await new Promise((resolve) => setTimeout(resolve, 800));
178
+ this.messages.update((messages) => [`New message ${messages.length}`, ...messages]);
179
+ }
180
+ }
181
+ ```
182
+
183
+ ## iOS: riding the native bounce
184
+
185
+ In iOS mode (`BfPlatformService.mode() === 'ios'`) a touch works like `UIRefreshControl`. The
186
+ refresher claims nothing — no pointer capture, no `preventDefault`, no `touchmove` guard — so the
187
+ content rubber-bands natively (`bf-content` keeps even a short page scrollable by 1 px, so it always
188
+ bounces). While a finger is down it watches the scroller's `scroll` events: once the bounce opens
189
+ more than 8 px above the content, the disc shows inside that gap, at its resting position just
190
+ above the content and moving with it, with opacity and scale (0.6 → 1) following the pull up to the
191
+ 64 px threshold, where one light haptic ticks. Lift the finger at 64 px or more and the disc
192
+ settles onto the content top while the content snaps back, and `refreshRequested` emits; lift it earlier and
193
+ the disc hides. A bounce with no finger down — the overshoot of a fling — never shows the disc.
194
+
195
+ This follows the engine (`BfPlatformService.mode()`, iOS only on native iOS), not
196
+ `provideBfShell({ mode })`: the bounce mode claims nothing and relies on WebKit reporting a negative
197
+ `scrollTop` while it rubber-bands. Chrome never reports a negative `scrollTop`, so an Android app
198
+ forced into `mode: 'ios'` keeps the material pull; with the shell mode, its pull-to-refresh would
199
+ stop working.
200
+
201
+ ## Android and mouse: the material pull
202
+
203
+ Everywhere else — Material mode, and a mouse or pen in iOS mode (the desktop docs preview) — the
204
+ refresher drives the pull itself, as described above: the slop, the pointer capture, the resisted
205
+ disc and the park.
206
+
207
+ ## Clicks pass through
208
+
209
+ A tap or click that never moves past the slop is left completely alone — the row or button
210
+ underneath it still receives its own `click`. `bf-refresher` only starts intercepting the pointer
211
+ once a move proves it's a downward pull, not a press on the content beneath it.
212
+
213
+ ## Touch
214
+
215
+ In Material mode a phone's scroller is `touch-pan-y`, so the browser would take a downward drag as
216
+ its own pan and cancel the pointer stream. While a press is tracked, `bf-refresher` adds a non-passive
217
+ `touchmove` listener to the scroller that prevents a downward drag at the top (and every move once
218
+ the pull is claimed); upward drags are never prevented, so the list scrolls as usual. The row under
219
+ the finger losing its implicit pointer capture when the scroller claims the pull does not end it.
220
+
221
+ ## The `complete()` contract
222
+
223
+ `refreshRequested` emits once per completed pull, with a `BfRefresherRefreshEvent`
224
+ (`{ complete: () => void }`); the 0.2 name `refresh` still emits the same event until 0.4.0. The disc stays parked
225
+ with its spinner spinning — and further pulls are ignored — until `complete()` is called; call it
226
+ whether the reload succeeded or failed, or the disc never leaves the top. `complete()` animates the
227
+ disc back to its resting position above the content and only then does another pull start working
228
+ again.
229
+
230
+ ## Disabling
231
+
232
+ ```ts
233
+ import { Component, signal } from '@angular/core';
234
+ import { BfContentComponent, BfPageComponent, BfRefresherComponent } from '@bitforgehq/ui/mobile';
235
+
236
+ /**
237
+ * A refresher that is off while the page is read-only.
238
+ *
239
+ * @preview none
240
+ */
241
+ @Component({
242
+ selector: 'app-example-refresher-disabled',
243
+ imports: [BfContentComponent, BfPageComponent, BfRefresherComponent],
244
+ template: `
245
+ <bf-page>
246
+ <bf-content>
247
+ <bf-refresher [disabled]="isReadOnly()" />
248
+ <p class="text-body p-4">An archived project cannot be refreshed.</p>
249
+ </bf-content>
250
+ </bf-page>
251
+ `,
252
+ })
253
+ export class RefresherDisabledExampleComponent {
254
+ protected readonly isReadOnly = signal(true);
255
+ }
256
+ ```
257
+
258
+ `disabled` stops pull tracking entirely; the disc never moves and `refreshRequested` never emits. It has no
259
+ effect on a pull already in progress — flip it before the next `pointerdown`.
260
+
261
+ ## Reduced motion
262
+
263
+ The disc still follows the pointer 1:1 (resisted) during the pull under
264
+ `prefers-reduced-motion: reduce` — direct manipulation tied to the pointer's own position isn't
265
+ motion. What changes is the parts driven by the Web Animations API: parking the disc when a pull
266
+ completes, `complete()`'s dismissal, and settling back on a pull that didn't reach the threshold
267
+ all become instant jumps instead of animated transitions. The `refreshing` state and the `refreshRequested`
268
+ event fire exactly the same either way.
269
+
270
+ ## Desktop
271
+
272
+ A mouse can drag the pull (the Material pull runs for a mouse or pen), but desktop users do not expect pull-to-refresh and a wheel or trackpad scroll never starts it. On desktop, offer a Refresh [bf-button](https://bitforge-ui.web.app/components/button) in the toolbar that calls the same reload.
273
+
274
+ ## Theming
275
+
276
+ - Disc: `bg-surface`, `shadow-raised`, `rounded-full`, 40 × 40 px; `visibility: hidden` at rest so
277
+ its shadow never bleeds above the content, shown only once a pull is claimed (or, on iOS, once
278
+ the bounce opens more than 8 px).
279
+ - Host: carries `data-bf-refresher` while enabled, so `bf-content`'s Android edge stretch leaves
280
+ the top edge to the refresher.
281
+ - Spinner: `bf-spinner` inherits `currentColor`; set `text-primary` on an ancestor to color it,
282
+ the same as any other spinner usage.
283
+ - No background or border of its own beyond the disc — it never changes the scroller's
284
+ `overscroll-behavior`.
285
+
286
+ ## Accessibility
287
+
288
+ A visually hidden `role="status"` region with `aria-live="polite"` is always in the accessibility
289
+ tree: empty at rest, it reads `provideBfLabels()`'s `refreshing` ("Refreshing") while a refresh runs, so the change is announced. The
290
+ disc and its spinner are `aria-hidden="true"` — purely visual, with no status of their own. The pull gesture itself has no
291
+ keyboard equivalent — wire a manual refresh action (e.g. a toolbar button calling the same reload
292
+ method) for keyboard and switch-control users.
293
+
294
+ ## Inputs
295
+
296
+ | Name | Type | Default | Description |
297
+ | ---------- | --------- | ------- | -------------------------------------------------------------------------------- |
298
+ | `disabled` | `boolean` | `false` | Disables pull tracking; the disc never moves and `refreshRequested` never emits. |
299
+
300
+ ## Outputs
301
+
302
+ | Name | Payload | Description |
303
+ | ------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
304
+ | `refreshRequested` | `BfRefresherRefreshEvent` | Emitted when a pull past the threshold is released; call `complete()` to end it. |
305
+ | `refresh` | `BfRefresherRefreshEvent` | Emitted with the same event as `refreshRequested`, together with it. **Deprecated** (0.3.0 use refreshRequested; removed in 0.4.0) |
306
+
307
+ ## Methods
308
+
309
+ ### `complete(): void`
310
+
311
+ Ends a refresh: animates the disc away (skipped under reduced motion) and resets state.
312
+
313
+ ## Notes
314
+
315
+ ### Do
316
+
317
+ - Place exactly one `bf-refresher` as the first child of `bf-content`.
318
+ - Always call `complete()` from the `refreshRequested` handler, including on a failed reload.
319
+ - Leave `overscroll-behavior` alone; `bf-refresher` depends on `bf-content`'s `overscroll-contain`.
320
+
321
+ ### Don't
322
+
323
+ - Don't put `bf-refresher` anywhere the page can scroll away from the top on load — a pull only
324
+ starts at `scrollTop === 0`.
325
+ - Don't provide a second way to trigger the same pull gesture; give keyboard users a real button
326
+ instead of relying on the pull.
327
+
328
+ ## Migration notes
329
+
330
+ ### 0.3.0
331
+
332
+ **Deprecated:** `refresh` → `refreshRequested`, typed with the new exported
333
+ `BfRefresherRefreshEvent` (`{ complete: () => void }`, was an inline object type). Both outputs
334
+ emit the same event object for each completed pull until `refresh` is removed in 0.4.0 (see
335
+ Migration 23).
336
+
337
+ **Behaviour:** the status region announces `provideBfLabels()`'s `refreshing` ("Refreshing").
338
+
339
+ ### 0.2.0
340
+
341
+ New `bf-refresher`: pull-to-refresh, placed as the first child of `bf-content`. It tracks Pointer
342
+ Events on the page's scroller (read from `BfPageContext`, falling back to its own parent element)
343
+ only while `scrollTop === 0`. A press by itself changes nothing; only once a move clears an 8 px
344
+ downward slop (out-vertical'ing any horizontal travel) does it claim the gesture, capture the
345
+ pointer on the scroller, and start resisting the drag at `delta * 0.5` (capped at 72 px) into a
346
+ 40 px `bg-surface shadow-raised rounded-full` disc holding a `bf-spinner`, from a resting −48 px
347
+ up to a 64 px threshold — so a plain click on the content underneath (a row, a button) still
348
+ reaches it. Crossing the threshold ticks `BfHapticsService.impact('light')` once; releasing past
349
+ it sets `refreshing` (a readonly signal), parks the disc at 0 and emits `refresh` with `{
350
+ complete: () => void }` — releasing before the threshold animates the disc back to rest instead.
351
+ `complete()` animates the disc away and clears `refreshing`, after which the next pull starts
352
+ working again; `disabled` stops tracking pulls entirely, and further pulls are ignored while
353
+ already `refreshing`. The park/settle/`complete()` transitions use the Web Animations API;
354
+ `prefers-reduced-motion` only skips those — the disc still tracks the pointer 1:1 during the pull
355
+ itself, since direct manipulation isn't motion. For touch, a non-passive `touchmove` guard,
356
+ attached only while a press is tracked, prevents a downward drag at the top (and every move once
357
+ claimed) so the browser does not take it as a pan and cancel the pointer stream; upward drags keep
358
+ scrolling. Only the scroller's own `lostpointercapture` ends a pull, not the bubbling one from the
359
+ row that held the touch's implicit capture. The disc (with its spinner) is `aria-hidden` and
360
+ `visibility: hidden` until a pull is claimed (so its shadow never bleeds above the content); a
361
+ separate, permanent visually hidden `role="status" aria-live="polite"` region is empty at rest and
362
+ reads "Refreshing" while a refresh runs. It never touches the scroller's `overscroll-behavior`.
363
+
364
+ Changed: on iOS a touch rides the native bounce. In iOS mode (`BfPlatformService.mode()`) a touch
365
+ press (`pointerType === 'touch'`, or a `touchstart`, tracked with passive listeners) is not
366
+ claimed — no pointer capture, no `preventDefault`, no `touchmove` guard — so the scroller
367
+ rubber-bands natively. While a finger is down, passive `scroll` events with a negative `scrollTop`
368
+ show the disc at its resting −48 px, inside the gap the bounce opens, once `−scrollTop > 8`, with
369
+ `opacity` = progress and `scale(0.6 + 0.4 × progress)` (progress = `min(1, −scrollTop / 64)`) and
370
+ one light haptic at 1. Lifting the last finger at `−scrollTop ≥ 64` sets `refreshing`, settles the
371
+ disc to 0 and emits `refresh`; otherwise the disc hides. A negative `scrollTop` with no finger down
372
+ (a fling's overshoot) never shows it. Material mode and a mouse or pen in iOS mode keep the pull
373
+ unchanged. The host carries `data-bf-refresher` while not `disabled`. A pen press in iOS mode,
374
+ which also fires touch events, never starts a bounce, so its trailing `touchend` cannot hide the
375
+ parked disc; the bounce writes the disc's styles only when it shows, changes or hides, not on
376
+ every scroll; and a scroller change or destroy mid-bounce resets the disc's `opacity`, `transform`
377
+ and `visibility`.