@bitforgehq/ui 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3665 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/ai/MANIFEST.index.json +383 -0
- package/ai/MANIFEST.json +7528 -0
- package/ai/REFERENCE.index.json +1461 -0
- package/ai/REFERENCE.json +7213 -0
- package/ai/UI_GUIDE.md +1159 -0
- package/ai/components/action-bar.md +209 -0
- package/ai/components/action-sheet.md +526 -0
- package/ai/components/alert-dialog.md +509 -0
- package/ai/components/alert.md +376 -0
- package/ai/components/avatar.md +146 -0
- package/ai/components/back-button.md +286 -0
- package/ai/components/badge.md +171 -0
- package/ai/components/button.md +431 -0
- package/ai/components/card.md +303 -0
- package/ai/components/checkbox.md +418 -0
- package/ai/components/chip-group.md +205 -0
- package/ai/components/chip.md +300 -0
- package/ai/components/content.md +474 -0
- package/ai/components/desktop-toast.md +420 -0
- package/ai/components/dialog.md +962 -0
- package/ai/components/empty-state.md +146 -0
- package/ai/components/field.md +319 -0
- package/ai/components/header.md +809 -0
- package/ai/components/icon-button.md +221 -0
- package/ai/components/icon.md +176 -0
- package/ai/components/input.md +455 -0
- package/ai/components/list-item.md +326 -0
- package/ai/components/list.md +178 -0
- package/ai/components/menu-button.md +377 -0
- package/ai/components/menu.md +661 -0
- package/ai/components/modal.md +613 -0
- package/ai/components/number-input.md +423 -0
- package/ai/components/page.md +299 -0
- package/ai/components/progress.md +215 -0
- package/ai/components/radio-group.md +432 -0
- package/ai/components/refresher.md +377 -0
- package/ai/components/segmented.md +517 -0
- package/ai/components/select.md +510 -0
- package/ai/components/sheet.md +845 -0
- package/ai/components/spinner.md +171 -0
- package/ai/components/stack-outlet.md +661 -0
- package/ai/components/tab-bar.md +427 -0
- package/ai/components/tab.md +295 -0
- package/ai/components/tabs.md +408 -0
- package/ai/components/textarea.md +371 -0
- package/ai/components/toast.md +522 -0
- package/ai/components/toggle.md +387 -0
- package/ai/reference/bf-action-directive.md +21 -0
- package/ai/reference/bf-action-sheet-action-role.md +27 -0
- package/ai/reference/bf-action-sheet-action.md +28 -0
- package/ai/reference/bf-action-sheet-options.md +28 -0
- package/ai/reference/bf-action-sheet-service.md +33 -0
- package/ai/reference/bf-alert-dialog-button-role.md +27 -0
- package/ai/reference/bf-alert-dialog-button.md +27 -0
- package/ai/reference/bf-alert-dialog-options.md +29 -0
- package/ai/reference/bf-alert-dialog-service.md +34 -0
- package/ai/reference/bf-alert-variant.md +28 -0
- package/ai/reference/bf-autocapitalize.md +30 -0
- package/ai/reference/bf-avatar-size.md +27 -0
- package/ai/reference/bf-avatar-variant.md +28 -0
- package/ai/reference/bf-back-at-root.md +28 -0
- package/ai/reference/bf-back-button-service.md +40 -0
- package/ai/reference/bf-badge-variant.md +29 -0
- package/ai/reference/bf-button-size.md +29 -0
- package/ai/reference/bf-button-type.md +28 -0
- package/ai/reference/bf-button-variant.md +29 -0
- package/ai/reference/bf-card-padding.md +26 -0
- package/ai/reference/bf-card-variant.md +28 -0
- package/ai/reference/bf-chip-group-mode.md +27 -0
- package/ai/reference/bf-chip-variant.md +26 -0
- package/ai/reference/bf-compare-with.md +24 -0
- package/ai/reference/bf-component-overlay-ref.md +33 -0
- package/ai/reference/bf-confirm-button-role.md +26 -0
- package/ai/reference/bf-confirm-button.md +27 -0
- package/ai/reference/bf-confirm-options.md +31 -0
- package/ai/reference/bf-confirm-size.md +26 -0
- package/ai/reference/bf-content-scroll-event.md +21 -0
- package/ai/reference/bf-defaults-options.md +32 -0
- package/ai/reference/bf-defaults.md +28 -0
- package/ai/reference/bf-desktop-toast-config.md +28 -0
- package/ai/reference/bf-desktop-toast-harness.md +33 -0
- package/ai/reference/bf-desktop-toast-options.md +28 -0
- package/ai/reference/bf-desktop-toast-position.md +28 -0
- package/ai/reference/bf-desktop-toast-service.md +42 -0
- package/ai/reference/bf-dialog-config.md +44 -0
- package/ai/reference/bf-dialog-harness.md +52 -0
- package/ai/reference/bf-dialog-ref.md +44 -0
- package/ai/reference/bf-dialog-role.md +26 -0
- package/ai/reference/bf-dialog-service.md +55 -0
- package/ai/reference/bf-dialog-size.md +31 -0
- package/ai/reference/bf-dialog-template-context.md +32 -0
- package/ai/reference/bf-empty-state-heading-level.md +26 -0
- package/ai/reference/bf-enter-key-hint.md +31 -0
- package/ai/reference/bf-field-control-class.md +32 -0
- package/ai/reference/bf-field-control-state.md +28 -0
- package/ai/reference/bf-field-size.md +29 -0
- package/ai/reference/bf-haptics-impact-style.md +27 -0
- package/ai/reference/bf-haptics-notification-type.md +27 -0
- package/ai/reference/bf-haptics-service.md +41 -0
- package/ai/reference/bf-harness-filters.md +29 -0
- package/ai/reference/bf-horizontal-scroll-directive.md +129 -0
- package/ai/reference/bf-icon-button-variant.md +28 -0
- package/ai/reference/bf-icon-size.md +27 -0
- package/ai/reference/bf-input-autocapitalize.md +36 -0
- package/ai/reference/bf-input-enter-key-hint.md +38 -0
- package/ai/reference/bf-input-input-mode.md +38 -0
- package/ai/reference/bf-input-mode.md +32 -0
- package/ai/reference/bf-input-type.md +35 -0
- package/ai/reference/bf-jsdom-shim-options.md +22 -0
- package/ai/reference/bf-keyboard-assist-behavior.md +26 -0
- package/ai/reference/bf-keyboard-assist-directive.md +185 -0
- package/ai/reference/bf-keyboard-phase.md +30 -0
- package/ai/reference/bf-keyboard-service.md +50 -0
- package/ai/reference/bf-labels-overrides.md +26 -0
- package/ai/reference/bf-labels.md +44 -0
- package/ai/reference/bf-menu-button-harness.md +40 -0
- package/ai/reference/bf-menu-button-variant.md +27 -0
- package/ai/reference/bf-menu-divider-directive.md +13 -0
- package/ai/reference/bf-menu-group-directive.md +14 -0
- package/ai/reference/bf-menu-harness.md +55 -0
- package/ai/reference/bf-menu-item-checkbox-directive.md +31 -0
- package/ai/reference/bf-menu-item-directive.md +31 -0
- package/ai/reference/bf-menu-item-harness.md +52 -0
- package/ai/reference/bf-menu-item-radio-directive.md +29 -0
- package/ai/reference/bf-menu-placement.md +29 -0
- package/ai/reference/bf-menu-trigger-directive.md +46 -0
- package/ai/reference/bf-modal-options.md +30 -0
- package/ai/reference/bf-modal-presentation.md +26 -0
- package/ai/reference/bf-modal-service.md +38 -0
- package/ai/reference/bf-nav-controller.md +59 -0
- package/ai/reference/bf-nav-direction.md +23 -0
- package/ai/reference/bf-nav-target.md +21 -0
- package/ai/reference/bf-number-input-mode.md +28 -0
- package/ai/reference/bf-number-input-step.md +25 -0
- package/ai/reference/bf-overlay-auto-focus.md +29 -0
- package/ai/reference/bf-overlay-controller.md +22 -0
- package/ai/reference/bf-overlay-host.md +36 -0
- package/ai/reference/bf-overlay-options.md +53 -0
- package/ai/reference/bf-overlay-position.md +30 -0
- package/ai/reference/bf-overlay-ref.md +53 -0
- package/ai/reference/bf-overlay-role.md +27 -0
- package/ai/reference/bf-overlay-service.md +53 -0
- package/ai/reference/bf-overlay-stack-options.md +24 -0
- package/ai/reference/bf-overlay-stack.md +81 -0
- package/ai/reference/bf-overlay-template-context.md +24 -0
- package/ai/reference/bf-overscroll-stretch-directive.md +163 -0
- package/ai/reference/bf-page-context.md +24 -0
- package/ai/reference/bf-platform-service.md +53 -0
- package/ai/reference/bf-platform.md +28 -0
- package/ai/reference/bf-progress-size.md +26 -0
- package/ai/reference/bf-progress-tone.md +28 -0
- package/ai/reference/bf-radio-group-harness.md +49 -0
- package/ai/reference/bf-radio-group-orientation.md +26 -0
- package/ai/reference/bf-radio-option.md +30 -0
- package/ai/reference/bf-refresher-refresh-event.md +22 -0
- package/ai/reference/bf-segment-option.md +29 -0
- package/ai/reference/bf-segmented-harness.md +48 -0
- package/ai/reference/bf-segmented-size.md +29 -0
- package/ai/reference/bf-select-harness.md +70 -0
- package/ai/reference/bf-select-option.md +29 -0
- package/ai/reference/bf-sheet-auto-focus.md +29 -0
- package/ai/reference/bf-sheet-options.md +32 -0
- package/ai/reference/bf-sheet-ref.md +45 -0
- package/ai/reference/bf-sheet-service.md +34 -0
- package/ai/reference/bf-shell-config-mode.md +28 -0
- package/ai/reference/bf-shell-config.md +38 -0
- package/ai/reference/bf-shell-dynamic-type-limit.md +21 -0
- package/ai/reference/bf-shell-dynamic-type.md +25 -0
- package/ai/reference/bf-shell-mode.md +28 -0
- package/ai/reference/bf-shell-testing-options.md +26 -0
- package/ai/reference/bf-size.md +45 -0
- package/ai/reference/bf-spinner-size.md +27 -0
- package/ai/reference/bf-stack-change.md +22 -0
- package/ai/reference/bf-stack-reuse-strategy.md +32 -0
- package/ai/reference/bf-stack-transition-event.md +26 -0
- package/ai/reference/bf-stack-transition.md +26 -0
- package/ai/reference/bf-stacked-overlay.md +23 -0
- package/ai/reference/bf-system-bars-style.md +38 -0
- package/ai/reference/bf-tab-bar-controller.md +51 -0
- package/ai/reference/bf-tab-bar-host.md +52 -0
- package/ai/reference/bf-tab-context.md +32 -0
- package/ai/reference/bf-tab-links.md +37 -0
- package/ai/reference/bf-testing-options.md +27 -0
- package/ai/reference/bf-text-scale-service.md +35 -0
- package/ai/reference/bf-theme.md +27 -0
- package/ai/reference/bf-toast-action.md +25 -0
- package/ai/reference/bf-toast-controller.md +17 -0
- package/ai/reference/bf-toast-dismiss-reason.md +29 -0
- package/ai/reference/bf-toast-options.md +29 -0
- package/ai/reference/bf-toast-position.md +26 -0
- package/ai/reference/bf-toast-ref.md +41 -0
- package/ai/reference/bf-toast-service.md +39 -0
- package/ai/reference/bf-toggle-label-position.md +26 -0
- package/ai/reference/bf-transition-kind.md +28 -0
- package/ai/reference/bf-validation-message.md +31 -0
- package/ai/reference/bf-validation-messages.md +33 -0
- package/ai/reference/bf-view-event.md +28 -0
- package/ai/reference/bf-view-lifecycle.md +41 -0
- package/ai/reference/bf-view-state.md +28 -0
- package/ai/reference/bf_default_labels.md +21 -0
- package/ai/reference/bf_default_values.md +21 -0
- package/ai/reference/bf_defaults.md +21 -0
- package/ai/reference/bf_desktop_toast_defaults.md +21 -0
- package/ai/reference/bf_dialog_data.md +17 -0
- package/ai/reference/bf_labels.md +21 -0
- package/ai/reference/bf_overlay_data.md +19 -0
- package/ai/reference/bf_overlay_host.md +21 -0
- package/ai/reference/bf_sheet_default_snap_points.md +17 -0
- package/ai/reference/bf_shell_config.md +21 -0
- package/ai/reference/bf_shell_defaults.md +21 -0
- package/ai/reference/bf_tab_bar.md +21 -0
- package/ai/reference/bf_tab_bar_controller.md +21 -0
- package/ai/reference/bf_tab_context.md +21 -0
- package/ai/reference/bf_tab_links.md +21 -0
- package/ai/reference/bf_theme_storage.md +19 -0
- package/ai/reference/create-transition.md +36 -0
- package/ai/reference/duration_fast.md +17 -0
- package/ai/reference/duration_md.md +17 -0
- package/ai/reference/duration_overlay.md +17 -0
- package/ai/reference/duration_push.md +17 -0
- package/ai/reference/fade-down.md +26 -0
- package/ai/reference/fade-in.md +25 -0
- package/ai/reference/fade-out.md +25 -0
- package/ai/reference/fade-up.md +26 -0
- package/ai/reference/inject-shell-mode.md +23 -0
- package/ai/reference/install-bf-jsdom-shims.md +31 -0
- package/ai/reference/ios_duration.md +17 -0
- package/ai/reference/ios_easing.md +17 -0
- package/ai/reference/is-text-entry.md +25 -0
- package/ai/reference/max_complete_ms.md +17 -0
- package/ai/reference/md-transition.md +31 -0
- package/ai/reference/md_duration.md +17 -0
- package/ai/reference/md_easing.md +17 -0
- package/ai/reference/overlay_duration.md +21 -0
- package/ai/reference/overlay_easing.md +19 -0
- package/ai/reference/overlay_reduced_duration.md +17 -0
- package/ai/reference/pop-frames-at.md +25 -0
- package/ai/reference/prefers-reduced-motion.md +25 -0
- package/ai/reference/provide-bf-defaults.md +33 -0
- package/ai/reference/provide-bf-desktop-toast.md +28 -0
- package/ai/reference/provide-bf-labels.md +38 -0
- package/ai/reference/provide-bf-shell-testing.md +34 -0
- package/ai/reference/provide-bf-shell.md +41 -0
- package/ai/reference/provide-bf-testing.md +33 -0
- package/ai/reference/reduced-transition.md +28 -0
- package/ai/reference/reduced_duration.md +17 -0
- package/ai/reference/resolve-shell-mode.md +33 -0
- package/ai/reference/scale-in.md +25 -0
- package/ai/reference/settle-duration.md +25 -0
- package/ai/reference/settle-pop.md +33 -0
- package/ai/reference/settle_easing.md +17 -0
- package/ai/reference/should-complete-pop.md +26 -0
- package/ai/reference/slide-down.md +26 -0
- package/ai/reference/slide-up.md +26 -0
- package/ai/reference/with-bf-shell-routing.md +19 -0
- package/bin/bf-ui.js +81 -0
- package/fesm2022/bitforgehq-ui-icons.mjs +230 -0
- package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs +5530 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs +680 -0
- package/fesm2022/bitforgehq-ui-number-input.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs +2675 -0
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-testing.mjs +402 -0
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui.mjs +7765 -0
- package/fesm2022/bitforgehq-ui.mjs.map +1 -0
- package/llms-full.txt +25394 -0
- package/llms.txt +329 -0
- package/package.json +94 -0
- package/theme.css +253 -0
- package/types/bitforgehq-ui-icons.d.ts +50 -0
- package/types/bitforgehq-ui-mobile.d.ts +2914 -0
- package/types/bitforgehq-ui-number-input.d.ts +282 -0
- package/types/bitforgehq-ui-overlay.d.ts +1514 -0
- package/types/bitforgehq-ui-testing.d.ts +495 -0
- package/types/bitforgehq-ui.d.ts +3499 -0
package/ai/UI_GUIDE.md
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
<!-- GENERATED by tools/gen-ai-docs.ts from tools/ui-guide.template.md and the component sources. Do not edit by hand; run `bun run ai:docs`. -->
|
|
2
|
+
|
|
3
|
+
# @bitforgehq/ui — guide for implementer agents
|
|
4
|
+
|
|
5
|
+
You are working in an app that uses `@bitforgehq/ui`, Bitforge's shared Angular 22 component
|
|
6
|
+
library (Meadow Sage design, Tailwind CSS v4, signals-first). **Before creating any UI primitive
|
|
7
|
+
(button, input, card, list, badge, …), check the component index below and use the library
|
|
8
|
+
component.** Per-component pages with the full API, examples and dos/don'ts are in
|
|
9
|
+
`node_modules/@bitforgehq/ui/ai/components/<name>.md`; the machine-readable version is
|
|
10
|
+
`node_modules/@bitforgehq/ui/ai/MANIFEST.json`.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
### 1. Check the prerequisites
|
|
15
|
+
|
|
16
|
+
Node 22.22.3 or later in the 22 line, 24.15 or later in the 24 line, or 26 and later (the Angular CLI 22 range `^22.22.3 || ^24.15.0 || >=26.0.0`; it refuses older versions and warns on odd releases such as 23 and 25), Bun 1.3 or later and the Angular CLI 22.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node --version # v22.22.3+, v24.15+ or v26+
|
|
20
|
+
bun --version # 1.3 or later
|
|
21
|
+
bun add --global @angular/cli@22
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Create the app
|
|
25
|
+
|
|
26
|
+
A new Angular 22 app with plain CSS styles. Skip this step in an existing app.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ng new my-app --style css --package-manager bun
|
|
30
|
+
cd my-app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Add Tailwind CSS
|
|
34
|
+
|
|
35
|
+
The theme is a Tailwind CSS 4 theme. `ng add tailwindcss` installs Tailwind and its PostCSS plugin and writes `.postcssrc.json`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ng add tailwindcss
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 4. Add the packages
|
|
42
|
+
|
|
43
|
+
`lucide` provides the icon data for `bf-icon`; the two `@fontsource` packages provide the Manrope and JetBrains Mono fonts.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun add @bitforgehq/ui lucide @fontsource/manrope @fontsource/jetbrains-mono
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 5. Wire the theme into styles.css
|
|
50
|
+
|
|
51
|
+
Replace the contents of `src/styles.css`. Keep `tailwindcss` first: the theme redefines the palette its utilities read, and registers the package as a Tailwind source, so no `tailwind.config.js` is needed.
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
@import 'tailwindcss';
|
|
55
|
+
@import '@bitforgehq/ui/theme.css';
|
|
56
|
+
@import '@fontsource/manrope/500.css';
|
|
57
|
+
@import '@fontsource/manrope/600.css';
|
|
58
|
+
@import '@fontsource/manrope/700.css';
|
|
59
|
+
@import '@fontsource/manrope/800.css';
|
|
60
|
+
@import '@fontsource/jetbrains-mono/400.css';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. Theme the page
|
|
64
|
+
|
|
65
|
+
In `src/index.html`, give `<body>` the theme background and text colour, so the page itself follows light and dark mode.
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<body class="bg-bg text-text">
|
|
69
|
+
<app-root></app-root>
|
|
70
|
+
</body>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 7. Switch to dark mode
|
|
74
|
+
|
|
75
|
+
Add the `dark` class to `<html>` (and remove it for light mode). Every token is redefined under `.dark`, and the theme sets `color-scheme`, so scrollbars and native controls follow too. Components need no `dark:` classes.
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<html lang="en" class="dark">
|
|
79
|
+
<!-- head and body stay as they are -->
|
|
80
|
+
</html>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 8. Use a first component
|
|
84
|
+
|
|
85
|
+
Import a widget from `@bitforgehq/ui` into the component that uses it. Replace `src/app/app.ts` with the code below and delete the welcome page it no longer uses, `app.html` and `app.css`. `app.spec.ts` tests that welcome page too: delete its `should render title` case (or make it check the button), so `ng test` keeps passing.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { Component } from '@angular/core';
|
|
89
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
90
|
+
|
|
91
|
+
@Component({
|
|
92
|
+
selector: 'app-root',
|
|
93
|
+
imports: [BfButtonComponent],
|
|
94
|
+
template: `<bf-button variant="primary">Save</bf-button>`,
|
|
95
|
+
})
|
|
96
|
+
export class App {}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 9. Check it
|
|
100
|
+
|
|
101
|
+
Run the app and open it: you should now see an indigo, pill-shaped **Save** button in Manrope on the theme background. With `class="dark"` on `<html>`, the whole page is dark.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
ng serve
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Required peers: `@angular/common`, `@angular/core`, `@angular/forms` and `lucide`. An app that
|
|
108
|
+
imports `@bitforgehq/ui/overlay` (the overlay core: no Capacitor, no router) also installs
|
|
109
|
+
`@angular/cdk` and `rxjs` (a CDK peer); an app that imports `@bitforgehq/ui/mobile` installs those
|
|
110
|
+
plus `@angular/router` and `@capacitor/core`. They are declared optional so a desktop-only
|
|
111
|
+
install of the widgets stays free of them.
|
|
112
|
+
|
|
113
|
+
`styles.css` is the same for web and mobile apps. Nothing else: `theme.css` registers the library
|
|
114
|
+
as a Tailwind source itself. Do not create a `tailwind.config.js`, and add no Tailwind plugin:
|
|
115
|
+
the widgets style their native controls themselves.
|
|
116
|
+
|
|
117
|
+
Optional per-app overrides go after the import. A brand colour is a set: override
|
|
118
|
+
`--color-primary` together with `--color-primary-text` (primary as text, ≥ 4.5:1 on `bg` and
|
|
119
|
+
`surface`) and the rest of the `--color-primary-*` tokens, in `@theme` and in `.dark`; the docs
|
|
120
|
+
site's theme configurator derives the whole contrast-checked set from one colour.
|
|
121
|
+
`--radius-control` (`9999px`, pills) reshapes buttons, chips, badges, segmented, the tab bar, the
|
|
122
|
+
action bar and toggles at once:
|
|
123
|
+
|
|
124
|
+
```css
|
|
125
|
+
@theme {
|
|
126
|
+
--color-primary: #123456;
|
|
127
|
+
--color-primary-text: #123456;
|
|
128
|
+
--radius-control: 8px;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Compatibility
|
|
133
|
+
|
|
134
|
+
`@bitforgehq/ui` 0.x is client-rendered only. SSR, hydration and prerendering of pages that render
|
|
135
|
+
its components are not supported or tested. Component ids come from a module-level counter and can
|
|
136
|
+
differ between server and client, and the shell's theme is read from `localStorage` on the client.
|
|
137
|
+
Render library components only in the browser, for example in a `@defer` block or a client-only
|
|
138
|
+
route. Support will be decided before 1.0.
|
|
139
|
+
|
|
140
|
+
The one server-side check is a render smoke, not a support test: the library repository's
|
|
141
|
+
`tools/ssr-smoke.ts` renders 28 fixtures with `renderApplication()` (every root widget, `bf-icon`,
|
|
142
|
+
`bf-number-input`, a `/mobile` page with `provideBfShell()`, `bf-page`, `bf-header` and
|
|
143
|
+
`bf-content`, `bf-tabs` with `bf-stack-outlet` under the router, and an `/overlay` dialog), and all
|
|
144
|
+
of them render without an error. 19 of them render identically twice. The other 9 number their ids
|
|
145
|
+
differently on every render, from a module-level counter, so they could not hydrate: `bf-checkbox`,
|
|
146
|
+
`bf-input`, `bf-radio-group`, `bf-segmented`, `bf-select`, `bf-textarea`, `bf-toggle`,
|
|
147
|
+
`bf-number-input` and the `/overlay` dialog. Hydration itself is still not supported or tested, and
|
|
148
|
+
more stands in its way: `/mobile` writes to `<html>` and adds a keyboard probe to `<body>`, and an
|
|
149
|
+
`/overlay` dialog renders outside the app root and marks it inert.
|
|
150
|
+
|
|
151
|
+
## Keeping this guide in sync
|
|
152
|
+
|
|
153
|
+
This file is installed by the `bf-ui` CLI that ships with the package. Run `bunx bf-ui sync-ai`
|
|
154
|
+
to write (or refresh) `.ai/UI_GUIDE.md` in the consuming project, and add the CLAUDE.md/AGENTS.md
|
|
155
|
+
snippet it prints. `bunx bf-ui sync-ai --check` exits non-zero when the installed copy is missing
|
|
156
|
+
or out of date — wire it into CI so agent guidance never drifts from the installed package version.
|
|
157
|
+
|
|
158
|
+
## Using components
|
|
159
|
+
|
|
160
|
+
- Import the standalone class into the component that uses it:
|
|
161
|
+
`import { BfButtonComponent } from '@bitforgehq/ui';` and add it to `imports: [...]`.
|
|
162
|
+
- Icons: `import { BfIconComponent } from '@bitforgehq/ui/icons';` plus the icon data from
|
|
163
|
+
`lucide` (`import { Search } from 'lucide'`), then `<bf-icon [icon]="Search" />`. Every icon
|
|
164
|
+
position in every widget is a content slot (`<bf-icon bfSlot="icon-start" ... />`).
|
|
165
|
+
- Number field: `import { BfNumberInputComponent } from '@bitforgehq/ui/number-input';`. It is
|
|
166
|
+
the one widget in its own entry point, because it loads Angular Forms (`@angular/forms`, a
|
|
167
|
+
required peer); `@bitforgehq/ui` itself loads no forms code, so an app without forms ships none.
|
|
168
|
+
- Layout classes go on the element (`<bf-button class="mt-4">`); never restyle a widget's internals.
|
|
169
|
+
- Colors: use only the semantic tokens (`bg-primary`, `text-text-muted`, `bg-surface`,
|
|
170
|
+
`border-border`, `rounded-field`, `shadow-raised`, `text-body`). Never raw palette classes and
|
|
171
|
+
never `dark:` variants; put `dark` on `<html>` and the theme re-colors everything.
|
|
172
|
+
- Forms: `bf-input`, `bf-number-input`, `bf-textarea`, `bf-select`, `bf-checkbox`, `bf-toggle`,
|
|
173
|
+
`bf-radio-group` and `bf-segmented` expose `value` / `checked` as `model()` and implement
|
|
174
|
+
Signal Forms' `FormValueControl` / `FormCheckboxControl`. Bind either `[formField]="form.field"` or
|
|
175
|
+
`[(value)]` / `[(checked)]`, never both. Import `FormField`, `FormRoot`, `form` and the
|
|
176
|
+
validators from `@angular/forms/signals`. The first error shows once the field is touched: its
|
|
177
|
+
own `message` (`required(p.email, { message: 'Enter your email' })`), else the default text for
|
|
178
|
+
its kind from `provideBfLabels()` ("Fill in this field", "Use at least 5 characters", …), so a
|
|
179
|
+
field never turns red without text. Wrap the fields in
|
|
180
|
+
`<form [formRoot]="myForm">` with `submission: { action, onInvalid }` in `form()`'s options, so
|
|
181
|
+
an empty submit shows every message instead of the browser's bubbles; errors the `action`
|
|
182
|
+
returns show under their field. `errorMessage` is for errors from outside the form.
|
|
183
|
+
- Typed values: `bf-select`, `bf-radio-group` and `bf-segmented` are generic over their option
|
|
184
|
+
values (`BfSelectOption<number>`, `BfRadioOption<T>`, `BfSegmentOption<T>`; `string` by
|
|
185
|
+
default) and bind `T | null`, where `null` means nothing is chosen (start the field at `null`;
|
|
186
|
+
`required()` treats it as empty; `(changed)` emits only a chosen `T`). Options match with
|
|
187
|
+
`Object.is`; for object values pass `[compareWith]="byId"`. Numbers bind to `bf-number-input`
|
|
188
|
+
(`number | null`, `null` while empty; `min()`/`max()` reach the native input; unreadable text is
|
|
189
|
+
a `parse` error on the bound field, blocking `submit()`), never to a `string` model on
|
|
190
|
+
`bf-input type="number"`.
|
|
191
|
+
- Reactive Forms work through Angular 22's built-in interop: `[formControl]` and
|
|
192
|
+
`formControlName` bind every form widget above with no `ControlValueAccessor` (never add one).
|
|
193
|
+
Value, touched, dirty, validity, pending, disabled, required and errors flow into the widget;
|
|
194
|
+
the widget writes back only its value (marking the control dirty) and touched on blur. There is
|
|
195
|
+
no read-only state in Reactive Forms: bind `[readonly]` yourself. `Validators.*` errors carry no
|
|
196
|
+
message, so a touched invalid field shows the default text for the error's kind (`required`,
|
|
197
|
+
`minlength`, … read from the error's `context`); pass `errorMessage` only for your own text.
|
|
198
|
+
Type controls like the widgets' values: `new FormControl<number | null>(null, Validators.min(1))`
|
|
199
|
+
on a `bf-number-input` or a select of numeric ids.
|
|
200
|
+
- Links: navigation is a real `<a>`. `<a bfButton routerLink="/new">` styles a link as a
|
|
201
|
+
`bf-button` (same `variant`/`size`; import `BfButtonComponent`). For a clickable `bf-card` or
|
|
202
|
+
`bf-list-item`, put `bfAction` (`BfActionDirective`) on the one `<a>` or `<button>` holding the
|
|
203
|
+
heading or title, `<h3><a bfAction routerLink="/projects/1">Atlas</a></h3>`: it stretches over
|
|
204
|
+
the surface while the heading stays a heading; give other controls inside `class="relative z-1"`.
|
|
205
|
+
Never use the deprecated `bf-card [interactive]`. Inside `bf-tabs` the tab bar renders a `nav` of
|
|
206
|
+
links with `aria-current`; a standalone `bf-tab-bar` keeps `tablist` semantics.
|
|
207
|
+
|
|
208
|
+
## API conventions
|
|
209
|
+
|
|
210
|
+
Every public output, name input and type follows one convention (the generator enforces it), so
|
|
211
|
+
you can guess a binding without looking it up:
|
|
212
|
+
|
|
213
|
+
- Outputs are events in the past tense: `(clicked)`, `(changed)`, `(dismissed)`, `(focused)`,
|
|
214
|
+
`(blurred)`, `(scrolled)`, `(reselected)`, `(stackChanged)`, `(transitionStarted)`,
|
|
215
|
+
`(refreshRequested)`. None starts with `on`, and no declared output is `xChange` (that name is
|
|
216
|
+
the implicit output of a `model()`, `[(value)]`). Exceptions: `touch` (the Signal Forms
|
|
217
|
+
contract) and the page lifecycle `bfViewWillEnter`/`bfViewDidEnter`/`bfViewWillLeave`/`bfViewDidLeave`.
|
|
218
|
+
- Actions emit `(clicked)`; a sub-part's action is `<part>Clicked` (the desktop toast's
|
|
219
|
+
`actionClicked`). Exception: menu items (`bfMenuItem`, `bfMenuItemCheckbox`, `bfMenuItemRadio`)
|
|
220
|
+
emit `(triggered)`, mirroring the CDK menu, and it fires on `Enter` and `Space` too; binding
|
|
221
|
+
`(clicked)` on a menu item never fires.
|
|
222
|
+
- Every value control (a `value`/`checked`/`selected`/`active` model: the form fields, `bf-chip`,
|
|
223
|
+
`bf-tab-bar`, `bfMenuItemCheckbox`) has exactly one `(changed)`, emitted with the new value right
|
|
224
|
+
after a user change, never for a programmatic write.
|
|
225
|
+
- Names: `label` is a component's own name (visible, or the accessible name of an icon button,
|
|
226
|
+
spinner, tab bar, progress or icon); every component that renders a native input, select or
|
|
227
|
+
textarea (checkbox and toggle included) also takes `ariaLabel` and `ariaLabelledby` for when no
|
|
228
|
+
visible `label` is shown; a built-in sub-control's name is `<part>Label` (`dismissLabel`,
|
|
229
|
+
`closeLabel`, `actionLabel`); `alt` only on `bf-avatar`.
|
|
230
|
+
- Types: every union or object type of an input, model, output, `Bf…Options`/`Bf…Config` property
|
|
231
|
+
or an interface they name is a named type exported from the same entry
|
|
232
|
+
(`BfToggleLabelPosition`, `BfRefresherRefreshEvent`, `BfDialogRole`, …), so type your own state
|
|
233
|
+
with it instead of repeating the literals. A field's "left off" value `''` has its own type
|
|
234
|
+
(`BfInputInputMode`, `BfInputEnterKeyHint`, `BfInputAutocapitalize`); `BfInputMode`,
|
|
235
|
+
`BfEnterKeyHint` and `BfAutocapitalize` list only the native keywords.
|
|
236
|
+
- Renamed 0.2 outputs keep working as deprecated aliases that emit together with the new name
|
|
237
|
+
until 0.4.0: `bf-chip` `toggled` → `changed`, `bf-tab-bar` `tabChange` → `changed` and
|
|
238
|
+
`tabReselect` → `reselected`, `bf-stack-outlet` `stackChange` → `stackChanged`,
|
|
239
|
+
`transitionStart` → `transitionStarted` and `transitionEnd` → `transitionEnded`, `bf-refresher`
|
|
240
|
+
`refresh` → `refreshRequested`. Write new code with the new names.
|
|
241
|
+
|
|
242
|
+
## Configuration
|
|
243
|
+
|
|
244
|
+
Two optional providers from `@bitforgehq/ui` set app-wide widget defaults. Each returns a plain
|
|
245
|
+
`Provider` merged over the nearest parent injector's value, so it works in
|
|
246
|
+
`ApplicationConfig.providers` and in a component's `providers` (for a sub-tree, e.g. a dense
|
|
247
|
+
toolbar or a German settings page); a key you leave out keeps the parent's value.
|
|
248
|
+
|
|
249
|
+
- `provideBfDefaults({ size, fieldSize, controlSize })` sets the height step (`BfSize`: `sm` 32,
|
|
250
|
+
`md` 40, `lg` 48 px) a widget uses when its own `size` input is unset. `fieldSize` covers the
|
|
251
|
+
text fields (`bf-input`, `bf-number-input`, `bf-select`, `bf-textarea`), `controlSize` the
|
|
252
|
+
actions and choice controls (`bf-button`, `bf-icon-button`, `bf-chip`, `bf-segmented`,
|
|
253
|
+
`bf-checkbox`, `bf-toggle`, `bf-radio-group`); `size` sets both, and an explicit
|
|
254
|
+
`fieldSize`/`controlSize` wins over it. The library default is compact `md` for both (desktop
|
|
255
|
+
first): a desktop web app sets nothing, a data-dense app or sub-tree (a table toolbar, a filter
|
|
256
|
+
row) sets `{ size: 'sm' }`, a Capacitor app sets `{ fieldSize: 'lg' }` once. Never hard-code
|
|
257
|
+
`size` on every widget to change density; set it in a provider. Under a fine pointer the visible
|
|
258
|
+
box is the hit area (≥ 24 px, 32 px and up for sized controls); under a coarse pointer every
|
|
259
|
+
action and choice control reaches 44 px through an invisible extension, and text fields do not
|
|
260
|
+
(hence `fieldSize: 'lg'` for touch). Keep `gap-2` or more between wrapping rows of `sm` controls.
|
|
261
|
+
- `provideBfLabels({ ... })` replaces the built-in English strings (`loading`, `dismiss`, `tabBar`,
|
|
262
|
+
`tabBadge(count)`, `back`, `ok`, `cancel`, `actions`, `sheet`, `modal`, `refreshing`, `close`,
|
|
263
|
+
`dialog`) and the
|
|
264
|
+
default error text per validator kind (`validation`: `required`, `email`, `pattern`, `parse`,
|
|
265
|
+
`min(n)`, `max(n)`, `minLength(n)`, `maxLength(n)`, `minDate(d)`, `maxDate(d)`, `fallback`),
|
|
266
|
+
merged key by key; a key set to `undefined` also keeps the parent's string. Use it for any
|
|
267
|
+
non-English app instead of passing `errorMessage`/`ariaLabel` text to every widget; German copy
|
|
268
|
+
writes "ss", never "ß" (`dismiss: 'Schliessen'`).
|
|
269
|
+
`bfValidationMessage(error, labels.validation)` returns the text a widget shows for one error:
|
|
270
|
+
its own `message`, else the default for its kind. `bf-sheet` and `bf-modal` opened without an
|
|
271
|
+
`ariaLabel` are named by the first heading in their content (also one that renders later), and
|
|
272
|
+
fall back to `sheet`/`modal`.
|
|
273
|
+
- What a component-level `provideBfLabels()` covers: the widgets, form controls and
|
|
274
|
+
`bf-back-button`/`bf-refresher` rendered inside that component (`loading`, `dismiss`, `tabBar`,
|
|
275
|
+
`tabBadge`, `back`, `refreshing`, `validation`). The overlay services (`BfAlertDialogService`
|
|
276
|
+
`ok`, `BfActionSheetService` `cancel`/`actions`, `BfSheetService` `sheet`, `BfModalService`
|
|
277
|
+
`modal`, `BfDialogService` `close`/`dialog`/`ok`/`cancel`, `BfDesktopToastService`
|
|
278
|
+
`notifications`) are root singletons: they read the app-level labels unless the call passes the
|
|
279
|
+
feature's injector, `open(…, { injector: inject(Injector) })`, which also makes it the parent of
|
|
280
|
+
the overlay content's injector, so the content sees the feature's providers too.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// Desktop web app: nothing to configure; md is the default. Localise the strings once.
|
|
284
|
+
export const appConfig: ApplicationConfig = {
|
|
285
|
+
providers: [
|
|
286
|
+
provideBfLabels({
|
|
287
|
+
back: 'Zurück',
|
|
288
|
+
cancel: 'Abbrechen',
|
|
289
|
+
tabBadge: (count) => `${count} neu`,
|
|
290
|
+
validation: { required: 'Pflichtfeld' },
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Capacitor app: 48 px text fields as on a phone (use { size: 'lg' } to enlarge every control).
|
|
296
|
+
export const appConfig: ApplicationConfig = {
|
|
297
|
+
providers: [
|
|
298
|
+
provideRouter(routes, withBfShellRouting()),
|
|
299
|
+
provideBfShell(),
|
|
300
|
+
provideBfDefaults({ fieldSize: 'lg' }),
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Overlays (`@bitforgehq/ui/overlay`)
|
|
306
|
+
|
|
307
|
+
A desktop or plain web app (no Capacitor, no router required) installs `@angular/cdk` and `rxjs`
|
|
308
|
+
alongside `@bitforgehq/ui` and imports this entry for a centred dialog, a stacked toast and
|
|
309
|
+
keyboard-operable menus. Open every overlay from code through its service; never place `bf-dialog`
|
|
310
|
+
or `bf-desktop-toast` in a template outside the service that renders them.
|
|
311
|
+
|
|
312
|
+
- **Dialog vs sheet/modal:** `BfDialogService.open(component | template, config)` opens a centred
|
|
313
|
+
`bf-dialog` (`role="dialog"`, or `"alertdialog"` for `confirm()`) that traps focus, locks page
|
|
314
|
+
scroll and is sized `sm`/`md`/`lg`/`xl`. Use it for any desktop or web app screen. `/mobile`'s
|
|
315
|
+
`BfSheetService`/`BfModalService` render phone-shaped, edge-anchored panels for the Capacitor
|
|
316
|
+
shell — do not use them in a desktop-only app; use `bf-dialog` there instead, even for a form
|
|
317
|
+
that would be a sheet on a phone.
|
|
318
|
+
- **Confirm:** `BfDialogService.confirm({ title, message?, buttons?, size?, injector? })` opens an
|
|
319
|
+
alert dialog and resolves the pressed button's `id ?? label` (default `cancel`/`ok`); focus
|
|
320
|
+
starts on the cancel button, `Escape` answers with it (or is ignored without one, per APG alert
|
|
321
|
+
dialog). It is the desktop equivalent of `/mobile`'s `BfAlertDialogService.open()`.
|
|
322
|
+
- **Desktop toast vs mobile toast:** `BfDesktopToastService.show({ message, action?, duration?,
|
|
323
|
+
injector? })` stacks up to three cards bottom-right (configurable with
|
|
324
|
+
`provideBfDesktopToast()`), following the same timing/persistence/replace rules as `/mobile`'s
|
|
325
|
+
`BfToastService` (which shows one phone-width toast at a time). Use the desktop service outside
|
|
326
|
+
the Capacitor shell; never both in the same app screen.
|
|
327
|
+
- **Menu:** `bf-menu` (a `CdkMenu` panel) opened from `[bfMenuTriggerFor]` on any element, or from
|
|
328
|
+
the styled `bf-menu-button`. Items are `bfMenuItem` (`<button>` or `<a>`), `bfMenuItemCheckbox`,
|
|
329
|
+
`bfMenuItemRadio` (grouped with `bfMenuGroup`), dividers with `bfMenuDivider`, and submenus by
|
|
330
|
+
nesting another `[bfMenuTriggerFor]` on an item. Keyboard follows APG menu button/menu: open
|
|
331
|
+
keys, arrow navigation with wrap, Home/End, type-ahead, `Escape`/`Tab` close with focus back on
|
|
332
|
+
the trigger, `ArrowRight`/`ArrowLeft` for submenus. A menu opened from inside a `bf-dialog`:
|
|
333
|
+
`Escape` closes only the menu, never the dialog underneath.
|
|
334
|
+
- **`closeOnNavigation`:** every overlay (`BfDialogConfig`, `confirm()`, and the `/mobile` sheet,
|
|
335
|
+
modal, action sheet and alert options) closes when the URL path changes by default (`true`);
|
|
336
|
+
pass `closeOnNavigation: false` to keep one open across a navigation (e.g. a draft that must
|
|
337
|
+
survive browser back). Toasts (both desktop and mobile) never close on navigation.
|
|
338
|
+
- **The `injector` option:** `BfDialogService.open()`/`confirm()`, `BfDesktopToastService.show()`
|
|
339
|
+
and the `/mobile` sheet, modal, action sheet and alert services all take an `injector` (default
|
|
340
|
+
the root injector), which becomes the parent of the content's injector and is what a
|
|
341
|
+
feature-level `provideBfLabels()` or other providers must reach through — pass
|
|
342
|
+
`inject(Injector)` from the calling feature, not the app root's. The `/mobile` `BfToastService`
|
|
343
|
+
has no `injector` option (its one-at-a-time toast never opens a labelled stack).
|
|
344
|
+
- **Focus rules:** a dialog traps Tab/Shift+Tab inside its pane and restores focus to the opener
|
|
345
|
+
on close (`autoFocus`: `'first-tabbable'`, the default, or `'panel'` for long content with no
|
|
346
|
+
natural first control); a menu returns focus to its trigger on `Escape`, `Tab` or an item
|
|
347
|
+
activation. Initial focus in `confirm()`/alert-style overlays lands on the `cancel`-role button
|
|
348
|
+
(APG alert dialog), not the first button.
|
|
349
|
+
- **`F6` for toasts:** pressing `F6` on the page moves focus to the action of the newest toast
|
|
350
|
+
that has one; `F6` inside the toast stack moves focus back to where it was (the key is left to
|
|
351
|
+
the browser otherwise) — this is the desktop toast's only keyboard path to its action beyond Tab
|
|
352
|
+
through the page content. It does nothing while a modal overlay is open (`BfOverlayStack.count()
|
|
353
|
+
> 0`), so it never escapes that overlay's focus trap.
|
|
354
|
+
- **Scroll lock and inert outside the shell:** without `provideBfShell()` (a desktop or plain web
|
|
355
|
+
app), an open non-passive overlay (dialog, confirm) blocks document scroll and makes every other
|
|
356
|
+
direct child of `<body>` `inert` except the CDK overlay container and live-announcer elements
|
|
357
|
+
(mark an element that must stay usable, such as your own live region, `data-bf-overlay-keep`).
|
|
358
|
+
Inside the shell (`provideBfShell()`) nothing changes: the document never scrolls, so no lock is
|
|
359
|
+
applied, and the page stack (not the whole body) is made inert instead. Toasts never lock scroll
|
|
360
|
+
or make anything inert, on either entry.
|
|
361
|
+
- **Import migration:** the overlay core (`BfOverlayService`, `BfOverlayRef`, `BfOverlayStack`,
|
|
362
|
+
the animations, …) moved from `@bitforgehq/ui/mobile` to `@bitforgehq/ui/overlay` in 0.3.0;
|
|
363
|
+
`/mobile`'s old exports are deprecated aliases removed in 0.4.0. See the CHANGELOG's `### overlay
|
|
364
|
+
entry` migration table for the full old-import → new-import list.
|
|
365
|
+
|
|
366
|
+
## Mobile shell (`@bitforgehq/ui/mobile`)
|
|
367
|
+
|
|
368
|
+
A Capacitor app additionally imports `@bitforgehq/ui/mobile` for the native app shell. Three
|
|
369
|
+
rules, then the setup, step by step (the docs site's `/mobile` page shows the same steps):
|
|
370
|
+
|
|
371
|
+
- The app renders inside a fixed page frame — the frame itself never scrolls.
|
|
372
|
+
- `bf-content` is the only scroller inside a page: one per `bf-page`, after its `bf-header`.
|
|
373
|
+
- Routed pages read route state (params, query params, data) through `input()`, never by
|
|
374
|
+
injecting `ActivatedRoute` directly in the page.
|
|
375
|
+
|
|
376
|
+
### 1. Add the shell and its peers
|
|
377
|
+
|
|
378
|
+
Start from an app set up as on [Install](https://bitforge-ui.web.app/install). `@bitforgehq/ui/mobile` needs `@angular/cdk`, `@angular/router`, `@capacitor/core` and `rxjs`: optional peers of the package, required for this entry. The shell's Capacitor plugins are optional peers, and the entry builds and runs without the ones you skip, but a shell app needs `@capacitor/keyboard`: the `capacitor.config.ts` of step 5 imports `KeyboardResize` from it for the required `resize: 'none'`, as the mobile demo's does. The plugins:
|
|
379
|
+
|
|
380
|
+
- `@capacitor/app` (optional): the Android hardware back button;
|
|
381
|
+
- `@capacitor/keyboard` (needed): the keyboard height and events, and the `Keyboard` config;
|
|
382
|
+
- `@capacitor/haptics` (optional): the tick on a tab change.
|
|
383
|
+
|
|
384
|
+
List the plugins in the `dependencies` of the `package.json` beside `capacitor.config.ts`. The Capacitor CLI finds plugins only there: `cap sync` skips a plugin listed only in a workspace root, so check its "Found N Capacitor plugins" lines.
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
bun add @angular/cdk @angular/router @capacitor/core rxjs
|
|
388
|
+
bun add @capacitor/app @capacitor/keyboard @capacitor/haptics
|
|
389
|
+
bun add -d @capacitor/cli
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### 2. Provide the shell
|
|
393
|
+
|
|
394
|
+
In `src/app/app.config.ts`, add `provideBfShell()` and pass `withComponentInputBinding()` and `withBfShellRouting()` to `provideRouter()`. The shell does not render correctly without `provideBfShell()` and `withBfShellRouting()`; `withComponentInputBinding()` binds route params and data to the pages' inputs. `provideBfDefaults({ fieldSize: 'lg' })` gives text fields their 48 px phone height (see [Density](https://bitforge-ui.web.app/density#capacitor-app)).
|
|
395
|
+
|
|
396
|
+
`app.config.ts`:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { type ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
400
|
+
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
|
401
|
+
import { provideBfDefaults } from '@bitforgehq/ui';
|
|
402
|
+
import { provideBfShell, withBfShellRouting } from '@bitforgehq/ui/mobile';
|
|
403
|
+
import { routes } from './app.routes';
|
|
404
|
+
|
|
405
|
+
export const appConfig: ApplicationConfig = {
|
|
406
|
+
providers: [
|
|
407
|
+
provideBrowserGlobalErrorListeners(),
|
|
408
|
+
provideRouter(routes, withComponentInputBinding(), withBfShellRouting()),
|
|
409
|
+
provideBfShell(),
|
|
410
|
+
// 48 px text fields, as on a phone; buttons and choice controls keep md with a 44 px touch area.
|
|
411
|
+
provideBfDefaults({ fieldSize: 'lg' }),
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 3. Give the root a height
|
|
417
|
+
|
|
418
|
+
The root component (`src/app/app.ts`) hosts one `bf-stack-outlet`, and the outlet is `height: 100%` of it. Give the root's host `block h-dvh`: without a height the outlet is zero pixels tall and nothing renders, with no error in the console.
|
|
419
|
+
|
|
420
|
+
`app.ts`:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { Component } from '@angular/core';
|
|
424
|
+
import { BfStackOutletComponent } from '@bitforgehq/ui/mobile';
|
|
425
|
+
|
|
426
|
+
@Component({
|
|
427
|
+
selector: 'app-root',
|
|
428
|
+
// The outlet is `height: 100%`: without a height here it has zero height and shows nothing.
|
|
429
|
+
host: { class: 'block h-dvh' },
|
|
430
|
+
imports: [BfStackOutletComponent],
|
|
431
|
+
template: `<bf-stack-outlet />`,
|
|
432
|
+
})
|
|
433
|
+
export class App {}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 4. Set the viewport and the body
|
|
437
|
+
|
|
438
|
+
In `src/index.html`:
|
|
439
|
+
|
|
440
|
+
- `viewport-fit=cover` lets the page reach under the notch and the home indicator, and the shell pads itself by the safe areas.
|
|
441
|
+
- Leave out `maximum-scale` and `user-scalable`, so the web build keeps pinch zoom (WCAG 1.4.4). On native platforms `provideBfShell()` adds `maximum-scale=1, user-scalable=no` at boot: WKWebView zooms into a focused control whose text is under 16 px and never zooms back, so the native shell forbids zoom and follows the system text size instead.
|
|
442
|
+
- `bg-bg text-text` on `<body>` gives the page the theme colours, in light and dark mode.
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
446
|
+
<!-- … -->
|
|
447
|
+
<body class="bg-bg text-text">
|
|
448
|
+
<app-root></app-root>
|
|
449
|
+
</body>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 5. Configure Capacitor
|
|
453
|
+
|
|
454
|
+
Create `capacitor.config.ts` beside `package.json` (it imports `KeyboardResize` from `@capacitor/keyboard`, installed in step 1), with the `appId`, `appName` and the `webDir` your `ng build` writes. Keep each of these keys; the keyboard handling and the safe areas depend on every one:
|
|
455
|
+
|
|
456
|
+
- `ios.scrollEnabled: false`: the document never scrolls, so WKWebView cannot jump the page when focus moves between fields. `bf-content` is the only scroller.
|
|
457
|
+
- `plugins.Keyboard.resize: 'none'` (`KeyboardResize.None`): on iOS the keyboard overlays a stable page, and `bf-content` pads itself and reveals the focused field. Never set `Keyboard.resizeOnFullScreen`: the SystemBars plugin owns the Android keyboard inset.
|
|
458
|
+
- `plugins.SystemBars.insetsHandling: 'css'`: on Android the WebView resizes with the keyboard and the safe-area insets stay correct. `initialViewportFitValueHint: 'cover'` avoids a viewport jump at launch.
|
|
459
|
+
|
|
460
|
+
`capacitor.config.ts`:
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
import type { CapacitorConfig } from '@capacitor/cli';
|
|
464
|
+
import { KeyboardResize } from '@capacitor/keyboard';
|
|
465
|
+
|
|
466
|
+
const config: CapacitorConfig = {
|
|
467
|
+
appId: 'com.example.myapp',
|
|
468
|
+
appName: 'My App',
|
|
469
|
+
webDir: 'dist/my-app/browser',
|
|
470
|
+
// The document never scrolls: no WKWebView page jumps between fields.
|
|
471
|
+
ios: { scrollEnabled: false },
|
|
472
|
+
plugins: {
|
|
473
|
+
// 'none': on iOS the keyboard overlays a stable frame, and bf-content reveals the field.
|
|
474
|
+
Keyboard: { resize: KeyboardResize.None },
|
|
475
|
+
// Android: the WebView resizes with the keyboard and the safe-area insets stay correct.
|
|
476
|
+
SystemBars: { insetsHandling: 'css', initialViewportFitValueHint: 'cover' },
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export default config;
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 6. Check the Android manifest
|
|
484
|
+
|
|
485
|
+
`bunx cap add android` (step 11) writes `android/app/src/main/AndroidManifest.xml`. Keep its activity as generated:
|
|
486
|
+
|
|
487
|
+
- `uiMode` stays in `android:configChanges`, so switching the system's dark mode does not restart the app.
|
|
488
|
+
- Add no `android:windowSoftInputMode`: the SystemBars plugin resizes the WebView with the keyboard.
|
|
489
|
+
- Edge to edge needs no setting. From `targetSdkVersion` 35 Android draws the app under the system bars, and SystemBars passes their insets to the page (`--safe-area-inset-*`, and `env(safe-area-inset-*)` on WebView 140 and later), which the theme's `safe-*` spacing reads. Capacitor 8 has no `adjustMarginsForEdgeToEdge` option any more.
|
|
490
|
+
|
|
491
|
+
```xml
|
|
492
|
+
<activity
|
|
493
|
+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
|
494
|
+
android:name=".MainActivity"
|
|
495
|
+
android:exported="true">
|
|
496
|
+
<!-- … no android:windowSoftInputMode … -->
|
|
497
|
+
</activity>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### 7. Route a tabbed app
|
|
501
|
+
|
|
502
|
+
In `src/app/app.routes.ts`, a `tabs` route holds the tab bar page, and its children are the tabs. Every page of a tab starts with the tab's segment (`home`, `home/:id`), so it pushes onto that tab's own navigation stack. Routed pages read their params and data through `input()`, which `withComponentInputBinding()` binds; they never inject `ActivatedRoute`.
|
|
503
|
+
|
|
504
|
+
`app.routes.ts`:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { type Routes } from '@angular/router';
|
|
508
|
+
|
|
509
|
+
export const routes: Routes = [
|
|
510
|
+
{ path: '', pathMatch: 'full', redirectTo: 'tabs' },
|
|
511
|
+
{
|
|
512
|
+
path: 'tabs',
|
|
513
|
+
loadComponent: () => import('./tabs.page').then((m) => m.TabsPage),
|
|
514
|
+
// One navigation stack per tab, keyed by the segment after `tabs/`.
|
|
515
|
+
children: [
|
|
516
|
+
{ path: 'home', loadComponent: () => import('./home.page').then((m) => m.HomePage) },
|
|
517
|
+
{
|
|
518
|
+
path: 'settings',
|
|
519
|
+
loadComponent: () => import('./settings.page').then((m) => m.SettingsPage),
|
|
520
|
+
},
|
|
521
|
+
{ path: '', pathMatch: 'full', redirectTo: 'home' },
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 8. Add the tab bar
|
|
528
|
+
|
|
529
|
+
`src/app/tabs.page.ts` is the `tabs` route's page: [`bf-tabs`](https://bitforge-ui.web.app/components/tabs) around one [`bf-stack-outlet`](https://bitforge-ui.web.app/components/stack-outlet) and a [`bf-tab-bar`](https://bitforge-ui.web.app/components/tab-bar). Each [`bf-tab`](https://bitforge-ui.web.app/components/tab)'s `id` is its route segment, and `tabsPath` is the route's path.
|
|
530
|
+
|
|
531
|
+
`tabs.page.ts`:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { Component } from '@angular/core';
|
|
535
|
+
import { BfTabBarComponent, BfTabComponent } from '@bitforgehq/ui';
|
|
536
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
537
|
+
import { BfStackOutletComponent, BfTabsComponent } from '@bitforgehq/ui/mobile';
|
|
538
|
+
import { House, Settings } from 'lucide';
|
|
539
|
+
|
|
540
|
+
@Component({
|
|
541
|
+
selector: 'app-tabs',
|
|
542
|
+
host: { class: 'block h-full' },
|
|
543
|
+
imports: [
|
|
544
|
+
BfTabsComponent,
|
|
545
|
+
BfStackOutletComponent,
|
|
546
|
+
BfTabBarComponent,
|
|
547
|
+
BfTabComponent,
|
|
548
|
+
BfIconComponent,
|
|
549
|
+
],
|
|
550
|
+
template: `
|
|
551
|
+
<bf-tabs tabsPath="tabs">
|
|
552
|
+
<bf-stack-outlet />
|
|
553
|
+
<bf-tab-bar label="Main">
|
|
554
|
+
<!-- Each tab's id is its route segment under tabs/. -->
|
|
555
|
+
<bf-tab id="home" label="Home"><bf-icon [icon]="house" /></bf-tab>
|
|
556
|
+
<bf-tab id="settings" label="Settings"><bf-icon [icon]="settings" /></bf-tab>
|
|
557
|
+
</bf-tab-bar>
|
|
558
|
+
</bf-tabs>
|
|
559
|
+
`,
|
|
560
|
+
})
|
|
561
|
+
export class TabsPage {
|
|
562
|
+
protected readonly house = House;
|
|
563
|
+
protected readonly settings = Settings;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### 9. Add the first page
|
|
568
|
+
|
|
569
|
+
Every page is a [`bf-page`](https://bitforge-ui.web.app/components/page): a [`bf-header`](https://bitforge-ui.web.app/components/header), then one [`bf-content`](https://bitforge-ui.web.app/components/content), the only scroller of the page. The app frame never scrolls.
|
|
570
|
+
|
|
571
|
+
`home.page.ts`:
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
import { Component, signal } from '@angular/core';
|
|
575
|
+
import { BfButtonComponent, BfCardComponent } from '@bitforgehq/ui';
|
|
576
|
+
import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
|
|
577
|
+
|
|
578
|
+
@Component({
|
|
579
|
+
selector: 'app-home',
|
|
580
|
+
imports: [
|
|
581
|
+
BfPageComponent,
|
|
582
|
+
BfHeaderComponent,
|
|
583
|
+
BfContentComponent,
|
|
584
|
+
BfCardComponent,
|
|
585
|
+
BfButtonComponent,
|
|
586
|
+
],
|
|
587
|
+
template: `
|
|
588
|
+
<bf-page>
|
|
589
|
+
<bf-header title="Home" />
|
|
590
|
+
<!-- bf-content is the page's only scroller. -->
|
|
591
|
+
<bf-content>
|
|
592
|
+
<div class="flex flex-col gap-4 px-4 pt-2 pb-6">
|
|
593
|
+
<bf-card>
|
|
594
|
+
<p class="text-body">Tapped {{ taps() }} times.</p>
|
|
595
|
+
</bf-card>
|
|
596
|
+
<bf-button (clicked)="taps.update((n) => n + 1)">Tap</bf-button>
|
|
597
|
+
</div>
|
|
598
|
+
</bf-content>
|
|
599
|
+
</bf-page>
|
|
600
|
+
`,
|
|
601
|
+
})
|
|
602
|
+
export class HomePage {
|
|
603
|
+
protected readonly taps = signal(0);
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### 10. Add the second tab
|
|
608
|
+
|
|
609
|
+
The Settings tab has the same shape.
|
|
610
|
+
|
|
611
|
+
`settings.page.ts`:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { Component, signal } from '@angular/core';
|
|
615
|
+
import { BfListComponent, BfListItemComponent, BfToggleComponent } from '@bitforgehq/ui';
|
|
616
|
+
import { BfContentComponent, BfHeaderComponent, BfPageComponent } from '@bitforgehq/ui/mobile';
|
|
617
|
+
|
|
618
|
+
@Component({
|
|
619
|
+
selector: 'app-settings',
|
|
620
|
+
imports: [
|
|
621
|
+
BfPageComponent,
|
|
622
|
+
BfHeaderComponent,
|
|
623
|
+
BfContentComponent,
|
|
624
|
+
BfListComponent,
|
|
625
|
+
BfListItemComponent,
|
|
626
|
+
BfToggleComponent,
|
|
627
|
+
],
|
|
628
|
+
template: `
|
|
629
|
+
<bf-page>
|
|
630
|
+
<bf-header title="Settings" />
|
|
631
|
+
<bf-content>
|
|
632
|
+
<bf-list class="px-4 pt-2">
|
|
633
|
+
<bf-list-item>
|
|
634
|
+
<bf-toggle label="Notifications" labelPosition="start" [(checked)]="notifications" />
|
|
635
|
+
</bf-list-item>
|
|
636
|
+
</bf-list>
|
|
637
|
+
</bf-content>
|
|
638
|
+
</bf-page>
|
|
639
|
+
`,
|
|
640
|
+
})
|
|
641
|
+
export class SettingsPage {
|
|
642
|
+
protected readonly notifications = signal(true);
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### 11. Leave the keyboard and text size to the shell
|
|
647
|
+
|
|
648
|
+
- The shell keeps the focused field clear of the soft keyboard: `bf-content` pads and scrolls itself, and a sheet lifts above the keyboard. Add no keyboard handling of your own. To place something by the keyboard, read [`BfKeyboardService`](https://bitforge-ui.web.app/api/bf-keyboard-service), never `window.innerHeight`.
|
|
649
|
+
- On iOS, [`BfTextScaleService`](https://bitforge-ui.web.app/api/bf-text-scale-service), which `provideBfShell()` starts, follows Dynamic Type. It sets the `<html>` font size to the system body size over 17 px, capped at `dynamicType.maxScale` (default 2.35) and never below 100 %, and follows live changes. Android's WebView applies the system font scale itself.
|
|
650
|
+
- Size text and layout in `rem`, as the library's tokens are, so they scale together; a fixed `px` height clips enlarged text. Opt out with `provideBfShell({ dynamicType: false })`, and never set a `px` font size on `html`.
|
|
651
|
+
|
|
652
|
+
### 12. Match the system bars to the theme
|
|
653
|
+
|
|
654
|
+
On native iOS and Android `provideBfShell()` keeps the status-bar and navigation-bar icons readable in the in-app theme (`BfPlatformService.dark()`): it calls Capacitor's `SystemBars.setStyle()` at launch and on every theme change, with light icons in the dark theme and dark icons in the light theme. `provideBfShell({ syncSystemBars: false })` leaves the bars to your app.
|
|
655
|
+
|
|
656
|
+
An Android WebView older than 140 pads the page below the bars instead of drawing it under them, so the bar areas show the activity's window background, which follows the system theme, not the in-app one. On those WebViews the shell uses the `Default` style (icons for the system theme). Give the window the page colour, so the bars match the page whenever the two themes agree: in `android/app/src/main/res/values/styles.xml` add the item to `AppTheme.NoActionBar` (its parent is already a `DayNight` theme), and add `res/values/colors.xml` and `res/values-night/colors.xml`.
|
|
657
|
+
|
|
658
|
+
| WebView | App theme vs system theme | Result |
|
|
659
|
+
| ------- | ------------------------- | ------------------------------------------------------------------------- |
|
|
660
|
+
| ≥ 140 | any | The bars draw over the page (edge-to-edge) with icons in the in-app theme |
|
|
661
|
+
| < 140 | same | Bars in the page colour, readable icons |
|
|
662
|
+
| < 140 | different | Bars in the system colour with readable icons |
|
|
663
|
+
|
|
664
|
+
The last row is a known limitation: only app-side native code can recolour the window background.
|
|
665
|
+
|
|
666
|
+
The ≥ 140 row needs the setup of steps 4 and 5: `viewport-fit=cover` in the viewport meta and `SystemBars.insetsHandling: 'css'` (the default), so the WebView reaches under the bars and SystemBars passes their insets to the page. With `insetsHandling: 'disable'` or `'native'`, or without `viewport-fit=cover`, the page never draws under the bars, and they show the window background as on WebView < 140. The shell reads the WebView version from `Chrome/<n>` in the user agent.
|
|
667
|
+
|
|
668
|
+
```xml
|
|
669
|
+
<!-- res/values/styles.xml, in <style name="AppTheme.NoActionBar"> -->
|
|
670
|
+
<item name="android:windowBackground">@color/bf_window_background</item>
|
|
671
|
+
|
|
672
|
+
<!-- res/values/colors.xml -->
|
|
673
|
+
<resources>
|
|
674
|
+
<color name="bf_window_background">#f7f5ef</color>
|
|
675
|
+
</resources>
|
|
676
|
+
|
|
677
|
+
<!-- res/values-night/colors.xml -->
|
|
678
|
+
<resources>
|
|
679
|
+
<color name="bf_window_background">#111214</color>
|
|
680
|
+
</resources>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 13. Run it on a device
|
|
684
|
+
|
|
685
|
+
Build the web app, add the native platforms once, then copy the build into them and run it on a device or simulator. Xcode builds the iOS app and Android Studio the Android one. `cap sync` lists the plugins it found: check that all three from step 1 are there. `cap run android` runs Gradle, which needs a JDK it supports, such as 21 (`JAVA_HOME`); `--list` shows the device ids.
|
|
686
|
+
|
|
687
|
+
`cap run ios` builds with `xcodebuild`, then deploys through `Xcode.app/Contents/Developer/Applications/Simulator.app`. Xcode 27 has no Simulator.app there, so the build succeeds and the deploy fails ("There was an error opening simulator"). Then build, install and launch with `xcodebuild` and `simctl` instead, as below (the scheme is `App`; `ios/App/build` is git-ignored), or run the app from Xcode.
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
ng build
|
|
691
|
+
bun add @capacitor/ios @capacitor/android
|
|
692
|
+
bunx cap add ios
|
|
693
|
+
bunx cap add android
|
|
694
|
+
bunx cap sync
|
|
695
|
+
bunx cap run android --target <device-id>
|
|
696
|
+
bunx cap run ios --target <simulator-id>
|
|
697
|
+
|
|
698
|
+
# iOS without Simulator.app (Xcode 27): build, boot, install and launch
|
|
699
|
+
cd ios/App
|
|
700
|
+
xcodebuild -project App.xcodeproj -scheme App -configuration Debug -sdk iphonesimulator \
|
|
701
|
+
-destination "id=<simulator-id>" -derivedDataPath build build
|
|
702
|
+
xcrun simctl boot <simulator-id> || true # already booted is fine
|
|
703
|
+
xcrun simctl install <simulator-id> build/Build/Products/Debug-iphonesimulator/App.app
|
|
704
|
+
xcrun simctl launch <simulator-id> <appId>
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Keyboard
|
|
708
|
+
|
|
709
|
+
The shell keeps the focused text field clear of the soft keyboard; don't add your own keyboard
|
|
710
|
+
handling on top.
|
|
711
|
+
|
|
712
|
+
- On a page, `bf-content` pads and scrolls itself (`bfKeyboardAssist`); a custom scroll container
|
|
713
|
+
inside a page takes `bfKeyboardAssist` itself.
|
|
714
|
+
- A sheet lifts above the keyboard (iOS), is capped below the safe-area top while the keyboard is
|
|
715
|
+
up (both platforms), scrolls its content and reveals the focused field 24 px above the keyboard;
|
|
716
|
+
a page-sheet modal ends its content at the keyboard's top edge. Hiding the keyboard returns
|
|
717
|
+
both to their resting size.
|
|
718
|
+
- Keep a sheet's primary action directly below its fields and a modal's in its header, so it
|
|
719
|
+
stays reachable while the keyboard is up.
|
|
720
|
+
- Read the keyboard from `BfKeyboardService` (`visible()`, `height()`, `keyboardTop()`, the
|
|
721
|
+
`--bf-keyboard-height` CSS variable), never from `window.innerHeight`.
|
|
722
|
+
|
|
723
|
+
### Overlays
|
|
724
|
+
|
|
725
|
+
Overlays (`BfOverlayService` and the sheet, modal, toast, action-sheet and alert services built on
|
|
726
|
+
it) are opened from code through a service, never placed in a template. The overlay core
|
|
727
|
+
(`BfOverlayService`, `BfOverlayRef`, `BF_OVERLAY_DATA`, `BfOverlayStack`, the animations) is
|
|
728
|
+
shared with the desktop overlays and imported from `@bitforgehq/ui/overlay` (see "Overlays
|
|
729
|
+
(`@bitforgehq/ui/overlay`)" above for `closeOnNavigation`, the `injector` option and the inert/
|
|
730
|
+
scroll-lock rules outside the shell); the `/mobile` re-exports of those names are deprecated.
|
|
731
|
+
Shell-specific rules:
|
|
732
|
+
|
|
733
|
+
- Open overlays through their service (`BfSheetService.open(Component, options)`,
|
|
734
|
+
`BfModalService`, `BfToastService.show()`, `BfActionSheetService.open()`,
|
|
735
|
+
`BfAlertDialogService.open()`); never render `bf-sheet`, `bf-modal`, `bf-toast`,
|
|
736
|
+
`bf-action-sheet` or `bf-alert-dialog` yourself.
|
|
737
|
+
- A content component reads its input with `inject(BF_OVERLAY_DATA)` and closes itself with
|
|
738
|
+
`inject(BfOverlayRef).close(result)` (`BfSheetRef` in a sheet); the caller awaits
|
|
739
|
+
`ref.afterClosed` (`open()` returns the promise directly for action sheets and alert dialogs).
|
|
740
|
+
- A sheet's content region is the only scroller inside a sheet: no `bf-content` and no nested
|
|
741
|
+
`overflow-auto` in its content; mark the content's header `data-bf-sheet-drag`.
|
|
742
|
+
- An alert dialog needs a `cancel`-role button to be dismissible: without one, `Escape` and the
|
|
743
|
+
hardware back button are ignored and only a button answers it.
|
|
744
|
+
- A toast with an `action` persists until the action, `Escape` inside it, `dismiss()` or the next
|
|
745
|
+
`show()` (which replaces any persistent toast) ends it; don't give it a short `duration`, and
|
|
746
|
+
`dismiss()` its ref once it is stale. Every toast pauses while hovered or focused, returns focus
|
|
747
|
+
to where it came from when it closes holding focus, and is announced once through
|
|
748
|
+
`LiveAnnouncer` — never wrap one in your own live region.
|
|
749
|
+
- A desktop web app (no Capacitor) shows toasts with `BfDesktopToastService` from
|
|
750
|
+
`@bitforgehq/ui/overlay` instead of `BfToastService` (see "Overlays (`@bitforgehq/ui/overlay`)"
|
|
751
|
+
above).
|
|
752
|
+
|
|
753
|
+
While a modal-like overlay is open the page stack is `inert`; outside the shell (a desktop page
|
|
754
|
+
using the overlays without `provideBfShell()`) every other child of `<body>` is `inert` instead
|
|
755
|
+
(mark an element that must stay usable, such as your own live region, `data-bf-overlay-keep`;
|
|
756
|
+
this works only on a direct child of `<body>`, never inside the app root).
|
|
757
|
+
Inside the shell no scroll lock is needed (the document never scrolls); outside it the overlay
|
|
758
|
+
core blocks document scroll behind every non-passive overlay. `Escape` closes the top dismissible
|
|
759
|
+
overlay, and under `provideBfShell()` so does the hardware back button (neither ever reaches one
|
|
760
|
+
underneath); a backdrop tap closes sheets and action sheets only — modals and alerts ignore it.
|
|
761
|
+
Toasts are passive and never block the page or its scroll.
|
|
762
|
+
|
|
763
|
+
## Testing
|
|
764
|
+
|
|
765
|
+
Specs of an app on `@bitforgehq/ui` run under the Angular CLI's Vitest (or Jest) with
|
|
766
|
+
`@bitforgehq/ui/testing`: the jsdom shims, the test providers and CDK harnesses (the docs site's
|
|
767
|
+
`/testing` page shows the same steps).
|
|
768
|
+
|
|
769
|
+
### 1. Add the CDK as a dev dependency
|
|
770
|
+
|
|
771
|
+
`@bitforgehq/ui/testing` holds the jsdom shims, `provideBfTesting()` and the component harnesses. The harnesses build on `@angular/cdk/testing`, so the entry needs `@angular/cdk`; a dev dependency is enough. An app that uses `@bitforgehq/ui/overlay` or `@bitforgehq/ui/mobile` already has the CDK as a dependency: skip this step.
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
bun add -d @angular/cdk
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### 2. Set up Vitest, the CLI default
|
|
778
|
+
|
|
779
|
+
An Angular 22 app runs its specs with Vitest and jsdom through `ng test`. jsdom has no Web Animations (`Element.animate`), no `PointerEvent` and no `matchMedia`, which the overlays and gestures use. Name a setup file in the `test` target of `angular.json`, and in it call `installBfJsdomShims()`, which installs a stand-in for each one that is missing.
|
|
780
|
+
|
|
781
|
+
```json
|
|
782
|
+
"test": {
|
|
783
|
+
"builder": "@angular/build:unit-test",
|
|
784
|
+
"options": {
|
|
785
|
+
"setupFiles": ["src/test-setup.ts"]
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
`test-setup.ts`:
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
/*
|
|
794
|
+
* The test runner's setup file (`setupFiles` of the `test` target in angular.json): the browser
|
|
795
|
+
* APIs jsdom lacks and the library needs (Web Animations, PointerEvent, matchMedia), installed only
|
|
796
|
+
* where missing. `@bitforgehq/ui/testing` needs `@angular/cdk` as a dev dependency.
|
|
797
|
+
*/
|
|
798
|
+
import { installBfJsdomShims } from '@bitforgehq/ui/testing';
|
|
799
|
+
|
|
800
|
+
installBfJsdomShims();
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### 3. Or set up Jest
|
|
804
|
+
|
|
805
|
+
With Jest and `jest-preset-angular`: install them (`bun add -d jest jest-environment-jsdom jest-preset-angular @types/jest`), and in `tsconfig.spec.json` set `"types": ["jest"]` in place of `vitest/globals`. Then add the two files below: the setup file calls `setupZonelessTestEnv()` (without it every spec fails with "Need to call TestBed.initTestEnvironment() first"), then `installBfJsdomShims()`.
|
|
806
|
+
|
|
807
|
+
The library ships ES modules (`.mjs`), which the preset's own `transformIgnorePatterns` lets through, together with Angular's locale files. A config that sets its own `transformIgnorePatterns` replaces that default: keep `.mjs`, `@angular/common/locales/`, `@bitforgehq/` and `lucide` in it, or the first import fails with `SyntaxError: Cannot use import statement outside a module`.
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
// jest.config.mjs
|
|
811
|
+
export default {
|
|
812
|
+
preset: 'jest-preset-angular',
|
|
813
|
+
setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
|
|
814
|
+
testEnvironment: 'jsdom',
|
|
815
|
+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$|@angular/common/locales/.*\\.js$|@bitforgehq/|lucide)'],
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/setup-jest.ts
|
|
819
|
+
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
|
|
820
|
+
import { installBfJsdomShims } from '@bitforgehq/ui/testing';
|
|
821
|
+
|
|
822
|
+
setupZonelessTestEnv();
|
|
823
|
+
installBfJsdomShims();
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### 4. Add the test providers
|
|
827
|
+
|
|
828
|
+
Add one provider to `TestBed.configureTestingModule()`, as the specs below do:
|
|
829
|
+
|
|
830
|
+
- `provideBfTesting()` from `@bitforgehq/ui/testing`, for the widgets and the desktop overlays. It takes the `defaults` and `labels` the app runs with. After each test it removes what overlays and the theme leave on the document: the `dark` class, the scroll block, `inert` on body children and stray overlay panes.
|
|
831
|
+
- `provideBfShellTesting()` from `@bitforgehq/ui/mobile`, for a Capacitor app. It provides the router with `withComponentInputBinding()` and `withBfShellRouting()`, the shell with swipe-back and haptics off, an in-memory theme store and the same cleanup.
|
|
832
|
+
|
|
833
|
+
The cleanup runs when TestBed tears the test environment down. Keep TestBed's default `teardown: { destroyAfterEach: true }`: with `false`, nothing is cleaned between tests.
|
|
834
|
+
|
|
835
|
+
### 5. Wait until the fixture is stable
|
|
836
|
+
|
|
837
|
+
Angular 22 apps are zoneless: nothing runs change detection between the steps of a test. After you create a component or change its state, `await fixture.whenStable()` before you read the DOM. Harness methods wait on their own.
|
|
838
|
+
|
|
839
|
+
`app.component.spec.ts`:
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import { Component } from '@angular/core';
|
|
843
|
+
import { TestBed } from '@angular/core/testing';
|
|
844
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
845
|
+
|
|
846
|
+
@Component({
|
|
847
|
+
imports: [BfButtonComponent],
|
|
848
|
+
template: `<bf-button variant="tonal">Save</bf-button>`,
|
|
849
|
+
})
|
|
850
|
+
class HostComponent {}
|
|
851
|
+
|
|
852
|
+
describe('bf-button in a consumer app (Vitest)', () => {
|
|
853
|
+
it('renders a native button with the projected label', async () => {
|
|
854
|
+
const fixture = TestBed.createComponent(HostComponent);
|
|
855
|
+
await fixture.whenStable();
|
|
856
|
+
const button = (fixture.nativeElement as HTMLElement).querySelector('button');
|
|
857
|
+
expect(button).not.toBeNull();
|
|
858
|
+
expect(button?.textContent?.trim()).toBe('Save');
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### 6. Drive a form widget through its harness
|
|
864
|
+
|
|
865
|
+
The entry ships CDK harnesses for the widgets whose markup is not obvious to drive: `BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfMenuButtonHarness` and `BfDesktopToastHarness`. `with({ label })` finds a widget by its label. Load an in-page widget with `TestbedHarnessEnvironment.loader(fixture)`.
|
|
866
|
+
|
|
867
|
+
`select-harness.spec.ts`:
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
/*
|
|
871
|
+
* A Signal Forms `bf-select` with numeric options, driven through `BfSelectHarness` by label: the
|
|
872
|
+
* form model holds the option's number, and a touched required select shows the default message.
|
|
873
|
+
*/
|
|
874
|
+
import { Component, signal } from '@angular/core';
|
|
875
|
+
import { TestBed } from '@angular/core/testing';
|
|
876
|
+
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
|
877
|
+
import { FormField, form, required } from '@angular/forms/signals';
|
|
878
|
+
import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
|
|
879
|
+
import { BfSelectHarness, provideBfTesting } from '@bitforgehq/ui/testing';
|
|
880
|
+
|
|
881
|
+
@Component({
|
|
882
|
+
imports: [BfSelectComponent, FormField],
|
|
883
|
+
template: `<bf-select label="Quantity" [options]="quantities" [formField]="order.quantity" />`,
|
|
884
|
+
})
|
|
885
|
+
class OrderFormComponent {
|
|
886
|
+
readonly quantities: readonly BfSelectOption<number>[] = [
|
|
887
|
+
{ value: 1, label: 'One' },
|
|
888
|
+
{ value: 2, label: 'Two' },
|
|
889
|
+
{ value: 3, label: 'Three' },
|
|
890
|
+
];
|
|
891
|
+
private readonly model = signal<{ quantity: number | null }>({ quantity: null });
|
|
892
|
+
readonly order = form(this.model, (path) => {
|
|
893
|
+
required(path.quantity);
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
describe('BfSelectHarness in a consumer app (Vitest)', () => {
|
|
898
|
+
beforeEach(() => {
|
|
899
|
+
TestBed.configureTestingModule({ providers: [provideBfTesting()] });
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
async function setup() {
|
|
903
|
+
const fixture = TestBed.createComponent(OrderFormComponent);
|
|
904
|
+
await fixture.whenStable();
|
|
905
|
+
const select = await TestbedHarnessEnvironment.loader(fixture).getHarness(
|
|
906
|
+
BfSelectHarness.with({ label: 'Quantity' }),
|
|
907
|
+
);
|
|
908
|
+
return { fixture, select };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
it('selects an option by label and the form model holds its number', async () => {
|
|
912
|
+
const { fixture, select } = await setup();
|
|
913
|
+
|
|
914
|
+
expect(await select.getOptionLabels()).toEqual(['One', 'Two', 'Three']);
|
|
915
|
+
await select.selectOption('Two');
|
|
916
|
+
|
|
917
|
+
expect(fixture.componentInstance.order.quantity().value()).toBe(2);
|
|
918
|
+
expect(await select.getValueLabel()).toBe('Two');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('shows the default required message once the select was touched', async () => {
|
|
922
|
+
const { select } = await setup();
|
|
923
|
+
|
|
924
|
+
expect(await select.getErrorText()).toBe('');
|
|
925
|
+
await select.focus();
|
|
926
|
+
await select.blur();
|
|
927
|
+
|
|
928
|
+
expect(await select.getErrorText()).toBe('Fill in this field');
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### 7. Drive a dialog from the document root
|
|
934
|
+
|
|
935
|
+
Dialogs, menus and toasts render in the CDK overlay container at the end of `<body>`, outside the fixture. Load their harnesses with `TestbedHarnessEnvironment.documentRootLoader(fixture)`.
|
|
936
|
+
|
|
937
|
+
`dialog-harness.spec.ts`:
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
/*
|
|
941
|
+
* A `confirm()` alert dialog through `BfDialogHarness`: dialogs render in the CDK overlay
|
|
942
|
+
* container, so the harness comes from the document root loader.
|
|
943
|
+
*/
|
|
944
|
+
import { Component, inject } from '@angular/core';
|
|
945
|
+
import { TestBed } from '@angular/core/testing';
|
|
946
|
+
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
|
947
|
+
import { BfDialogService } from '@bitforgehq/ui/overlay';
|
|
948
|
+
import { BfDialogHarness, provideBfTesting } from '@bitforgehq/ui/testing';
|
|
949
|
+
|
|
950
|
+
@Component({ template: '' })
|
|
951
|
+
class ProjectActionsComponent {
|
|
952
|
+
private readonly dialogs = inject(BfDialogService);
|
|
953
|
+
|
|
954
|
+
confirmDelete(): Promise<string | undefined> {
|
|
955
|
+
return this.dialogs.confirm({
|
|
956
|
+
title: 'Delete project?',
|
|
957
|
+
message: 'This cannot be undone.',
|
|
958
|
+
buttons: [
|
|
959
|
+
{ id: 'cancel', label: 'Cancel', role: 'cancel' },
|
|
960
|
+
{ id: 'delete', label: 'Delete', role: 'destructive' },
|
|
961
|
+
],
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
describe('BfDialogHarness in a consumer app (Vitest)', () => {
|
|
967
|
+
beforeEach(() => {
|
|
968
|
+
TestBed.configureTestingModule({ providers: [provideBfTesting()] });
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('reads a confirm() alert dialog and resolves the pressed button id', async () => {
|
|
972
|
+
const fixture = TestBed.createComponent(ProjectActionsComponent);
|
|
973
|
+
await fixture.whenStable();
|
|
974
|
+
const answer = fixture.componentInstance.confirmDelete();
|
|
975
|
+
|
|
976
|
+
const dialog = await TestbedHarnessEnvironment.documentRootLoader(fixture).getHarness(
|
|
977
|
+
BfDialogHarness.with({ label: 'Delete project?' }),
|
|
978
|
+
);
|
|
979
|
+
expect(await dialog.getRole()).toBe('alertdialog');
|
|
980
|
+
expect(await dialog.getText()).toBe('This cannot be undone.');
|
|
981
|
+
|
|
982
|
+
await dialog.clickButton('Delete');
|
|
983
|
+
await expect(answer).resolves.toBe('delete');
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### 8. Test a sheet in a Capacitor app
|
|
989
|
+
|
|
990
|
+
Under `provideBfShellTesting()` the shell's overlay services work as they do in the app. Open a sheet in an injection context, then find it in the document. A theme change stays in memory, so `localStorage` is never written, and the `dark` class is gone after the test.
|
|
991
|
+
|
|
992
|
+
`sheet.spec.ts`:
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
/*
|
|
996
|
+
* The mobile shell under a consumer's Vitest: `provideBfShellTesting()` (router, shell, in-memory
|
|
997
|
+
* theme, document cleanup); a `bf-sheet` opens into the document and is gone once dismissed, and
|
|
998
|
+
* no theme class is left on `<html>` afterwards.
|
|
999
|
+
*/
|
|
1000
|
+
import { Component, inject } from '@angular/core';
|
|
1001
|
+
import { TestBed } from '@angular/core/testing';
|
|
1002
|
+
import { BfPlatformService, BfSheetService, provideBfShellTesting } from '@bitforgehq/ui/mobile';
|
|
1003
|
+
|
|
1004
|
+
@Component({
|
|
1005
|
+
selector: 'app-sheet-body',
|
|
1006
|
+
template: `<p class="p-4">Sheet body</p>`,
|
|
1007
|
+
})
|
|
1008
|
+
class SheetBodyComponent {}
|
|
1009
|
+
|
|
1010
|
+
describe('bf-sheet in a consumer app (Vitest)', () => {
|
|
1011
|
+
beforeEach(() => {
|
|
1012
|
+
TestBed.configureTestingModule({ providers: [provideBfShellTesting()] });
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('opens a sheet into the document and removes it once dismissed', async () => {
|
|
1016
|
+
const ref = TestBed.runInInjectionContext(() =>
|
|
1017
|
+
inject(BfSheetService).open(SheetBodyComponent),
|
|
1018
|
+
);
|
|
1019
|
+
TestBed.inject(BfPlatformService).theme.set('dark');
|
|
1020
|
+
TestBed.tick();
|
|
1021
|
+
|
|
1022
|
+
expect(document.querySelector('bf-sheet')?.textContent).toContain('Sheet body');
|
|
1023
|
+
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
|
1024
|
+
expect(window.localStorage.getItem('bf-theme')).toBeNull();
|
|
1025
|
+
|
|
1026
|
+
ref.close();
|
|
1027
|
+
await ref.afterClosed;
|
|
1028
|
+
|
|
1029
|
+
expect(document.querySelector('bf-sheet')).toBeNull();
|
|
1030
|
+
expect(document.body.textContent).not.toContain('Sheet body');
|
|
1031
|
+
|
|
1032
|
+
// What runs after each test: the environment's teardown clears the theme class.
|
|
1033
|
+
TestBed.resetTestingModule();
|
|
1034
|
+
expect(document.documentElement.classList.contains('dark')).toBe(false);
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### 9. Assert on roles, text and values
|
|
1040
|
+
|
|
1041
|
+
Test what a user and the rest of the app see:
|
|
1042
|
+
|
|
1043
|
+
- the role and accessible name of an element, or a harness found by its label;
|
|
1044
|
+
- the text a widget shows, such as its validation message;
|
|
1045
|
+
- the values it writes into the form model or emits from its outputs.
|
|
1046
|
+
|
|
1047
|
+
Don't assert on the library's CSS classes or internal markup. They are styling details, not API, and they change with the design.
|
|
1048
|
+
|
|
1049
|
+
## Design foundations (Meadow Sage)
|
|
1050
|
+
|
|
1051
|
+
Warm off-white canvas (`bg-bg`), white surfaces, sage green primary, pill-shaped controls,
|
|
1052
|
+
radii 12/16/24 px (`rounded-tile/field/card`), 1.5 px control borders, soft layered shadows
|
|
1053
|
+
(`shadow-raised`, `shadow-floating`), Manrope type (`text-display/title/heading/body/caption`).
|
|
1054
|
+
|
|
1055
|
+
### Type and spacing scale
|
|
1056
|
+
|
|
1057
|
+
- Type (rem tokens; px at a 16 px root): `text-display` 40, `text-title` 28, `text-heading` 18,
|
|
1058
|
+
`text-body` 15/24 and `text-label` 15/20, `text-label-sm` 14, `text-caption` 13/18 and
|
|
1059
|
+
`text-label-xs` 13/16, `text-micro` 12 (weight 600), `text-nano` 11. Never `text-[15px]` or
|
|
1060
|
+
Tailwind's `text-sm`/`text-xs`.
|
|
1061
|
+
- Spacing: 2 · 4 · 6 · 8 · 10 · 12 · 14 · 16 · 18 · 20 · 24 · 32 · 48 px as `--spacing` multiples
|
|
1062
|
+
(`p-3.5`, `px-4.5`); never arbitrary px. Semantic steps: `gutter` 16, `card` 18,
|
|
1063
|
+
`control-sm/md/lg` 32/40/48, `hit` 44 (`min-h-hit`), `box` 22 (`size-box`).
|
|
1064
|
+
- Also `rounded-checkbox` (7 px), `bg-scrim` (black in both modes), `ease-ios`/`ease-md`/`ease-settle`.
|
|
1065
|
+
|
|
1066
|
+
## Component index
|
|
1067
|
+
|
|
1068
|
+
Grouped by category. Each page in `ai/components/<name>.md` follows the same order as the docs
|
|
1069
|
+
site: overview, usage sections with examples, inputs, outputs, slots, dos and don'ts, migration
|
|
1070
|
+
notes.
|
|
1071
|
+
|
|
1072
|
+
### Actions
|
|
1073
|
+
|
|
1074
|
+
| Component | Entry point | Purpose |
|
|
1075
|
+
| ---------------- | ---------------- | ------------------------------------------------------------------------------ |
|
|
1076
|
+
| `bf-action-bar` | `@bitforgehq/ui` | Floating pill of contextual actions that appears while items are selected |
|
|
1077
|
+
| `bf-button` | `@bitforgehq/ui` | Pill-shaped action button with five variants, three sizes and a loading state |
|
|
1078
|
+
| `bf-icon-button` | `@bitforgehq/ui` | Round icon-only button for toolbars and rows, with a required accessible label |
|
|
1079
|
+
|
|
1080
|
+
### Forms
|
|
1081
|
+
|
|
1082
|
+
| Component | Entry point | Purpose |
|
|
1083
|
+
| ----------------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
|
|
1084
|
+
| `bf-checkbox` | `@bitforgehq/ui` | Rounded checkbox with an optional label that binds to Signal Forms |
|
|
1085
|
+
| `bf-chip` | `@bitforgehq/ui` | Toggleable filter chip with a check mark when selected, in tonal or ink style |
|
|
1086
|
+
| `bf-chip-group` | `@bitforgehq/ui` | Named row of chips that picks one like radios, or groups independent toggles |
|
|
1087
|
+
| `bf-field` | `@bitforgehq/ui` | Label, help text and error text wrapper that wires ids for assistive tech |
|
|
1088
|
+
| `bf-input` | `@bitforgehq/ui` | Single-line text field with label, help and error text, icon slots and Signal Forms binding |
|
|
1089
|
+
| `bf-number-input` | `@bitforgehq/ui/number-input` | Number field that binds number or null, with native min, max and step |
|
|
1090
|
+
| `bf-radio-group` | `@bitforgehq/ui` | Group of radio buttons for choosing one option, with optional per-option descriptions |
|
|
1091
|
+
| `bf-segmented` | `@bitforgehq/ui` | Segmented control for switching between two to four views, keyboard-navigable like radios |
|
|
1092
|
+
| `bf-select` | `@bitforgehq/ui` | Native single-choice dropdown styled like the text field, with Signal Forms binding |
|
|
1093
|
+
| `bf-textarea` | `@bitforgehq/ui` | Multi-line text field with label, help and error text, and Signal Forms binding |
|
|
1094
|
+
| `bf-toggle` | `@bitforgehq/ui` | On/off switch for settings that apply immediately, with Signal Forms binding |
|
|
1095
|
+
|
|
1096
|
+
### Data display
|
|
1097
|
+
|
|
1098
|
+
| Component | Entry point | Purpose |
|
|
1099
|
+
| -------------- | ---------------- | --------------------------------------------------------------------------------------- |
|
|
1100
|
+
| `bf-avatar` | `@bitforgehq/ui` | Round avatar showing a photo or initials, in three sizes and four color variants |
|
|
1101
|
+
| `bf-badge` | `@bitforgehq/ui` | Small status label in semantic tones, or a compact numeric pill for counts |
|
|
1102
|
+
| `bf-card` | `@bitforgehq/ui` | Content surface with an optional header slot, in four visual styles from raised to flat |
|
|
1103
|
+
| `bf-list` | `@bitforgehq/ui` | Container for list rows, inset by default with rounded corners on a warm background |
|
|
1104
|
+
| `bf-list-item` | `@bitforgehq/ui` | List row with leading slot, title, subtitle, trailing slot and an optional chevron |
|
|
1105
|
+
| `bf-progress` | `@bitforgehq/ui` | Progress bar in two sizes and four tones for completion, quotas and unknown waits |
|
|
1106
|
+
|
|
1107
|
+
### Feedback
|
|
1108
|
+
|
|
1109
|
+
| Component | Entry point | Purpose |
|
|
1110
|
+
| ---------------- | ---------------- | ---------------------------------------------------------------------------------- |
|
|
1111
|
+
| `bf-alert` | `@bitforgehq/ui` | Inline status message in four tones with a title and an optional dismiss button |
|
|
1112
|
+
| `bf-empty-state` | `@bitforgehq/ui` | Placeholder for an empty screen or list with icon, title, message and action |
|
|
1113
|
+
| `bf-spinner` | `@bitforgehq/ui` | Indeterminate loading spinner in three sizes that inherits its parent's text color |
|
|
1114
|
+
|
|
1115
|
+
### Navigation
|
|
1116
|
+
|
|
1117
|
+
| Component | Entry point | Purpose |
|
|
1118
|
+
| ----------------- | ----------------------- | ------------------------------------------------------------------------------------------- |
|
|
1119
|
+
| `bf-back-button` | `@bitforgehq/ui/mobile` | Header back button that pops the page stack or falls back to a default route |
|
|
1120
|
+
| `bf-content` | `@bitforgehq/ui/mobile` | Page's single scroll container that keeps focused fields clear of the soft keyboard |
|
|
1121
|
+
| `bf-header` | `@bitforgehq/ui/mobile` | Page header toolbar with a large title that collapses into the toolbar on scroll |
|
|
1122
|
+
| `bf-page` | `@bitforgehq/ui/mobile` | Frame for one stacked mobile screen that supplies lifecycle events and shared page state |
|
|
1123
|
+
| `bf-refresher` | `@bitforgehq/ui/mobile` | Pull-to-refresh indicator with haptic feedback, native iOS bounce and an Android-style pull |
|
|
1124
|
+
| `bf-stack-outlet` | `@bitforgehq/ui/mobile` | Router outlet keeping a live page stack with slide transitions and iOS swipe back |
|
|
1125
|
+
| `bf-tab` | `@bitforgehq/ui` | One tab bar destination with an icon, a label and an optional count badge |
|
|
1126
|
+
| `bf-tab-bar` | `@bitforgehq/ui` | Floating pill tab bar for mobile apps with arrow-key selection and count badges |
|
|
1127
|
+
| `bf-tabs` | `@bitforgehq/ui/mobile` | Tabs page with one navigation stack per tab, wiring the tab bar to the router |
|
|
1128
|
+
|
|
1129
|
+
### Overlays
|
|
1130
|
+
|
|
1131
|
+
| Component | Entry point | Purpose |
|
|
1132
|
+
| ------------------ | ------------------------ | --------------------------------------------------------------------------------------------- |
|
|
1133
|
+
| `bf-action-sheet` | `@bitforgehq/ui/mobile` | Bottom panel of labelled actions with a separate cancel button, destructive ones shown in red |
|
|
1134
|
+
| `bf-alert-dialog` | `@bitforgehq/ui/mobile` | Centred confirmation dialog with a title, message and a row of role-styled buttons |
|
|
1135
|
+
| `bf-desktop-toast` | `@bitforgehq/ui/overlay` | Desktop notification card in a bottom corner stack of up to three |
|
|
1136
|
+
| `bf-dialog` | `@bitforgehq/ui/overlay` | Desktop modal dialog with a title, scrolling body, footer actions and focus trap |
|
|
1137
|
+
| `bf-menu` | `@bitforgehq/ui/overlay` | Keyboard-operable menu panel with items, checkbox and radio items, groups and submenus |
|
|
1138
|
+
| `bf-menu-button` | `@bitforgehq/ui/overlay` | Styled button that opens a menu, with outline, text, tonal and icon-only looks |
|
|
1139
|
+
| `bf-modal` | `@bitforgehq/ui/mobile` | Modal overlay shown as an inset card over the scaled-down page, or full screen |
|
|
1140
|
+
| `bf-sheet` | `@bitforgehq/ui/mobile` | Bottom sheet with snap points, drag to dismiss and a backdrop |
|
|
1141
|
+
| `bf-toast` | `@bitforgehq/ui/mobile` | Passive status pill with an optional action that never steals focus |
|
|
1142
|
+
|
|
1143
|
+
### Icons
|
|
1144
|
+
|
|
1145
|
+
| Component | Entry point | Purpose |
|
|
1146
|
+
| --------- | ---------------------- | ------------------------------------------------------------------------------------- |
|
|
1147
|
+
| `bf-icon` | `@bitforgehq/ui/icons` | Tree-shakable Lucide icon renderer, decorative by default or labelled when meaningful |
|
|
1148
|
+
|
|
1149
|
+
## Upgrading
|
|
1150
|
+
|
|
1151
|
+
Each component page ends with its migration notes per version. `MANIFEST.json` marks deprecated
|
|
1152
|
+
members with their replacement.
|
|
1153
|
+
|
|
1154
|
+
0.3.0 moved the overlay core (`BfOverlayService`, `BfOverlayRef`, `BfOverlayStack`, the animations,
|
|
1155
|
+
…) from `@bitforgehq/ui/mobile` to the new `@bitforgehq/ui/overlay` entry; `/mobile`'s old exports
|
|
1156
|
+
are deprecated aliases of the same classes and tokens, removed in 0.4.0. The package
|
|
1157
|
+
`CHANGELOG.md` (shipped in `@bitforgehq/ui`), `## 0.3.0`, `### overlay entry` carries the full
|
|
1158
|
+
old-import → new-import table — run a project-wide replace of the import specifier for every name
|
|
1159
|
+
in it rather than editing one call site at a time.
|