@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,3665 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@bitforgehq/ui`. One `##` section per version; one `###` sub-section per touched component (the agent-docs generator lifts those into each component's page).
|
|
4
|
+
|
|
5
|
+
The heading for the version being worked on carries `(unreleased)` in place of a date. At release time, that placeholder is replaced with the release date (`## 0.3.1 (unreleased)` becomes `## 0.3.1 (YYYY-MM-DD)`) in the same commit that bumps `projects/ui/package.json`. See "Releasing" in the root `README.md`. 0.3.0 is the first version published to npm; the 0.2.0 and 0.1.0 sections below are the history of versions that were never published.
|
|
6
|
+
|
|
7
|
+
## 0.3.0 (2026-09-28)
|
|
8
|
+
|
|
9
|
+
### Migration
|
|
10
|
+
|
|
11
|
+
Breaking changes from 0.2.x, each with its fix:
|
|
12
|
+
|
|
13
|
+
1. Fields show default text for errors that carry no `message` (Signal Forms validators without
|
|
14
|
+
`{ message }`, Reactive `Validators.*`). Before: a touched invalid field turned red with no
|
|
15
|
+
text unless you passed `errorMessage`. After: it shows its kind's default text ("Fill in this
|
|
16
|
+
field", "Use at least 5 characters", …). Fix: translate with
|
|
17
|
+
`provideBfLabels({ validation: {...} })`, or keep passing `errorMessage`.
|
|
18
|
+
2. Text fields default to the compact `md` step. Before: `bf-input`, `bf-select` and `bf-textarea`
|
|
19
|
+
defaulted to `lg` (48 px, 16 px sides). After: `md` (40 px, 14 px sides, 14 px type; the
|
|
20
|
+
textarea's padding and type). Fix: Capacitor apps add `provideBfDefaults({ fieldSize: 'lg' })`
|
|
21
|
+
to their `ApplicationConfig.providers` to keep the 0.2 look, or `provideBfDefaults({ size: 'lg' })`
|
|
22
|
+
to enlarge every control; a single field takes `size="lg"`.
|
|
23
|
+
3. `bf-segmented` heights follow the shared scale. Before: `md` (default) was a 44 px track (36 px
|
|
24
|
+
segments), `sm` a 40 px track. After: the track is `sm` 32, `md` 40, `lg` 48 px. Fix:
|
|
25
|
+
`size="lg"` (48) where the old 44 px control must stay touch-sized, or
|
|
26
|
+
`provideBfDefaults({ controlSize: 'lg' })` app-wide.
|
|
27
|
+
4. `bf-chip` has a `size`. Before: a fixed 36 px pill. After: `md` 40 px by default (`sm` 32,
|
|
28
|
+
`lg` 48). Fix: `size="sm"` for a 32 px filter row.
|
|
29
|
+
5. `BfFieldSize`, `BfButtonSize` and `BfSegmentedSize` are deprecated aliases of `BfSize`. Before:
|
|
30
|
+
separate unions (`BfFieldSize` `'md' | 'lg'`, `BfSegmentedSize` `'sm' | 'md'`). After: all three
|
|
31
|
+
are `BfSize` (`'sm' | 'md' | 'lg'`). Fix: no action needed unless you switch exhaustively over
|
|
32
|
+
`BfFieldSize` or `BfSegmentedSize` (add the new step); import `BfSize` from `@bitforgehq/ui`
|
|
33
|
+
going forward, as the aliases are removed in 1.0.0.
|
|
34
|
+
6. "Nothing selected" is `null`, not `''`, in `bf-select`, `bf-radio-group` and `bf-segmented`.
|
|
35
|
+
Before: `value` was a `string` defaulting to `''`, and `bf-select` showed its placeholder for any
|
|
36
|
+
falsy value. After: the controls are generic (`BfSelectComponent<T = string>`, …) with
|
|
37
|
+
`value: T | null` defaulting to `null`; an option whose value is `''` is now a real, selectable
|
|
38
|
+
option, and the placeholder shows only while no option matches. `[formField]` and
|
|
39
|
+
`[(value)]` bindings to a `string` field or signal keep compiling (Angular does not type-check
|
|
40
|
+
the write-back of a two-way binding); an explicit `(valueChange)="x.set($event)"` into a
|
|
41
|
+
`string` signal no longer compiles (`string | null` is not assignable to `string`). Fix: type
|
|
42
|
+
the bound signal or field `T | null` and start it at `null` (a `required()` rule treats `null`
|
|
43
|
+
as empty), compare against `null` instead of `''`, or listen to `(changed)`, which emits only a
|
|
44
|
+
chosen, never-null `T`.
|
|
45
|
+
7. Object option values need `compareWith`. Before: options matched by `===` on strings only.
|
|
46
|
+
After: a value is matched to an option with `Object.is` by default, so an object value that is
|
|
47
|
+
equal but not the same instance (e.g. reloaded from the server) selects nothing. Fix: pass
|
|
48
|
+
`[compareWith]="byId"` with `byId = (a: Team, b: Team) => a.id === b.id`.
|
|
49
|
+
8. Numbers bind to `bf-number-input`. Before: a numeric field used `bf-input type="number"` with a
|
|
50
|
+
`string` model and could not use Signal Forms `min()`/`max()`. After: `bf-number-input` binds
|
|
51
|
+
`number | null` (empty is `null`), takes `min`/`max`/`step`, and reports unreadable text as a
|
|
52
|
+
`parse` error. Fix: replace `<bf-input type="number" [(value)]="qty">` and its string model with
|
|
53
|
+
`<bf-number-input [(value)]="qty">` (import `BfNumberInputComponent` from
|
|
54
|
+
`@bitforgehq/ui/number-input`) and `qty = signal<number | null>(…)`;
|
|
55
|
+
`bf-input type="number"` still works and stays string-typed.
|
|
56
|
+
9. Object option values render their index as the native value. Before: every option value was a
|
|
57
|
+
string, rendered as `<option value="…">` and as the radio's `value`. After: string, number and
|
|
58
|
+
boolean values still render as themselves (`"42"` for `42`); an object value renders its option
|
|
59
|
+
index (`"0"`, `"1"`, …) in `bf-select` and `bf-radio-group`. Fix: for object options, pick by
|
|
60
|
+
label in browser tests (`selectOption({ label })`) and post the model, not native form data.
|
|
61
|
+
10. `bf-segmented`'s public `tabStop` is an index. Before: the value of the segment that owned the
|
|
62
|
+
tab stop (`''` when none). After: its option index (`-1` when every option is disabled). Fix:
|
|
63
|
+
read `options()[tabStop()]?.value` where you read `tabStop()`.
|
|
64
|
+
11. Inside `bf-tabs` the tab bar is a `nav` of links, not a `tablist`. Before: `nav` >
|
|
65
|
+
`role="tablist"` > `button role="tab"` with `aria-selected`, a roving tab stop and arrow-key
|
|
66
|
+
selection. After: `nav` > plain group > `<a href>` per tab with `aria-current` on the active
|
|
67
|
+
one (`page` at the tab's root page, `true` on a child page of it); every tab is in the tab order
|
|
68
|
+
and the arrow keys do nothing (Tab moves between tabs). In Safari and iOS WKWebView (the
|
|
69
|
+
Capacitor iOS app) Tab reaches links only with "Press Tab to highlight each item" turned on
|
|
70
|
+
(Option+Tab otherwise), so a hardware-keyboard user who reached the old tab buttons with Tab
|
|
71
|
+
now needs Option+Tab there; VoiceOver and Full Keyboard Access are unaffected. A standalone
|
|
72
|
+
`bf-tab-bar`, and one inside a routed page under `bf-tabs`, is unchanged. Fix: in tests and
|
|
73
|
+
scripts, query `getByRole('link', { name: 'Projects' })` instead of `getByRole('tab', …)` and
|
|
74
|
+
assert `aria-current` (`page`, or `true` below the tab's root) instead of
|
|
75
|
+
`aria-selected="true"`; select a tab with a click or Enter, not the arrow keys.
|
|
76
|
+
12. `bf-card [interactive]` and its `clicked` output are deprecated. Before:
|
|
77
|
+
`<bf-card [interactive]="true" (clicked)="open()">` made the whole card one `<button>`, hiding
|
|
78
|
+
headings and nesting controls. After: they still work in 0.3 and are removed in 1.0.0. Fix: put
|
|
79
|
+
a `bfAction` link or button in the card's heading,
|
|
80
|
+
`<h3><a bfAction routerLink="/projects/atlas">Atlas rebrand</a></h3>` (import
|
|
81
|
+
`BfActionDirective`), and give other controls in the card `class="relative z-1"`.
|
|
82
|
+
13. Custom themes set `--color-primary-text`. Before: the text button, the focused field label
|
|
83
|
+
and the iOS back button used `--color-primary` as text, so a light brand primary (yellow, light
|
|
84
|
+
green) failed 4.5:1. After: they use the new `--color-primary-text` (`#2b26ba` light, `#8c88f5`
|
|
85
|
+
dark by default), which the library cannot derive from your override. Fix: an app that
|
|
86
|
+
overrides `--color-primary` adds `--color-primary-text` (a shade of it with ≥ 4.5:1 on `bg` and
|
|
87
|
+
`surface`) to its `@theme` and `.dark` blocks; regenerate the snippet with the docs site's
|
|
88
|
+
theme configurator, which also derives a contrast-safe `--color-primary-strong`. Optionally set
|
|
89
|
+
`--radius-control` (default `9999px`) to move buttons, chips, badges, segmented, the tab bar,
|
|
90
|
+
the action bar and toggles off the pill.
|
|
91
|
+
14. `BfTabBarHost` and `BfTabLinks` gained required members. Before: `BfTabBarHost` had `active`,
|
|
92
|
+
`tabStop` and `select`. After: it also requires `linkMode`, `linkHref(id)` and
|
|
93
|
+
`linkIsRoot(id)`, and the new `BfTabLinks` (`BF_TAB_LINKS`) requires `href(id)` and
|
|
94
|
+
`isRoot(id)`. Fix: only a class that implements `BfTabBarHost` itself (a custom bar providing
|
|
95
|
+
`BF_TAB_BAR`) breaks; add `readonly linkMode = false`, `linkHref = () => null` and
|
|
96
|
+
`linkIsRoot = () => true`.
|
|
97
|
+
15. `bf-alert`'s role follows `urgent`, not `variant`. Before: a `danger` alert was
|
|
98
|
+
`role="alert"` (assertive), every other variant `role="status"`. After: every alert is
|
|
99
|
+
`status` unless it has `urgent`, in any variant. Fix: add `urgent` to danger alerts that must
|
|
100
|
+
interrupt; in tests, query `getByRole('status')` for the others.
|
|
101
|
+
16. `bf-alert`'s dismiss button is named after the title. Before: "Dismiss" (or `dismissLabel`).
|
|
102
|
+
After: "Dismiss: Sync failed" when the alert has a `title`, the label alone without one. Fix:
|
|
103
|
+
in tests, query `getByRole('button', { name: 'Dismiss: Sync failed' })`, or match the label as
|
|
104
|
+
a prefix.
|
|
105
|
+
17. Alert dialogs and action sheets focus the cancel button first. Before: focus landed on the
|
|
106
|
+
first button (`BfAlertDialogService`) or the first row (`BfActionSheetService`). After: it lands
|
|
107
|
+
on the `cancel`-role button (an action sheet always has one), else the first button. Fix: in
|
|
108
|
+
tests, stop assuming the first button is focused; press Tab or click the button you mean.
|
|
109
|
+
18. `BfAlertDialogService.open()` returns `Promise<string | undefined>`. Before: `Promise<string>`;
|
|
110
|
+
an alert without a cancel button could only be answered by tapping a button. After: a
|
|
111
|
+
navigation to another URL path closes every alert, resolving the cancel button's answer, or
|
|
112
|
+
`undefined` when it has no cancel button. Fix: handle `undefined` like cancel (for example
|
|
113
|
+
`if (answer === 'delete')` already does); where a `string` is required, write
|
|
114
|
+
`(await alerts.open(…)) ?? 'cancel'`, or pass `closeOnNavigation: false` to keep the alert open
|
|
115
|
+
until a button is tapped.
|
|
116
|
+
19. Overlays close when the URL path changes. Before: a sheet, modal, action sheet or alert stayed
|
|
117
|
+
open over the next page after a push, a pop, a tab switch or a deep link. After: they close
|
|
118
|
+
(`afterClosed` resolves `undefined`, an action sheet `undefined`, an alert its cancel answer);
|
|
119
|
+
query- and fragment-only changes do not close them, and toasts never close. An overlay opened
|
|
120
|
+
before the first navigation ends (from the root component, an `APP_INITIALIZER` or a
|
|
121
|
+
shell-level "What's new" sheet) closes when the start URL redirects (`''` to `'today'`). On
|
|
122
|
+
the web, browser back closes an open form sheet or modal before a route `canDeactivate` guard
|
|
123
|
+
runs. Fix: pass `closeOnNavigation: false` to an overlay that must stay open across a
|
|
124
|
+
navigation; open a start-up overlay after the first `NavigationEnd`; persist a draft that
|
|
125
|
+
must survive browser back.
|
|
126
|
+
20. `/mobile` overlays outside `provideBfShell()` make the page `inert`. Before: opening a sheet,
|
|
127
|
+
modal, action sheet or alert without the shell left the rest of the page interactive. After:
|
|
128
|
+
every direct child of `<body>` becomes `inert` while one is open, except the CDK overlay
|
|
129
|
+
container, the CDK live announcer and `AriaDescriber` containers,
|
|
130
|
+
`script`/`style`/`template`/`link`, and body children marked `data-bf-overlay-keep`. Fix: mark
|
|
131
|
+
an element that must stay usable outside the overlay (a body-level chat widget, your own live
|
|
132
|
+
region) `data-bf-overlay-keep`.
|
|
133
|
+
21. `/mobile` overlays outside `provideBfShell()` register no hardware back handler. Before: the
|
|
134
|
+
Android back button closed a sheet, modal, action sheet or alert through
|
|
135
|
+
`BfBackButtonService` alone. After: overlays reach the back button only through the new
|
|
136
|
+
optional `BF_OVERLAY_HOST` token, which `provideBfShell()` provides; without it, back falls
|
|
137
|
+
through to the page. Fix: an app that uses `/mobile` overlays without `provideBfShell()` and
|
|
138
|
+
relies on Android back closing them provides `BF_OVERLAY_HOST` itself.
|
|
139
|
+
22. `provideBfShell()` provides no service itself. Before: it listed `BfNavController`, the
|
|
140
|
+
platform services and every overlay service, so a lazy route's `providers: [provideBfShell()]`
|
|
141
|
+
created its own copies in that route's environment injector. After: all of them are
|
|
142
|
+
`providedIn: 'root'` and resolve from the root injector even under a lazy
|
|
143
|
+
`provideBfShell()`; only the config, the route reuse strategy and `BF_OVERLAY_HOST` stay in
|
|
144
|
+
that injector. Fix: put `provideBfShell()` in the application providers (as the setup guide
|
|
145
|
+
does); nothing changes there.
|
|
146
|
+
23. Deprecated output names. Before: `bf-chip` `(toggled)`, `bf-tab-bar` `(tabChange)` and
|
|
147
|
+
`(tabReselect)`, `bf-stack-outlet` `(stackChange)`, `(transitionStart)` and `(transitionEnd)`,
|
|
148
|
+
`bf-refresher` `(refresh)`. After: one naming convention (outputs are past-tense events; a value
|
|
149
|
+
control has one `changed`) names them `(changed)`, `(changed)` and `(reselected)`,
|
|
150
|
+
`(stackChanged)`, `(transitionStarted)` and `(transitionEnded)`, and `(refreshRequested)`; the
|
|
151
|
+
old names stay as deprecated aliases that emit the same payload at the same moment. Fix: rename
|
|
152
|
+
the binding (`(toggled)="x()"` → `(changed)="x()"`, a subscription `bar.tabChange` →
|
|
153
|
+
`bar.changed`); the old names keep emitting until 0.4.0, where they are removed.
|
|
154
|
+
24. `bf-tabs` no longer writes the bar's `active`, so `(activeChange)` and `[(active)]` on a bar
|
|
155
|
+
inside `bf-tabs` no longer fire. Before: `bf-tabs` set the projected `bf-tab-bar`'s `active`
|
|
156
|
+
to the current tab (and back after a refused navigation), which emitted `activeChange`, and
|
|
157
|
+
hid the bar with `setHidden()`. After: the bar derives its selection and hiding from the new
|
|
158
|
+
`BF_TAB_BAR_CONTROLLER` that `bf-tabs` provides; `active` keeps its initial value and
|
|
159
|
+
`activeChange` stays silent. Fix: listen to `(changed)` instead of `(activeChange)`, and read
|
|
160
|
+
the shown tab from the bar's `selected()` or `BfTabsComponent.current()`/`tabKey()` instead of
|
|
161
|
+
`active()`.
|
|
162
|
+
25. `bf-field` keeps its help text while an error shows. Before: the error paragraph replaced the
|
|
163
|
+
help paragraph, and `describedBy()` and the controls' `aria-describedby` pointed at one of
|
|
164
|
+
them. After: the help paragraph stays and the error renders under it; `aria-describedby` lists
|
|
165
|
+
both (`"<id>-help <id>-error"`), and every control that shows an error sets
|
|
166
|
+
`aria-errormessage="<id>-error"` while `aria-invalid="true"`. An error that appears while a
|
|
167
|
+
pointer button is held down shows once the press ends. Fix: shorten help text that repeated the
|
|
168
|
+
error, and update a test or a custom control that expected `describedBy()` to be the error id
|
|
169
|
+
alone (read the ids with `split(' ')`).
|
|
170
|
+
26. `bf-spinner` draws a CSS ring instead of an SVG, and does not rotate under reduced motion.
|
|
171
|
+
Before: an inline `<svg>` (`width`/`height` 16/20/24, a faint `<circle>` track and a `<path>`
|
|
172
|
+
arc) with `animate-spin`, spinning even under `prefers-reduced-motion`. After: a
|
|
173
|
+
`<span data-bf-spinner-ring aria-hidden="true">` ring with a `currentColor` border, inset by a
|
|
174
|
+
1 px margin in the same 16/20/24 px box (`motion-safe:animate-spin`), which pulses its
|
|
175
|
+
opacity under reduced motion (`motion-reduce:animate-pulse`). The `px` member (the box in px)
|
|
176
|
+
is deprecated. Fix: move CSS or test selectors from `bf-spinner svg` (`circle`, `path`,
|
|
177
|
+
`[width]`) to `bf-spinner [data-bf-spinner-ring]`, and read the `size` input instead of
|
|
178
|
+
`px()`, which keeps returning 16/20/24 until it is removed in 0.4.0; colour the spinner
|
|
179
|
+
through its parent's text colour as before.
|
|
180
|
+
27. `bf-icon` keeps the fills Lucide sets. Before: the renderer dropped `fill`, so icons with
|
|
181
|
+
filled details lost them (the dots of `Palette`, the points of `ChartScatter`, the hole of
|
|
182
|
+
`KeyRound`, 10 distinct icons in all). After: those shapes render as solid `currentColor`. Fix:
|
|
183
|
+
nothing, unless an app drew the missing dots itself or styled `bf-icon circle { fill }`;
|
|
184
|
+
drop that.
|
|
185
|
+
28. A dismissible sheet has two controls of its own ahead of its content. Before: the sheet's
|
|
186
|
+
first focusable element was the content's. After: a visually hidden "Close" button
|
|
187
|
+
(`[data-bf-sheet-close]`) comes first, then, with two or more snap points, the grabber button
|
|
188
|
+
(`[data-bf-sheet-grabber-button]`, "Expand"/"Collapse"); initial focus never lands on them:
|
|
189
|
+
it goes to the panel (the default) or, with `autoFocus: 'first-field'`, to the content's first
|
|
190
|
+
field (see Migration 33). Fix: tests that count a sheet's buttons or walk its Tab order skip
|
|
191
|
+
the two (`:not([data-bf-sheet-close]):not([data-bf-sheet-grabber-button])`); keep a visible
|
|
192
|
+
close or cancel control in the content.
|
|
193
|
+
29. The page-sheet `bf-modal` starts below the status bar. Before: the card sat 12 px from the
|
|
194
|
+
viewport top (`top-3`) and its `bf-header` padded itself by the top safe area. After: the
|
|
195
|
+
card's top is the safe-area top plus 10 px, and a `bf-header` (or `fullscreen` `bf-content`)
|
|
196
|
+
inside it adds no safe-area padding (`BfPageContext.safeAreaTop` false). Fix: drop any
|
|
197
|
+
padding an app added to make up for either; a fullscreen modal is unchanged. A `bf-page`
|
|
198
|
+
inside a sheet modal's content is not supported (its fresh context brings the band back); the
|
|
199
|
+
modal docs say so.
|
|
200
|
+
30. Native system bars follow the in-app theme. Before: the shell left the status-bar and
|
|
201
|
+
navigation-bar style to the app. After: under `provideBfShell()` on native iOS and Android,
|
|
202
|
+
`SystemBars.setStyle()` follows `BfPlatformService.dark()` at launch and on every theme
|
|
203
|
+
change (`Default` on Android WebView < 140). Fix: an app that styles the bars itself passes
|
|
204
|
+
`provideBfShell({ syncSystemBars: false })`.
|
|
205
|
+
31. `bf-header` collapses its large title over the title's rendered height. Before: the collapse
|
|
206
|
+
ran over a fixed 56 px of scroll (the iOS large title is about 70 px tall, so it finished
|
|
207
|
+
early). After: the range is the large title block's measured height, at most 200 px (a taller
|
|
208
|
+
title collapses its whole height over 200 px, so faster than it scrolls), at least 44 px, and
|
|
209
|
+
56 px until measured; the snap goes to it; a page whose rest range is shorter keeps the title
|
|
210
|
+
expanded; `data-bf-scrolled` still switches on at full collapse, now at that height. At compact
|
|
211
|
+
heights (`(max-height: 500px)`) there is no large title and no collapse. Fix: tests or styles
|
|
212
|
+
that scroll 56 px to collapse the header scroll by the large title's height (`.bf-large` block
|
|
213
|
+
`offsetHeight`, capped at 200) instead.
|
|
214
|
+
32. `bf-list-item` titles wrap. Before: the title, the title slot and the subtitle each truncated
|
|
215
|
+
on one line. After: long ones wrap (`wrap-anywhere`), so no text is lost at large text sizes.
|
|
216
|
+
Fix: add `truncate` to rows that should keep one line (`<bf-list-item truncate>`).
|
|
217
|
+
33. Sheets focus their panel on open. Before: `BfSheetService.open()` focused the content's first
|
|
218
|
+
focusable element, so a sheet with a text field raised the Android keyboard at once. After:
|
|
219
|
+
focus goes to the sheet itself (the named `role="dialog"` pane, `tabindex="-1"`), new option
|
|
220
|
+
`autoFocus: 'panel' | 'first-field'` (default `'panel'`). Fix: pass `autoFocus: 'first-field'`
|
|
221
|
+
for a sheet that exists to be typed into; tests that expected the first field focused focus it
|
|
222
|
+
themselves or pass the option.
|
|
223
|
+
34. Android back at a stack root backgrounds the app. Before: when no overlay and no stack could go
|
|
224
|
+
back, `BfBackButtonService` called `App.exitApp()`, so the next launch started cold. After: it
|
|
225
|
+
calls `App.minimizeApp()`, and a relaunch resumes warm on the same tab. Fix: pass
|
|
226
|
+
`provideBfShell({ backAtRoot: 'exit' })` to keep finishing the activity.
|
|
227
|
+
35. Overlays close on a browser's close request. Before: Android back in Chrome or a PWA (no
|
|
228
|
+
Capacitor) went back in history under an open overlay. After: where `CloseWatcher` exists,
|
|
229
|
+
each non-passive overlay creates one, and a close request runs its `Escape` path (the
|
|
230
|
+
overlay closes, or `onEscape` runs); the URL stays. Fix: nothing; an overlay that must survive
|
|
231
|
+
Android web back passes `dismissOnEscape: false`.
|
|
232
|
+
36. The shown page's `bf-content` is the `main` landmark. Before: the shell rendered no `main`.
|
|
233
|
+
After: the scroller of the page a `bf-stack-outlet` shows has `role="main"` (not inside
|
|
234
|
+
`bf-modal`, and not for a `bf-page` outside an outlet), and while it overflows with nothing
|
|
235
|
+
focusable inside it is a tab stop (`tabindex="0"`). Fix: remove a `main` element or
|
|
236
|
+
`role="main"` an app put around the outlet or inside its pages, so there is one `main`; tests
|
|
237
|
+
that count `main` landmarks or Tab stops expect these.
|
|
238
|
+
37. `bfHorizontalScroll` is deprecated. Before: the docs said a horizontal scroller in a stack page
|
|
239
|
+
needed it. After: a horizontal scroller pans on its own (`touch-action` is not inherited past
|
|
240
|
+
a scroll container); the directive still sets `touch-action: pan-x pan-y` until 0.4.0. Fix:
|
|
241
|
+
remove `bfHorizontalScroll` and its import.
|
|
242
|
+
38. `bf-segmented` keeps its labels whole and scrolls sideways when they cannot fit. Before: a
|
|
243
|
+
label broke anywhere (`wrap-anywhere`), letter by letter at the largest text sizes ("S y st e
|
|
244
|
+
m" at iOS AX5), and the side padding was rem (it grew with Dynamic Type). After: the side
|
|
245
|
+
padding is px (12/14/18, the same at the default size), a segment shrinks only to its longest
|
|
246
|
+
word (`min-w-min`), labels wrap between words, and when the segments still cannot fit (iOS
|
|
247
|
+
235 % on a phone) the control scrolls sideways inside a new `[data-bf-segmented-scroller]`
|
|
248
|
+
wrapper around the track. Fix: nothing; tests that read the track as the host's first child
|
|
249
|
+
look inside the scroller, and styles that set `px-*` on segments use px values.
|
|
250
|
+
39. The iOS edge swipe-back reacts to touch only. Before: any primary pointer that pressed in the
|
|
251
|
+
left 50 px edge zone and dragged right armed it, so a mouse drag (a desktop page with the mode
|
|
252
|
+
forced to `ios`) popped the page. After: it arms only for `pointerType` `touch`; a mouse, a
|
|
253
|
+
text selection from the edge and a pen never pop. Fix: nothing in an app (a desktop user goes
|
|
254
|
+
back with the header's back button); a test that simulates swipe-back dispatches its
|
|
255
|
+
`pointerdown`/`pointermove`/`pointerup` with `pointerType: 'touch'` (as
|
|
256
|
+
`e2e/navigation.spec.ts` does) instead of a mouse drag.
|
|
257
|
+
|
|
258
|
+
### config
|
|
259
|
+
|
|
260
|
+
New app-level configuration in `@bitforgehq/ui` (additive). `BfSize` (`'sm' | 'md' | 'lg'`, 32/40/48
|
|
261
|
+
px) is the one control-height scale. `provideBfDefaults({ size, fieldSize, controlSize })` sets
|
|
262
|
+
the default height step of text fields (`fieldSize`) and of actions and choice controls
|
|
263
|
+
(`controlSize`) through the `BF_DEFAULTS` token (library default `BF_DEFAULT_VALUES`, `md` for
|
|
264
|
+
both); `size` sets both, an explicit `fieldSize`/`controlSize` wins. `provideBfLabels(overrides)`
|
|
265
|
+
replaces the library's built-in English strings (`BfLabels`: `loading`, `dismiss`, `tabBar`,
|
|
266
|
+
`tabBadge(count)`, `back`, `ok`, `cancel`, `actions`, `sheet`, `modal`, `refreshing`, `close`,
|
|
267
|
+
`expand`, `collapse`, `sheetPosition`, `dialog`, `notifications`) and the default error text per validator kind (`validation`, a `BfValidationMessages`) through the
|
|
268
|
+
`BF_LABELS` token (defaults `BF_DEFAULT_LABELS`). Both providers return a plain `Provider` that
|
|
269
|
+
merges over the parent injector's value (labels' `validation` key by key; a key set to
|
|
270
|
+
`undefined` keeps the parent's string), so they work in
|
|
271
|
+
`ApplicationConfig.providers` and in a component's `providers` for a sub-tree. A component-level
|
|
272
|
+
`provideBfLabels()` covers the widgets, form controls, `bf-back-button` and `bf-refresher` inside
|
|
273
|
+
it; the overlay services (`ok`, `cancel`, `actions`, `sheet`, `modal`, `close`, `dialog`,
|
|
274
|
+
`notifications`) are root singletons and use it only when the call passes the feature's `injector` (see those entries).
|
|
275
|
+
`close` ("Close") names the `bf-dialog` close button, `dialog` ("Dialog") a dialog with no
|
|
276
|
+
title, heading or `ariaLabel`, and `notifications` ("Notifications") the desktop toast stack's
|
|
277
|
+
region.
|
|
278
|
+
`bfValidationMessage(error, messages)` returns an error's own `message`, else the default for its
|
|
279
|
+
kind (Signal Forms kinds and the Reactive Forms interop kinds, limits read from the error or its
|
|
280
|
+
`context`), else `validation.fallback`.
|
|
281
|
+
|
|
282
|
+
JSDoc of `defaults.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
283
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
284
|
+
|
|
285
|
+
JSDoc of `labels.ts` for the API reference: `@summary`/`@since` on the exported symbols; `@default`
|
|
286
|
+
on the option properties the code applies; `@param` docs. No behaviour change.
|
|
287
|
+
|
|
288
|
+
JSDoc of `size.ts` for the API reference: `@summary`/`@since` on the exported symbols. No behaviour
|
|
289
|
+
change.
|
|
290
|
+
|
|
291
|
+
JSDoc of `validation-messages.ts` for the API reference: `@summary`/`@since` on the exported
|
|
292
|
+
symbols; `@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
293
|
+
|
|
294
|
+
`document-reset.ts` (new, internal, exported as `ɵprovideBfDocumentReset` for the `/testing` and
|
|
295
|
+
`/mobile` test providers only; not public API): the document cleanup of `provideBfTesting()` and
|
|
296
|
+
`provideBfShellTesting()`.
|
|
297
|
+
|
|
298
|
+
New label keys `expand` ("Expand") and `collapse` ("Collapse"), the names of `bf-sheet`'s grabber
|
|
299
|
+
button below and at the top snap point, and `sheetPosition(position, count)` ("Sheet at 1 of 2"),
|
|
300
|
+
the sheet's position, which completes the grabber's name and is announced when it changes; `close` ("Close") now also names `bf-sheet`'s
|
|
301
|
+
screen-reader Close. Like the other overlay labels they come from the injector the sheet is
|
|
302
|
+
opened with.
|
|
303
|
+
|
|
304
|
+
### theme
|
|
305
|
+
|
|
306
|
+
**Breaking for custom themes:** new `--color-primary-text` (primary as text: the text button, the
|
|
307
|
+
focused `bf-field` label, the iOS back button), declared with its own value in `@theme`
|
|
308
|
+
(`#2b26ba`) and `.dark` (`#8c88f5`), ≥ 4.5:1 on `bg` and `surface`. An app that overrides
|
|
309
|
+
`--color-primary` must set it too. See Migration 13.
|
|
310
|
+
|
|
311
|
+
New `--radius-control: 9999px` (utility `rounded-control`): the shape of buttons, icon buttons,
|
|
312
|
+
chips, badges, segmented, the tab bar and its tabs, the action bar, toggles and the alert's dismiss
|
|
313
|
+
button. The default keeps them pills; avatars, radios, the tab count badge and the spinner stay
|
|
314
|
+
`rounded-full` (a circle carries meaning). Additive.
|
|
315
|
+
|
|
316
|
+
New pressed tokens, light and dark (F-045), each ≥ 4.5:1 with the text it carries:
|
|
317
|
+
`--color-primary-pressed` (`#1f1b8c` / `#b9b7fb`, primary filled), `--color-primary-soft-pressed`
|
|
318
|
+
(`#cfcdf6` / `#34357a`, tonal fills and outline or text presses), `--color-surface-pressed`
|
|
319
|
+
(`#e4e0d6` / `#2e3139`, neutral rows, segments and unselected chips) and `--color-danger-pressed`
|
|
320
|
+
(`#7f2618` / `#f4a497`, danger filled). `tools/theme-contrast.test.ts` checks every
|
|
321
|
+
text-on-pressed pair. A custom theme that overrides `--color-primary` should set the two primary
|
|
322
|
+
pressed tokens too; the docs theme configurator derives them one step past their hover siblings.
|
|
323
|
+
New `--animate-progress-indeterminate` with its `bf-progress-indeterminate` keyframes (the
|
|
324
|
+
indeterminate `bf-progress` stripe).
|
|
325
|
+
|
|
326
|
+
**Behaviour:** light `--color-text-muted` is `#5b6555` (was `#616b5b`), so muted text keeps 4.5:1
|
|
327
|
+
on `--color-surface-pressed` (a pressed list row's subtitle and trailing notes: 4.63:1; 6.11:1 on
|
|
328
|
+
`surface`).
|
|
329
|
+
|
|
330
|
+
New `--spacing-card-lg: 1.5rem` (24 px, utility `p-card-lg`): the `bf-card` `lg` padding, beside
|
|
331
|
+
`--spacing-card` (18 px, `md`). Additive (F-063).
|
|
332
|
+
|
|
333
|
+
### input
|
|
334
|
+
|
|
335
|
+
New `changed` output: the new text right after each user edit (not for a programmatic write), so
|
|
336
|
+
every value control has one `changed`. New exported `BfInputInputMode` (`BfInputMode | ''`),
|
|
337
|
+
`BfInputEnterKeyHint` (`BfEnterKeyHint | ''`) and `BfInputAutocapitalize`
|
|
338
|
+
(`BfAutocapitalize | ''`), the types of `inputMode`, `enterKeyHint` and `autocapitalize` (were
|
|
339
|
+
the same unions written inline; `''`, the default, leaves the attribute off). `BfInputMode`,
|
|
340
|
+
`BfEnterKeyHint` and `BfAutocapitalize` keep their 0.2.0 members (their `@since` now says 0.2.0).
|
|
341
|
+
|
|
342
|
+
**Breaking:** the default `size` is `md` (40 px) instead of `lg` (48 px); `size` is a `BfSize`
|
|
343
|
+
(`sm` 32, `md` 40, `lg` 48 px — `sm` is new) and, unset, follows `provideBfDefaults()`'s
|
|
344
|
+
`fieldSize`. See Migration 2. Icon insets and padding are logical (`start-*`/`end-*`, `ps-*`/`pe-*`),
|
|
345
|
+
so `icon-start` and `icon-end` sit at the inline start and end under `dir="rtl"`.
|
|
346
|
+
|
|
347
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
348
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
349
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
350
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
351
|
+
|
|
352
|
+
JSDoc of `input.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
353
|
+
`@slot` docs for `icon-start`, `icon-end`; `@param` docs; template handlers and framework hooks
|
|
354
|
+
marked `@internal`, so the reference leaves them out. No behaviour change.
|
|
355
|
+
|
|
356
|
+
**Behaviour:** the help text stays while an error shows, with the error under it (F-020):
|
|
357
|
+
`aria-describedby` lists both (`"<id>-help <id>-error"`), and `aria-errormessage` points at the
|
|
358
|
+
error paragraph while `aria-invalid="true"`. See Migration 25.
|
|
359
|
+
|
|
360
|
+
### textarea
|
|
361
|
+
|
|
362
|
+
New `changed` output: the new text right after each user edit (not for a programmatic write).
|
|
363
|
+
|
|
364
|
+
**Breaking:** the default `size` is `md` instead of `lg`; `size` is a `BfSize` (`sm` is new) and,
|
|
365
|
+
unset, follows `provideBfDefaults()`'s `fieldSize`. A step sets padding and type, never a height
|
|
366
|
+
(`rows` and `autoResize` do), and the docs now say so. See Migration 2.
|
|
367
|
+
|
|
368
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
369
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
370
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
371
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
372
|
+
|
|
373
|
+
JSDoc of `textarea.component.ts` for the API reference: `@param` docs; template handlers and
|
|
374
|
+
framework hooks marked `@internal`, so the reference leaves them out. No behaviour change.
|
|
375
|
+
|
|
376
|
+
**Behaviour:** the help text stays while an error shows, with the error under it (F-020):
|
|
377
|
+
`aria-describedby` lists both (`"<id>-help <id>-error"`), and `aria-errormessage` points at the
|
|
378
|
+
error paragraph while `aria-invalid="true"`. See Migration 25.
|
|
379
|
+
|
|
380
|
+
New `maxRows = input<number | undefined>(undefined)` (F-019). With `autoResize` the box keeps
|
|
381
|
+
`field-sizing: content` and now starts at `rows` lines (a `min-block-size` of `rows` times `1lh`
|
|
382
|
+
plus the size's padding and border), so an empty auto-resizing textarea is as tall as a fixed one with the
|
|
383
|
+
same `rows` (it was one line); with `maxRows` it stops at that many lines (`max-block-size`, the same
|
|
384
|
+
way) and scrolls. **Behaviour:** where the browser has no `field-sizing` (`CSS.supports()` says no, read once;
|
|
385
|
+
also on the server) the box keeps its fixed `rows` and the resize
|
|
386
|
+
handle instead of the one-line box it had.
|
|
387
|
+
|
|
388
|
+
### select
|
|
389
|
+
|
|
390
|
+
**Breaking:** typed values. `BfSelectComponent<T = string>` is generic over the option value
|
|
391
|
+
(`BfSelectOption<T = string>`), so numbers, ids and objects bind directly; `value` is `T | null`
|
|
392
|
+
and defaults to `null` (was `''`), which shows the placeholder. New `compareWith`
|
|
393
|
+
(`BfCompareWith<T>`, default `Object.is`) matches `value` to an option, and new `changed` emits the
|
|
394
|
+
chosen value (never `null`) after a user change. A string, number or boolean option value is the
|
|
395
|
+
native `<option value>` (`"42"` for `42`); an object option's is its index; a choice maps back by
|
|
396
|
+
position, so duplicate native values still resolve. An option whose value is `''` is now
|
|
397
|
+
selectable instead of reading as "empty"; an option whose value is `null` is unsupported (it reads
|
|
398
|
+
as nothing selected). See Migration 6, 7 and 9.
|
|
399
|
+
|
|
400
|
+
**Breaking:** the default `size` is `md` (40 px) instead of `lg` (48 px); `size` is a `BfSize`
|
|
401
|
+
(`sm` 32, `md` 40, `lg` 48 px — `sm` is new) and, unset, follows `provideBfDefaults()`'s
|
|
402
|
+
`fieldSize`. See Migration 2. The chevron and the text padding are logical, so the chevron sits at
|
|
403
|
+
the inline end under `dir="rtl"`.
|
|
404
|
+
|
|
405
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
406
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
407
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
408
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
409
|
+
|
|
410
|
+
JSDoc of `select.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
411
|
+
`@param` docs; template handlers and framework hooks marked `@internal`, so the reference leaves
|
|
412
|
+
them out. No behaviour change.
|
|
413
|
+
|
|
414
|
+
**Behaviour:** the help text stays while an error shows, with the error under it (F-020):
|
|
415
|
+
`aria-describedby` lists both (`"<id>-help <id>-error"`), and `aria-errormessage` points at the
|
|
416
|
+
error paragraph while `aria-invalid="true"`. See Migration 25.
|
|
417
|
+
|
|
418
|
+
### checkbox
|
|
419
|
+
|
|
420
|
+
New `ariaLabelledby` input: the id(s) of the element(s) that name the box when no visible `label`
|
|
421
|
+
is shown (a table's column header and row title), set as the native checkbox's `aria-labelledby`.
|
|
422
|
+
Additive.
|
|
423
|
+
|
|
424
|
+
**Behaviour:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): rows
|
|
425
|
+
are `sm` 32, `md` 40 (default), `lg` 48 px under a fine pointer, and at least 44 px under a coarse
|
|
426
|
+
pointer (was 44 px everywhere); the box is 18 / 22 / 24 px with a matching check mark, and the error
|
|
427
|
+
text's indent is logical (`ps-*`). The row is at least 24 px wide (`min-w-6`), so a label-less box
|
|
428
|
+
with only `ariaLabel` still meets the 24×24 px target size of WCAG 2.5.8 under a fine pointer.
|
|
429
|
+
|
|
430
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
431
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
432
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
433
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
434
|
+
|
|
435
|
+
JSDoc of `checkbox.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs;
|
|
436
|
+
template handlers and framework hooks marked `@internal`, so the reference leaves them out. No
|
|
437
|
+
behaviour change.
|
|
438
|
+
|
|
439
|
+
**Behaviour:** a pressed state (F-045): the box border turns `text` while the row is held down. None while disabled or read-only; under forced colours the system colours win.
|
|
440
|
+
|
|
441
|
+
**Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
|
|
442
|
+
`aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
|
|
443
|
+
elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
|
|
444
|
+
(F-020).
|
|
445
|
+
|
|
446
|
+
### toggle
|
|
447
|
+
|
|
448
|
+
New `ariaLabelledby` input: the id of the element that names the switch when no visible `label`
|
|
449
|
+
is shown, set as the native switch's `aria-labelledby`. Additive.
|
|
450
|
+
|
|
451
|
+
New exported `BfToggleLabelPosition` (`'end' | 'start'`), the type of `labelPosition` (was an
|
|
452
|
+
inline union with the same values).
|
|
453
|
+
|
|
454
|
+
**Behaviour:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): rows
|
|
455
|
+
are `sm` 32, `md` 40 (default), `lg` 48 px under a fine pointer, and at least 44 px under a coarse
|
|
456
|
+
pointer (was 44 px everywhere); the track is 24×40 / 28×48 / 32×56 px with a matching knob, and the knob
|
|
457
|
+
travels to the inline end (mirrored under `dir="rtl"`).
|
|
458
|
+
|
|
459
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
460
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
461
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
462
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
463
|
+
|
|
464
|
+
**Behaviour:** the track and the knob use `rounded-control` (`--radius-control`, `9999px` by
|
|
465
|
+
default, so they render as before).
|
|
466
|
+
|
|
467
|
+
**Behaviour:** the knob carries the state for any configured primary: `on-primary` on the on track
|
|
468
|
+
(was `surface`), `primary-strong` when read-only and on (was `primary`); off and disabled knobs
|
|
469
|
+
stay `surface`.
|
|
470
|
+
|
|
471
|
+
JSDoc of `toggle.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs;
|
|
472
|
+
template handlers and framework hooks marked `@internal`, so the reference leaves them out. No
|
|
473
|
+
behaviour change.
|
|
474
|
+
|
|
475
|
+
**Behaviour:** a pressed state (F-045): the track takes a `text` border while the row is held down. None while disabled or read-only; under forced colours the system colours win.
|
|
476
|
+
|
|
477
|
+
**Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
|
|
478
|
+
`aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
|
|
479
|
+
elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
|
|
480
|
+
(F-020).
|
|
481
|
+
|
|
482
|
+
**Behaviour:** the label wraps (`min-w-0 wrap-anywhere`) instead of overflowing its row at large
|
|
483
|
+
text sizes (F-051): a `labelPosition="start"` label in a `bf-list-item` spilled past the row's
|
|
484
|
+
start edge at the iOS AX sizes.
|
|
485
|
+
|
|
486
|
+
### radio-group
|
|
487
|
+
|
|
488
|
+
New exported `BfRadioGroupOrientation` (`'vertical' | 'horizontal'`), the type of `orientation`
|
|
489
|
+
(was an inline union with the same values).
|
|
490
|
+
|
|
491
|
+
**Breaking:** typed values. `BfRadioGroupComponent<T = string>` is generic over the option value
|
|
492
|
+
(`BfRadioOption<T = string>`); `value` is `T | null` and defaults to `null` (was `''`), with
|
|
493
|
+
nothing checked; `changed` emits `T` (was `string`). New `compareWith` (default `Object.is`)
|
|
494
|
+
matches `value` to an option. A radio's native `value` is its option value for a string, number
|
|
495
|
+
or boolean and its index for an object. See Migration 6, 7 and 9.
|
|
496
|
+
|
|
497
|
+
**Behaviour:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): rows
|
|
498
|
+
are `sm` 32, `md` 40 (default), `lg` 48 px under a fine pointer, and at least 44 px under a coarse
|
|
499
|
+
pointer (was 44 px everywhere); the ring is 18 / 22 / 24 px with a matching dot.
|
|
500
|
+
|
|
501
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
502
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
503
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
504
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
505
|
+
|
|
506
|
+
**Behaviour:** the dot is `primary-strong` (was `primary`), so it reaches 3:1 in its `surface`
|
|
507
|
+
ring for any configured primary.
|
|
508
|
+
|
|
509
|
+
JSDoc of `radio-group.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
510
|
+
symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
|
|
511
|
+
leaves them out. No behaviour change.
|
|
512
|
+
|
|
513
|
+
**Behaviour:** a pressed state (F-045): an option's ring border turns `text` while the option is held down. None while disabled or read-only; under forced colours the system colours win.
|
|
514
|
+
|
|
515
|
+
**Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
|
|
516
|
+
`aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
|
|
517
|
+
elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
|
|
518
|
+
(F-020).
|
|
519
|
+
|
|
520
|
+
### segmented
|
|
521
|
+
|
|
522
|
+
**Breaking:** typed values. `BfSegmentedComponent<T = string>` is generic over the option value
|
|
523
|
+
(`BfSegmentOption<T = string>`); `value` is `T | null` and defaults to `null` (was `''`), with
|
|
524
|
+
nothing checked and the first enabled segment owning the tab stop; `changed` emits `T` (was
|
|
525
|
+
`string`). New `compareWith` (default `Object.is`) matches `value` to an option. `tabStop` is now
|
|
526
|
+
the index of the segment that owns the tab stop (was its value, `-1` when every option is
|
|
527
|
+
disabled), and `select(value)` matches through `compareWith`. See Migration 6, 7 and 10.
|
|
528
|
+
|
|
529
|
+
**Breaking:** heights follow the shared `BfSize` scale: the track is `sm` 32, `md` 40 (default,
|
|
530
|
+
was 44), `lg` 48 px (new); unset, `size` follows `provideBfDefaults()`'s `controlSize`. The
|
|
531
|
+
segments' 44 px hit extension applies only under a coarse pointer (`pointer-coarse:`). See
|
|
532
|
+
Migration 3.
|
|
533
|
+
|
|
534
|
+
**Behaviour:** an error without a message shows its kind's default text (was a red border only):
|
|
535
|
+
once touched, the field shows the first error's own `message`, else `provideBfLabels()`'s
|
|
536
|
+
`validation` text for its kind (Signal Forms and Reactive interop kinds), else
|
|
537
|
+
`validation.fallback`. `errorMessage` still wins. See Migration 1.
|
|
538
|
+
|
|
539
|
+
**Behaviour:** the selected segment carries a 1.5 px inset `primary-strong` outline (≥ 3:1 on
|
|
540
|
+
`surface` and the track), so the selection no longer rests on a fill 1.18:1 from the track; a
|
|
541
|
+
disabled selected segment is outlined in `text-disabled`. The focus ring sits on a 2 px `surface`
|
|
542
|
+
offset (≥ 3:1) instead of on the track (2.79:1). The track and segments use `rounded-control`
|
|
543
|
+
(`--radius-control`).
|
|
544
|
+
|
|
545
|
+
**Behaviour:** under forced colours the selected segment's outline, focus ring (on a `Canvas`
|
|
546
|
+
offset) and shadow use system colours only (`CanvasText`; no shadow), since it opts out of the
|
|
547
|
+
adjustment; a disabled selected segment's outline is `GrayText` instead of the authored
|
|
548
|
+
`text-disabled`.
|
|
549
|
+
|
|
550
|
+
JSDoc of `segmented.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
551
|
+
symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
|
|
552
|
+
leaves them out. No behaviour change.
|
|
553
|
+
|
|
554
|
+
**Behaviour:** a pressed state (F-045): an editable segment takes `surface-pressed` (an unselected one with `text`, which holds 4.5:1 there) while held down. None while disabled or read-only; under forced colours the system colours win.
|
|
555
|
+
|
|
556
|
+
**Behaviour:** sets `aria-errormessage` to its error paragraph (`<id>-error`) while
|
|
557
|
+
`aria-invalid="true"`, and an error that appears while a pointer button is held down (a press
|
|
558
|
+
elsewhere that blurred it) shows once the press ends, so the layout never moves under the pointer
|
|
559
|
+
(F-020).
|
|
560
|
+
|
|
561
|
+
**Behaviour:** labels stay whole at every text size. The segments' side padding is px
|
|
562
|
+
(`px-[12px]`/`px-[14px]`/`px-[18px]`, the rem values at the default size) and so is the track's
|
|
563
|
+
(`px-[4px]`, `gap-[2px]`), so Dynamic Type spends the width on the label; a segment shrinks only to
|
|
564
|
+
its longest word (`min-w-min`, was `min-w-0`) and a label wraps between words (was
|
|
565
|
+
`wrap-anywhere`, which broke "System" letter by letter at iOS AX5). When the segments still cannot
|
|
566
|
+
fit, the track grows past the control and a new `[data-bf-segmented-scroller]` wrapper
|
|
567
|
+
(`overflow-x-auto`, padded so the focus ring is not clipped) scrolls it sideways; the host gains
|
|
568
|
+
`max-w-full`. On the demo's Theme control the three segments fit side by side at iOS 200 %
|
|
569
|
+
(WCAG 1.4.4) on a 393 px phone, and scroll at 235 %. See Migration 38.
|
|
570
|
+
|
|
571
|
+
### number-input
|
|
572
|
+
|
|
573
|
+
New `changed` output: the new `number | null` right after a user edit that changes the model
|
|
574
|
+
(unreadable text changes nothing and does not emit). `step` is typed with the new exported
|
|
575
|
+
`BfNumberInputStep` (`number | 'any'`) and `inputMode` with `BfNumberInputMode`
|
|
576
|
+
(`'numeric' | 'decimal' | ''`); `enterKeyHint` takes `BfInputEnterKeyHint` (`BfEnterKeyHint | ''`).
|
|
577
|
+
|
|
578
|
+
New `bf-number-input`: a native `<input type="number">` with the chrome, sizes (`fieldSize`), slots
|
|
579
|
+
and states of `bf-input` that implements `FormValueControl<number | null>` through Angular's
|
|
580
|
+
`transformedValue()`. An empty field is `null`; text the browser cannot read as a number keeps the
|
|
581
|
+
model and raises a `parse` error ("Enter a valid number", `provideBfLabels()`'s
|
|
582
|
+
`validation.parse`), shown once the field is left, with or without a form. `min`, `max` and
|
|
583
|
+
`step` are native attributes that limit stepping, bound by Signal Forms `min()`/`max()` (whose
|
|
584
|
+
errors and default messages are the only range errors; a step mismatch is not reported). Unset,
|
|
585
|
+
`inputMode` is `numeric` for an integer (or no) step and `decimal` otherwise only when `min >= 0`;
|
|
586
|
+
without such a `min` it stays off, so phone keyboards keep a minus key. A form `reset()` replaces
|
|
587
|
+
unreadable text with the number. Works
|
|
588
|
+
with Reactive Forms `FormControl<number | null>` through the Signal Forms interop. See Migration 8.
|
|
589
|
+
It ships in its own entry point, `import { BfNumberInputComponent } from
|
|
590
|
+
'@bitforgehq/ui/number-input'`, because it parses with Angular's `transformedValue()` and so loads
|
|
591
|
+
Angular Forms (`@angular/forms`, a required peer); a parse error reaches the bound Signal Forms
|
|
592
|
+
field (blocking `submit()`) or Reactive `FormControl` (invalid) with no setup. The widgets entry
|
|
593
|
+
`@bitforgehq/ui` loads no `@angular/forms` code at runtime, so an app without forms ships none of
|
|
594
|
+
it.
|
|
595
|
+
|
|
596
|
+
JSDoc of `number-input.component.ts` for the API reference: `@slot` docs for `icon-start`,
|
|
597
|
+
`icon-end`; `@param` docs; template handlers and framework hooks marked `@internal`, so the
|
|
598
|
+
reference leaves them out. No behaviour change.
|
|
599
|
+
|
|
600
|
+
**Behaviour:** the help text stays while an error shows, with the error under it (F-020):
|
|
601
|
+
`aria-describedby` lists both (`"<id>-help <id>-error"`), and `aria-errormessage` points at the
|
|
602
|
+
error paragraph while `aria-invalid="true"`. See Migration 25.
|
|
603
|
+
|
|
604
|
+
### spinner
|
|
605
|
+
|
|
606
|
+
**Behaviour:** `label` defaults to `''` and falls back to `provideBfLabels()`'s `loading`
|
|
607
|
+
("Loading"); the rendered English default is unchanged.
|
|
608
|
+
|
|
609
|
+
JSDoc of `spinner.component.ts` for the API reference: `@summary`/`@since` on the exported symbols.
|
|
610
|
+
No behaviour change.
|
|
611
|
+
|
|
612
|
+
**Behaviour:** a pure-CSS ring with no inline SVG (F-066): a `rounded-full` ring sized
|
|
613
|
+
`size-3.5`, `size-4.5` or `size-5.5` (14/18/22 px, close to the old SVG's 14/17.5/21 px outer
|
|
614
|
+
edge) and inset by `m-px`, so the box stays 16/20/24 px, with a `border-2`, `border-2` or
|
|
615
|
+
`border-[3px]` border, `border-current/25` for the track and `border-t-current` for the arc; the
|
|
616
|
+
arc ends are square. It rotates with `motion-safe:animate-spin`; under `prefers-reduced-motion` it no longer
|
|
617
|
+
rotates at all but pulses its opacity (`motion-reduce:animate-pulse`, F-064). Under forced colours
|
|
618
|
+
the track is `GrayText` and the arc `CanvasText`. The `decorative`, `label` and status semantics
|
|
619
|
+
are unchanged.
|
|
620
|
+
|
|
621
|
+
**Deprecated:** `px` (the box in px, 16/20/24): use the `size` input; removed in 0.4.0. See
|
|
622
|
+
Migration 26.
|
|
623
|
+
|
|
624
|
+
### alert
|
|
625
|
+
|
|
626
|
+
**Behaviour:** `dismissLabel` defaults to `''` and falls back to `provideBfLabels()`'s `dismiss`
|
|
627
|
+
("Dismiss"); the rendered English default is unchanged.
|
|
628
|
+
|
|
629
|
+
**Behaviour:** the dismiss button uses `rounded-control` (`--radius-control`).
|
|
630
|
+
|
|
631
|
+
**Behaviour:** the role no longer follows the variant: the region is `role="status"` (polite)
|
|
632
|
+
unless the new `urgent` input makes it `role="alert"` (assertive), in any variant. Migration: add
|
|
633
|
+
`urgent` to danger alerts that must interrupt (see Migration 15).
|
|
634
|
+
|
|
635
|
+
New `announce` input: the live region renders empty and fills on the next frame, so an alert
|
|
636
|
+
inserted after an action is announced (APG alert, WCAG 4.1.3); it is read once, at creation.
|
|
637
|
+
|
|
638
|
+
**Behaviour:** with a `title` the dismiss button is named `<dismissLabel>: <title>` ("Dismiss:
|
|
639
|
+
Sync failed"), so two dismissible alerts have distinct buttons (see Migration 16). The docs show
|
|
640
|
+
moving focus in the `dismissed` handler; the widget never moves it.
|
|
641
|
+
|
|
642
|
+
JSDoc of `alert.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
643
|
+
`@slot` docs for `icon`, `default`, `actions`. No behaviour change.
|
|
644
|
+
|
|
645
|
+
### tab-bar
|
|
646
|
+
|
|
647
|
+
**Deprecated:** `tabChange` → `changed` and `tabReselect` → `reselected`. The new outputs emit the
|
|
648
|
+
tab id at the same moments; the old names still emit the same id right after them until 0.4.0 (see
|
|
649
|
+
Migration 23). `bf-tabs` listens to the new names.
|
|
650
|
+
|
|
651
|
+
**Breaking:** (ARIA contract) with the new `BF_TAB_LINKS` token provided by a router-aware parent
|
|
652
|
+
(`bf-tabs`) in the same template, the bar renders a `nav` holding a plain group of links instead
|
|
653
|
+
of a `tablist`: no roving tab stop and no arrow-key, Home or End handling. The token is looked up
|
|
654
|
+
with `host: true`, so a bar inside a routed page under `bf-tabs` stays a standalone `tablist`.
|
|
655
|
+
`BfTabBarHost` gains `linkMode`, `linkHref(id)` and `linkIsRoot(id)`. See Migration 11 and 14.
|
|
656
|
+
|
|
657
|
+
**Behaviour:** `label` defaults to `''` and falls back to `provideBfLabels()`'s `tabBar` ("Main");
|
|
658
|
+
the rendered English default is unchanged.
|
|
659
|
+
|
|
660
|
+
**Behaviour:** shaped with the new `rounded-control` (`--radius-control`, `9999px` by default, so it renders as before); override the token to move off the pill.
|
|
661
|
+
|
|
662
|
+
JSDoc of `tab-bar.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs;
|
|
663
|
+
template handlers and framework hooks marked `@internal`, so the reference leaves them out. No
|
|
664
|
+
behaviour change.
|
|
665
|
+
|
|
666
|
+
JSDoc of `tab-bar.token.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
667
|
+
`@param` docs. No behaviour change.
|
|
668
|
+
|
|
669
|
+
**Behaviour:** new `BF_TAB_BAR_CONTROLLER` token and `BfTabBarController` interface (additive,
|
|
670
|
+
extends `BfTabLinks`): a parent that projects the bar and drives it provides `current()`,
|
|
671
|
+
`hidden()`, `hideTransition()`, `select(id)` and `reselect(id)`. With a controller (looked up with
|
|
672
|
+
`host: true`, like `BF_TAB_LINKS`) the bar shows `current()` as its selection (`''` while it is
|
|
673
|
+
`null`), hides while `hidden()` is true with the transition `hideTransition()` gives, and hands a
|
|
674
|
+
user selection to `select(id)`/`reselect(id)`, still emitting `changed` or `reselected` (and the
|
|
675
|
+
deprecated `tabChange`/`tabReselect` right after them); it never writes `active` then. New
|
|
676
|
+
`selected` on the bar: the id shown as selected, `active` without a controller; `BfTabBarHost`
|
|
677
|
+
gains it as an optional member (`bf-tab` falls back to `active`), so a custom `BF_TAB_BAR`
|
|
678
|
+
provider keeps working. Inside `bf-tabs` `activeChange` no longer emits. Without a controller
|
|
679
|
+
nothing changes; `setHidden()` stays for other parents. See Migration 24.
|
|
680
|
+
|
|
681
|
+
**Behaviour:** large text and compact heights (F-051, F-054): the tabs' row has a minimum height
|
|
682
|
+
in px (`min-h-[64px]`, was the rem `h-16`) and grows with a label wrapped to two lines, which
|
|
683
|
+
Android's font scale (a text zoom that does not scale the bar) needs at 2.0. Being px, the bar no
|
|
684
|
+
longer grows with iOS Dynamic Type itself, as UITabBar does not: at AX5 it was 154.7 px (the rem
|
|
685
|
+
`h-16` at a 235 % root) and is now 98 px (a capsule and two capped label lines). At compact
|
|
686
|
+
heights (`(max-height: 500px)`, phone landscape) it is at least 48 px (`min-h-[48px]`). The bottom
|
|
687
|
+
offset stays.
|
|
688
|
+
|
|
689
|
+
### tab
|
|
690
|
+
|
|
691
|
+
**Breaking:** (ARIA contract) inside `bf-tabs` a tab is `<a href>` with `aria-current` while
|
|
692
|
+
active: `page` on the tab's root page, `true` on a child page of it (no `role="tab"` or
|
|
693
|
+
`aria-selected`); a disabled one is `<a role="link" aria-disabled="true">` without `href`. A plain
|
|
694
|
+
primary click selects the tab through the bar (default prevented); Ctrl/Cmd, Shift, Alt and middle
|
|
695
|
+
clicks open the link in a new tab or window. The link's underline is removed with an important
|
|
696
|
+
utility (`no-underline!`), so an unlayered global link rule cannot underline it. See Migration 11.
|
|
697
|
+
|
|
698
|
+
**Behaviour:** a badged tab is named by `aria-label` = label, comma, announcement ("Projects, 3
|
|
699
|
+
new"; was "Projects , 3 new", with a pause, from a visually hidden span that browsers joined with a
|
|
700
|
+
space), in both forms; the `sr-only` announcement span is gone. Without a badge the visible label
|
|
701
|
+
is the name, as before.
|
|
702
|
+
|
|
703
|
+
**Behaviour:** the badge is placed with a logical inset (`-end-2`), so it sits at the icon's
|
|
704
|
+
inline end under `dir="rtl"`.
|
|
705
|
+
|
|
706
|
+
**Behaviour:** a numeric `badge` is announced through `provideBfLabels()`'s `tabBadge(count)`
|
|
707
|
+
(`"{n} new"` by default, now translatable); a string badge is announced as given.
|
|
708
|
+
|
|
709
|
+
**Behaviour:** the active capsule carries a 1.5 px inset `primary-strong` outline (≥ 3:1 on its
|
|
710
|
+
`primary-soft` fill and the bar), so the active tab no longer rests on a fill 1.23:1 from the bar;
|
|
711
|
+
a disabled active capsule is outlined in `text-disabled`. The tab and its capsule use
|
|
712
|
+
`rounded-control` (`--radius-control`); the count badge stays a circle.
|
|
713
|
+
|
|
714
|
+
**Docs:** `bf-tab` has a page of its own (`tab-bar/tab.docs.md`): its id, its badges (moved from
|
|
715
|
+
the `bf-tab-bar` page) and a disabled tab, with the tab's own theming and accessibility; the
|
|
716
|
+
`bf-tab-bar` page links to it.
|
|
717
|
+
|
|
718
|
+
JSDoc of `tab.component.ts` for the API reference: `@slot` docs for `default`; template handlers and
|
|
719
|
+
framework hooks marked `@internal`, so the reference leaves them out. No behaviour change.
|
|
720
|
+
|
|
721
|
+
**Behaviour:** a pressed state (F-045): the capsule of an enabled tab takes `primary-soft-pressed` (an inactive one with the `text` icon) while the tab is held down. None while disabled; under forced colours the system colours win.
|
|
722
|
+
|
|
723
|
+
**Behaviour:** large text (F-051): the label's size and line height are capped at 200 %
|
|
724
|
+
(`min(var(--text-nano), 22px)`, `min(var(--text-nano--line-height), 28px)`, in px because iOS
|
|
725
|
+
Dynamic Type scales the root); it still wraps to at most two lines and never clips vertically. The
|
|
726
|
+
tab has 4 px of vertical padding, so a grown tab keeps off the bar's edge. The capsule (30 × 52
|
|
727
|
+
px), the badge (18 px, its count capped at 13 px), the padding and the gaps are px, so they keep
|
|
728
|
+
their size at every text size. The label hyphenates a long word where the engine has
|
|
729
|
+
hyphenation for the page's `lang` (`hyphens-auto wrap-break-word`, was `wrap-anywhere`), and
|
|
730
|
+
otherwise still breaks it rather than overflowing (WKWebView on the iOS 18 simulator does not
|
|
731
|
+
hyphenate "Projects", so it still reads "Projec/ts" at AX5). At
|
|
732
|
+
compact heights (`(max-height: 500px)`, F-054) the icon and the label sit side by side
|
|
733
|
+
(`flex-row gap-[6px]`).
|
|
734
|
+
|
|
735
|
+
### back-button
|
|
736
|
+
|
|
737
|
+
**Behaviour:** `label` defaults to `''` and falls back to `provideBfLabels()`'s `back` ("Back"),
|
|
738
|
+
both as the iOS text and as the Material button's name.
|
|
739
|
+
|
|
740
|
+
**Behaviour:** the iOS label and chevron use `primary-text` (was `primary`); hover stays
|
|
741
|
+
`primary-strong`.
|
|
742
|
+
|
|
743
|
+
JSDoc of `back-button.component.ts` for the API reference: docs for the undocumented members and
|
|
744
|
+
methods. No behaviour change.
|
|
745
|
+
|
|
746
|
+
JSDoc of `back-button.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
747
|
+
symbols; `@param` docs. No behaviour change.
|
|
748
|
+
|
|
749
|
+
**Behaviour:** large text (F-051): the host (`min-w-0 shrink`) and the iOS button (`shrink`, no
|
|
750
|
+
longer `shrink-0`) may shrink inside the header's `start` slot, down to the 44 px hit area: the
|
|
751
|
+
text truncates and then disappears before the chevron, as UIKit drops the back title when space is
|
|
752
|
+
short. The text stays the button's name. It carries `data-bf-back-text`, the part `bf-header`'s iOS
|
|
753
|
+
start-slot cap lets give way.
|
|
754
|
+
|
|
755
|
+
### alert-dialog
|
|
756
|
+
|
|
757
|
+
New exported `BfAlertDialogButtonRole` (`'cancel' | 'destructive'`), the type of
|
|
758
|
+
`BfAlertDialogButton.role` (was an inline union with the same values).
|
|
759
|
+
|
|
760
|
+
**Type:** `open()` returns `Promise<string | undefined>` (was `Promise<string>`): a navigation to
|
|
761
|
+
another URL path closes the alert and resolves the cancel button's answer, else `undefined`. New
|
|
762
|
+
`closeOnNavigation` option (default true). Migration: handle `undefined` like cancel (see
|
|
763
|
+
Migration 18).
|
|
764
|
+
|
|
765
|
+
**Behaviour:** the default button (no `buttons`) is labelled with `provideBfLabels()`'s `ok`
|
|
766
|
+
("OK").
|
|
767
|
+
|
|
768
|
+
New `injector` option: labels resolve from it per call (default the root injector) and it becomes
|
|
769
|
+
the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
|
|
770
|
+
other providers reach the dialog.
|
|
771
|
+
|
|
772
|
+
`BfAlertDialogButton` gains `id?: string`; `open()` resolves the tapped button's `id ?? label`,
|
|
773
|
+
and `Escape` and the hardware back button the cancel button's. Buttons render with
|
|
774
|
+
`track $index`, so two buttons with the same label both render.
|
|
775
|
+
|
|
776
|
+
**Behaviour:** focus starts on the `cancel`-role button, else the first button (APG alert dialog),
|
|
777
|
+
even when a destructive button comes first (see Migration 17).
|
|
778
|
+
|
|
779
|
+
JSDoc of `alert-dialog.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
780
|
+
symbols; `@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
781
|
+
|
|
782
|
+
The docs say that Android back in a browser with `CloseWatcher` answers with the cancel button,
|
|
783
|
+
and that an alert without one swallows it; see `### overlay core` and Migration 35.
|
|
784
|
+
|
|
785
|
+
### action-sheet
|
|
786
|
+
|
|
787
|
+
New exported `BfActionSheetActionRole` (`'destructive' | 'cancel'`), the type of
|
|
788
|
+
`BfActionSheetAction.role` (was an inline union with the same values).
|
|
789
|
+
|
|
790
|
+
**Behaviour:** a navigation to another URL path closes it, resolving `undefined`; new
|
|
791
|
+
`closeOnNavigation` option (default true). Migration: pass `closeOnNavigation: false` to keep it
|
|
792
|
+
open (see Migration 19).
|
|
793
|
+
|
|
794
|
+
**Behaviour:** the synthesized cancel button is labelled with `provideBfLabels()`'s `cancel`
|
|
795
|
+
("Cancel"), and a sheet without `title` is named by its `actions` label ("Actions").
|
|
796
|
+
|
|
797
|
+
New `injector` option: labels resolve from it per call (default the root injector) and it becomes
|
|
798
|
+
the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
|
|
799
|
+
other providers reach the action sheet.
|
|
800
|
+
|
|
801
|
+
`BfActionSheetAction` gains `id?: string`; `open()` resolves the tapped row's `id ?? label` (the
|
|
802
|
+
cancel action still resolves `undefined`). Rows render with `track $index`, so two rows with the
|
|
803
|
+
same label both render.
|
|
804
|
+
|
|
805
|
+
**Behaviour:** focus starts on the cancel button, not the first row (see Migration 17).
|
|
806
|
+
|
|
807
|
+
JSDoc of `action-sheet.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
808
|
+
symbols. No behaviour change.
|
|
809
|
+
|
|
810
|
+
JSDoc of `action-sheet.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
811
|
+
symbols; `@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
812
|
+
|
|
813
|
+
The docs say that Android back in a browser with `CloseWatcher` closes the action sheet, resolving
|
|
814
|
+
`undefined`; see `### overlay core` and Migration 35.
|
|
815
|
+
|
|
816
|
+
### sheet
|
|
817
|
+
|
|
818
|
+
**Behaviour:** a navigation to another URL path (a push, a pop, a tab switch) closes it with no
|
|
819
|
+
result, also a `dismissible: false` one; new `closeOnNavigation` option (default true). Migration:
|
|
820
|
+
pass `closeOnNavigation: false` to keep it open (see Migration 19).
|
|
821
|
+
|
|
822
|
+
**Behaviour:** named by the first heading: without `ariaLabel`, the panel is named by the first
|
|
823
|
+
heading in its content that is not `aria-hidden` (`h1`–`h6` or `role="heading"`, given an id
|
|
824
|
+
`bf-overlay-title-<n>` when it has none) through `aria-labelledby`, once the content has rendered;
|
|
825
|
+
without a heading, by `provideBfLabels()`'s `sheet` ("Sheet"). An explicit `ariaLabel` always wins.
|
|
826
|
+
|
|
827
|
+
New `injector` option: labels resolve from it per call (default the root injector) and it becomes
|
|
828
|
+
the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
|
|
829
|
+
other providers reach the sheet.
|
|
830
|
+
|
|
831
|
+
A heading that renders after the overlay opened (behind `@if`, a `resource()`) still names it: the
|
|
832
|
+
panel is watched until its first heading appears.
|
|
833
|
+
|
|
834
|
+
JSDoc of `sheet-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols; `@param`
|
|
835
|
+
docs. No behaviour change.
|
|
836
|
+
|
|
837
|
+
JSDoc of `sheet.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
838
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
839
|
+
|
|
840
|
+
**Behaviour:** the keyboard lift and cap (`translate`, `padding-bottom`, `max-height`,
|
|
841
|
+
`transition`) are written in the render phase (`afterRenderEffect({ write })`) instead of an
|
|
842
|
+
`effect`; internal timing only, no API change.
|
|
843
|
+
|
|
844
|
+
With two or more snap points the grabber is a button (`[data-bf-sheet-grabber-button]`), so the
|
|
845
|
+
sheet can be resized without dragging (WCAG 2.5.7): a tap, click, `Enter` or `Space` moves it one
|
|
846
|
+
point up, and from the top point back to the lowest; `ArrowUp`/`ArrowDown` step one point (never
|
|
847
|
+
past the ends, never dismissing), `Home`/`End` jump to the lowest/top point, all through
|
|
848
|
+
`snapTo()` (instant under reduced motion). It is named after the next press and the position
|
|
849
|
+
("Expand, Sheet at 1 of 2": `expand` below the top point, `collapse` at it, then
|
|
850
|
+
`sheetPosition`); focus stays on it, and every settle on another point (button, key, drag or
|
|
851
|
+
`snapTo()`) announces `sheetPosition` politely through CDK `LiveAnnouncer`. A drag that starts on
|
|
852
|
+
it still drags (a pointer click within the 6 px slop is the press; a keyboard or screen-reader
|
|
853
|
+
click, `detail` 0, is never swallowed). Alt, Ctrl and Meta combinations are left alone. It is
|
|
854
|
+
the full-width 24 px handle row, extended downward to 44 px under a coarse pointer, so a tap just
|
|
855
|
+
above the sheet reaches the backdrop. With one snap point the grabber stays decorative
|
|
856
|
+
(`aria-hidden`) and nothing is announced.
|
|
857
|
+
|
|
858
|
+
**Behaviour:** every dismissible sheet renders a visually hidden "Close" button
|
|
859
|
+
(`provideBfLabels()`'s `close`) as its first element, shown as a pill at the top-end corner when
|
|
860
|
+
focused; activating it closes the sheet. VoiceOver reaches it by swiping, since WKWebView does not
|
|
861
|
+
deliver the two-finger scrub to web content. A `dismissible: false` sheet has none. The Close and
|
|
862
|
+
the grabber button never take the initial focus. Keep a visible close or cancel control in the
|
|
863
|
+
content (Migration 28).
|
|
864
|
+
|
|
865
|
+
**Behaviour:** a sheet opens with focus on its panel (the `role="dialog"` pane, named and
|
|
866
|
+
`tabindex="-1"`), so a screen reader lands inside it and no soft keyboard comes up on open. New
|
|
867
|
+
option `autoFocus?: BfSheetAutoFocus` (`'panel' | 'first-field'`, default `'panel'`; new exported
|
|
868
|
+
type), mapped to the overlay core's `autoFocus` (`'first-field'` → `'first-tabbable'`, which
|
|
869
|
+
honours `[autofocus]`/`[cdkFocusInitial]` and skips the sheet's Close and grabber). Action sheets
|
|
870
|
+
and modals are unchanged. See Migration 33.
|
|
871
|
+
|
|
872
|
+
`bf-sheet` provides a fresh `BfPageContext` (`mainLandmark` false) to its content: a `bf-content`
|
|
873
|
+
in a sheet opened with a page's injector no longer resolves the page's context, so it is no second
|
|
874
|
+
`main` and does not replace the page's registered scroller.
|
|
875
|
+
|
|
876
|
+
Android back in a browser (no Capacitor) closes an open sheet first where `CloseWatcher` exists
|
|
877
|
+
(see `### overlay core`, Migration 35).
|
|
878
|
+
|
|
879
|
+
### modal
|
|
880
|
+
|
|
881
|
+
**Behaviour:** a navigation to another URL path closes it with no result, also a
|
|
882
|
+
`dismissible: false` one; new `closeOnNavigation` option (default true). Migration: pass
|
|
883
|
+
`closeOnNavigation: false` to keep it open (see Migration 19).
|
|
884
|
+
|
|
885
|
+
**Behaviour:** named by the first heading: without `ariaLabel`, the panel is named by the first
|
|
886
|
+
heading in its content that is not `aria-hidden` (such as the `bf-header` title) through
|
|
887
|
+
`aria-labelledby`, once the content has rendered; without a heading, by `provideBfLabels()`'s
|
|
888
|
+
`modal` ("Modal"). An explicit `ariaLabel` always wins.
|
|
889
|
+
|
|
890
|
+
New `injector` option: labels resolve from it per call (default the root injector) and it becomes
|
|
891
|
+
the parent of the content's injector, so a feature-level `provideBfLabels()` and the feature's
|
|
892
|
+
other providers reach the modal.
|
|
893
|
+
|
|
894
|
+
A heading that renders after the overlay opened (behind `@if`, a `resource()`) still names it: the
|
|
895
|
+
panel is watched until its first heading appears.
|
|
896
|
+
|
|
897
|
+
JSDoc of `modal.component.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
898
|
+
behaviour change.
|
|
899
|
+
|
|
900
|
+
JSDoc of `modal.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
901
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
902
|
+
|
|
903
|
+
**Behaviour:** the page-sheet card (`presentation: 'sheet'`) starts 10 px below the top safe
|
|
904
|
+
area (`top-[calc(var(--spacing-safe-top)+10px)]`, UIKit's page-sheet inset; was `top-3`), so it
|
|
905
|
+
sits below the status bar and the parent page peeks above it, and it sets its `BfPageContext`'s
|
|
906
|
+
`safeAreaTop` to false, so its content's header adds no second status-bar band (Migration 29). A
|
|
907
|
+
fullscreen modal is unchanged.
|
|
908
|
+
|
|
909
|
+
**Behaviour:** its content is no `main` landmark: `bf-modal` sets its `BfPageContext.mainLandmark`
|
|
910
|
+
to false (content in a dialog is not the page's main content; the page behind stays `main`, inert
|
|
911
|
+
while the modal is open). Android web back closes it where `CloseWatcher` exists (Migration 35).
|
|
912
|
+
|
|
913
|
+
### refresher
|
|
914
|
+
|
|
915
|
+
**Deprecated:** `refresh` → `refreshRequested`, typed with the new exported
|
|
916
|
+
`BfRefresherRefreshEvent` (`{ complete: () => void }`, was an inline object type). Both outputs
|
|
917
|
+
emit the same event object for each completed pull until `refresh` is removed in 0.4.0 (see
|
|
918
|
+
Migration 23).
|
|
919
|
+
|
|
920
|
+
**Behaviour:** the status region announces `provideBfLabels()`'s `refreshing` ("Refreshing").
|
|
921
|
+
|
|
922
|
+
### button
|
|
923
|
+
|
|
924
|
+
**Behaviour:** `size` is a `BfSize` and, unset, follows `provideBfDefaults()`'s `controlSize`
|
|
925
|
+
(`md` by default, as before). The 44 px hit extension of `sm` and `md` applies only under a coarse
|
|
926
|
+
pointer (`pointer-coarse:`), so dense desktop rows do not overlap; the `outline` variant's
|
|
927
|
+
extension adds its 1.5 px border back and reaches 44 px (was 42). See Migration 5 for
|
|
928
|
+
`BfButtonSize`.
|
|
929
|
+
|
|
930
|
+
New link form: the selector is `bf-button, a[bfButton]`. `<a bfButton routerLink="…">` (or
|
|
931
|
+
`href`) puts the variant, size and full-width classes on your own `<a>` (plus an important
|
|
932
|
+
`no-underline!`, so an unlayered global link rule cannot underline it; no
|
|
933
|
+
inner `<button>`), so a navigation action is a real link that looks like the button: exposed as
|
|
934
|
+
`link`, activated with Enter (not Space), opened in a new tab with Ctrl/Cmd-click, the URL
|
|
935
|
+
visible. `clicked` still emits; `type`, `disabled` and `loading` do not apply (a link is never
|
|
936
|
+
disabled; render a disabled `bf-button` instead). Additive. The host of `bf-button` now binds its
|
|
937
|
+
classes through `[class]`; a consumer's own `class` on the element is kept.
|
|
938
|
+
|
|
939
|
+
**Behaviour:** the `text` variant is coloured `primary-text` (new token, ≥ 4.5:1 on `bg` and
|
|
940
|
+
`surface`; was `primary`), still `primary-strong` on hover. The `danger` variant's hover label is
|
|
941
|
+
`danger-soft` on `danger` (was `on-primary`, which a custom light primary can turn black, 2.7:1 on
|
|
942
|
+
the default danger).
|
|
943
|
+
|
|
944
|
+
**Behaviour:** shaped with the new `rounded-control` (`--radius-control`, `9999px` by default, so
|
|
945
|
+
it renders as before); override the token to move off the pill.
|
|
946
|
+
|
|
947
|
+
JSDoc of `button.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
948
|
+
`@slot` docs for `icon-start`, `default`, `icon-end`; template handlers and framework hooks marked
|
|
949
|
+
`@internal`, so the reference leaves them out. No behaviour change.
|
|
950
|
+
|
|
951
|
+
**Behaviour:** a pressed state (F-045): `primary` takes `primary-pressed`, `tonal`, `outline` and `text` `primary-soft-pressed` (the `text` label turns `primary-strong`), `danger` `danger-pressed` with `danger-soft` text, while the button is held down (`enabled:active:`, and `active:` on `a[bfButton]`). None while disabled or loading; under forced colours the system colours win.
|
|
952
|
+
|
|
953
|
+
### icon-button
|
|
954
|
+
|
|
955
|
+
**Behaviour:** `size` is a `BfSize` and, unset, follows `provideBfDefaults()`'s `controlSize`. The
|
|
956
|
+
44 px hit extension applies only under a coarse pointer, and the `outline` variant's compensates its
|
|
957
|
+
1.5 px border.
|
|
958
|
+
|
|
959
|
+
**Behaviour:** shaped with the new `rounded-control` (`--radius-control`, `9999px` by default, so it renders as before); override the token to move off the pill.
|
|
960
|
+
|
|
961
|
+
JSDoc of `icon-button.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
962
|
+
symbols; `@slot` docs for `default`; template handlers and framework hooks marked `@internal`, so
|
|
963
|
+
the reference leaves them out. No behaviour change.
|
|
964
|
+
|
|
965
|
+
**Behaviour:** a pressed state (F-045): `primary` takes `primary-pressed`, `tonal`, `outline` and `text` `primary-soft-pressed` (the `text` icon turns `text`) while the button is held down. None while disabled; under forced colours the system colours win.
|
|
966
|
+
|
|
967
|
+
### chip-group
|
|
968
|
+
|
|
969
|
+
New `bf-chip-group` (`BfChipGroupComponent<T>`, `BfChipGroupMode` `'single' | 'multiple'`, root
|
|
970
|
+
entry, F-038): a named row of `bf-chip`s. `single` (default): the host is `role="radiogroup"`
|
|
971
|
+
named by `label` or `ariaLabelledby`, the chips are `role="radio"` with `aria-checked`, one roving
|
|
972
|
+
tab stop (the selected chip, else the first enabled), and the arrow keys (wrapping, skipping
|
|
973
|
+
disabled chips), `Home` and `End` move focus and the selection, as in `bf-segmented`; `Space` and
|
|
974
|
+
`Enter` select, there is no deselect. `[(value)]` holds the selected chip's `value` (`null` for
|
|
975
|
+
none) and `(changed)` emits it after a user change. `multiple`: the host is `role="group"` and the
|
|
976
|
+
chips stay independent `aria-pressed` toggles with their own `[(selected)]`. `disabled` disables
|
|
977
|
+
every chip. It is not a Signal Forms control; for a form field use `bf-segmented` or
|
|
978
|
+
`bf-radio-group`.
|
|
979
|
+
|
|
980
|
+
A chip without a `value` inside a `single` group never matches the group's value and cannot be
|
|
981
|
+
selected, so chips that all leave it unset never read as selected together; give every chip a
|
|
982
|
+
`value`.
|
|
983
|
+
|
|
984
|
+
### chip
|
|
985
|
+
|
|
986
|
+
**Deprecated:** `toggled` → `changed`. The new `changed` output emits the new `selected` state
|
|
987
|
+
after a user toggle; `toggled` still emits the same value right after it until 0.4.0 (see
|
|
988
|
+
Migration 23).
|
|
989
|
+
|
|
990
|
+
**Breaking:** new `size` (`BfSize`; unset follows `provideBfDefaults()`'s `controlSize`): `sm` 32,
|
|
991
|
+
`md` 40 (default, was a fixed 36), `lg` 48 px. The hit extension applies only under a coarse
|
|
992
|
+
pointer and compensates the chip's 1.5 px border, so it reaches 44 px (was 42). See Migration 4.
|
|
993
|
+
|
|
994
|
+
**Behaviour:** shaped with the new `rounded-control` (`--radius-control`, `9999px` by default, so it renders as before); override the token to move off the pill.
|
|
995
|
+
|
|
996
|
+
JSDoc of `chip.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
997
|
+
`@slot` docs for `icon-start`, `default`; template handlers and framework hooks marked `@internal`,
|
|
998
|
+
so the reference leaves them out. No behaviour change.
|
|
999
|
+
|
|
1000
|
+
**Behaviour:** a pressed state (F-045): unselected chips take `surface-pressed`, selected `tonal` ones `primary-soft-pressed`, selected `ink` ones `text-secondary` while held down. None while disabled; under forced colours the system colours win.
|
|
1001
|
+
|
|
1002
|
+
New `value = input<unknown>(undefined)`: what the chip stands for inside a `bf-chip-group`
|
|
1003
|
+
(compared with `Object.is`); unused outside one. Inside a `single` group the chip renders
|
|
1004
|
+
`role="radio"` with `aria-checked` (no `aria-pressed`), takes the group's roving `tabindex`, is
|
|
1005
|
+
disabled with the group, and its own `selected` is ignored: the group's `value` decides. A
|
|
1006
|
+
single-choice row without a group keeps working, and `deselectable` keeps its behaviour; the docs'
|
|
1007
|
+
single-choice section now points to the group. New `focus(options?)` focuses the chip's button.
|
|
1008
|
+
|
|
1009
|
+
### list-item
|
|
1010
|
+
|
|
1011
|
+
**Behaviour:** logical properties: text aligns to the inline start and the chevron sits at the
|
|
1012
|
+
inline end, pointing there (mirrored under `dir="rtl"`).
|
|
1013
|
+
|
|
1014
|
+
New navigation rows: a `title` content slot (`bfSlot="title"`), rendered in the title position
|
|
1015
|
+
and type, takes an `<a bfAction routerLink="…">` (the new `BfActionDirective`), whose `::after`
|
|
1016
|
+
stretches over the row. The row stays a `listitem`, the link's name is the title, and the row
|
|
1017
|
+
shows the pointer, the `surface-muted` hover and the focus ring (on keyboard focus) while it holds
|
|
1018
|
+
its own `bfAction`; the row box is `relative`. Text under the stretched link is not selectable.
|
|
1019
|
+
|
|
1020
|
+
**Behaviour:** the trailing slot is wrapped in a `relative z-1` flex box (`gap-3`, as between the
|
|
1021
|
+
row's items, hidden when empty), so a toggle or button there stays clickable above a stretched
|
|
1022
|
+
link. `interactive` stays for rows that act on the page.
|
|
1023
|
+
|
|
1024
|
+
**Docs:** `bf-list-item` has a page of its own (`list/list-item.docs.md`): the row's slots, the
|
|
1025
|
+
navigation rows and the action and settings rows (moved from the `bf-list` page), with the row's own
|
|
1026
|
+
theming and accessibility. The `bf-list` page keeps the container: a list of rows and the plain
|
|
1027
|
+
list, and links to the row page.
|
|
1028
|
+
|
|
1029
|
+
JSDoc of `list-item.component.ts` for the API reference: `@slot` docs for `leading`, `title`,
|
|
1030
|
+
`default`, `trailing`. No behaviour change.
|
|
1031
|
+
|
|
1032
|
+
**Behaviour:** a pressed state (F-045): an `interactive` row takes `surface-pressed` while held down, and a row holding a `bfAction` while its action is (`has-[[data-bf-action]:active]`). None while disabled; under forced colours the system colours win.
|
|
1033
|
+
|
|
1034
|
+
While a row is pressed its chevron turns `text-muted` (`text-subtle` misses 3:1 on
|
|
1035
|
+
`surface-pressed`).
|
|
1036
|
+
|
|
1037
|
+
**Behaviour:** the title, the title slot and the subtitle wrap by default (`wrap-anywhere`, no
|
|
1038
|
+
clamp) instead of truncating, so no text is lost at large text sizes (F-051). New
|
|
1039
|
+
`truncate = input(false, { transform: booleanAttribute })` restores one line with an ellipsis for
|
|
1040
|
+
each. See Migration 32. The mobile demo's Inbox rows use `truncate` (a mail list); its Today,
|
|
1041
|
+
project and Me rows wrap.
|
|
1042
|
+
|
|
1043
|
+
### card
|
|
1044
|
+
|
|
1045
|
+
**Behaviour:** logical properties: content aligns to the inline start (`text-start`).
|
|
1046
|
+
|
|
1047
|
+
**Deprecated:** `interactive` and `clicked` (removed in 1.0.0): a card-sized `<button>` hid the
|
|
1048
|
+
heading and could not hold other controls. Use the new stretched-link pattern instead: a
|
|
1049
|
+
`bfAction` link or button (`BfActionDirective`, new in `@bitforgehq/ui`) in the card's heading
|
|
1050
|
+
stretches its hit area over the card, which is now always `relative` and shows the pointer, the
|
|
1051
|
+
`shadow-floating` hover and the focus ring (on keyboard focus) while it holds its own `bfAction`
|
|
1052
|
+
(it registers with the nearest card or row, so a card holding navigation rows is not lit by them);
|
|
1053
|
+
secondary controls take `class="relative z-1"`. Text under the stretched area is not selectable.
|
|
1054
|
+
See Migration 12.
|
|
1055
|
+
|
|
1056
|
+
JSDoc of `card.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1057
|
+
`@slot` docs for `header`, `default`, `footer`. No behaviour change.
|
|
1058
|
+
|
|
1059
|
+
**Behaviour:** a pressed state (F-045): a card holding a `bfAction` drops from `shadow-floating` back to `shadow-raised` while its action is held down (`has-[[data-bf-action]:active]`), the deprecated `interactive` card while it is. None while disabled; under forced colours the system colours win.
|
|
1060
|
+
|
|
1061
|
+
`padding="lg"` is `p-card-lg` (the new `--spacing-card-lg` token, 24 px) instead of `p-6`
|
|
1062
|
+
(F-063); the rendered padding is unchanged, and an app can now retune it in its `@theme`.
|
|
1063
|
+
|
|
1064
|
+
### action-bar
|
|
1065
|
+
|
|
1066
|
+
**Behaviour:** logical properties: the pill's and the label's padding use `ps-*`/`pe-*`, so the
|
|
1067
|
+
label leads under `dir="rtl"`.
|
|
1068
|
+
|
|
1069
|
+
**Behaviour:** shaped with the new `rounded-control` (`--radius-control`, `9999px` by default, so it renders as before); override the token to move off the pill.
|
|
1070
|
+
|
|
1071
|
+
JSDoc of `action-bar.component.ts` for the API reference: `@slot` docs for `default`. No behaviour
|
|
1072
|
+
change.
|
|
1073
|
+
|
|
1074
|
+
### overlay core
|
|
1075
|
+
|
|
1076
|
+
New exported `BfOverlayRole` (`'dialog' | 'alertdialog' | 'status'`) and `BfOverlayAutoFocus`
|
|
1077
|
+
(`'first-tabbable' | 'panel'`), the types of `BfOverlayOptions.role` and `autoFocus` (were inline
|
|
1078
|
+
unions with the same values).
|
|
1079
|
+
|
|
1080
|
+
**Behaviour:** overlays close when the URL path changes. New `closeOnNavigation` option of
|
|
1081
|
+
`BfOverlayService.open()` (default true, false for passive overlays) and `onNavigation` (runs
|
|
1082
|
+
instead of `close()`, e.g. to answer with a cancel id). The core listens through
|
|
1083
|
+
`Location.onUrlChange()` from `@angular/common` (router navigations, `popstate` and `hashchange`),
|
|
1084
|
+
so `/overlay` still needs no `@angular/router`; the path is compared without its query and
|
|
1085
|
+
fragment, so query- or fragment-only changes never close (a matrix-parameter change is a path
|
|
1086
|
+
change and closes; a `history.pushState()` outside Angular's `Location` is not seen). Migration:
|
|
1087
|
+
pass `closeOnNavigation: false` to an overlay that must survive a navigation. The internal path
|
|
1088
|
+
watcher's unregister function is idempotent.
|
|
1089
|
+
|
|
1090
|
+
New `providers` option of `BfOverlayService.open()`: extra providers of the content's injector,
|
|
1091
|
+
built with the new overlay's ref (`BfDialogService` provides its typed `BfDialogRef` this way).
|
|
1092
|
+
New `autoFocus` option: `'first-tabbable'` (default, as before) or `'panel'`, which focuses the
|
|
1093
|
+
pane itself (for long content). Additive.
|
|
1094
|
+
|
|
1095
|
+
**Behaviour:** initial focus on an `[autofocus]`/`[cdkFocusInitial]` element: a focusable mark
|
|
1096
|
+
(a control, or any element with a `tabindex`, including `tabindex="-1"`, e.g. a region to start
|
|
1097
|
+
reading at) takes focus itself, as before; a mark that is not focusable (e.g. a `bf-button` host)
|
|
1098
|
+
now forwards focus to the first tabbable element inside it instead of nowhere; a mark whose
|
|
1099
|
+
controls are all disabled is skipped for the first tabbable element of the panel, else the panel
|
|
1100
|
+
(before: focus stayed behind the overlay). Migration: none.
|
|
1101
|
+
|
|
1102
|
+
**Behaviour:** on close, when the element that opened the overlay is gone (a deleted row, content
|
|
1103
|
+
re-rendered under a nested overlay), focus moves to the topmost overlay still open instead of
|
|
1104
|
+
the page behind it; with none left it stays on the body. Migration: after a destructive action
|
|
1105
|
+
that removes the opener, move focus yourself (the next row, the list heading).
|
|
1106
|
+
|
|
1107
|
+
**Behaviour:** an `ariaLabelledBy` whose id names no element inside the rendered panel is removed
|
|
1108
|
+
when `ariaLabel` is unset, so a panel never points at nothing. Migration: render the heading in
|
|
1109
|
+
the overlay content (or pass `ariaLabel`).
|
|
1110
|
+
|
|
1111
|
+
**Behaviour:** the animation helpers (`fadeIn`, `fadeOut`, `fadeUp`, `fadeDown`, `scaleIn`,
|
|
1112
|
+
`slideUp`, `slideDown`) and the backdrop fade work without the Web Animations API (jsdom in a
|
|
1113
|
+
consumer's Vitest or Jest run): they apply the end state (an exit's last frame; an enter leaves
|
|
1114
|
+
the element as it is) and return an already finished `Animation`. Migration: consumer tests can
|
|
1115
|
+
drop their `Element.prototype.animate` stand-in.
|
|
1116
|
+
|
|
1117
|
+
**Behaviour:** `Escape` from inside another CDK overlay pane (a CDK menu, such as `bf-menu`,
|
|
1118
|
+
opened from a dialog) no longer closes the overlay underneath: the menu closes and the dialog
|
|
1119
|
+
stays open. Migration: none.
|
|
1120
|
+
|
|
1121
|
+
**Behaviour:** pressing the backdrop of a modal overlay no longer moves focus to `<body>` (out of
|
|
1122
|
+
the dialog, alert or sheet): its `mousedown` is cancelled, so focus stays where it was; a
|
|
1123
|
+
dismissing backdrop still closes on its click. Migration: none.
|
|
1124
|
+
|
|
1125
|
+
The heading helper that names a `/mobile` sheet or modal by its content's first heading moved into
|
|
1126
|
+
the core (internal, `ɵ`-prefixed export; `/mobile` imports it by package name).
|
|
1127
|
+
|
|
1128
|
+
New `injector` option of `BfOverlayService.open()`: the parent of the content's injector (default
|
|
1129
|
+
the root injector), so overlay content resolves the opener's providers. Additive.
|
|
1130
|
+
|
|
1131
|
+
**Behaviour:** outside a shell the page behind a modal overlay is `inert`. When no page root is
|
|
1132
|
+
registered (no `bf-stack-outlet`, e.g. a desktop page), `BfOverlayStack` makes every direct child
|
|
1133
|
+
of `<body>` `inert` while a non-passive overlay is open, except the CDK overlay container, the CDK
|
|
1134
|
+
live announcer and `AriaDescriber` containers, `script`/`style`/`template`/`link` and direct body
|
|
1135
|
+
children marked `data-bf-overlay-keep`; an element that was already `inert` keeps it, and
|
|
1136
|
+
destroying the app (its injector) with an overlay open removes every `inert` the stack set. Inside
|
|
1137
|
+
the shell (registered roots) nothing changes. Migration: mark an element outside the overlay that
|
|
1138
|
+
must stay usable (your own live region, a chat widget) `data-bf-overlay-keep`; it works only on a
|
|
1139
|
+
direct child of `<body>`, since nothing inside an inert element (such as your app root) can opt
|
|
1140
|
+
out. A custom `OverlayContainer` placed inside the app root keeps that body child active, so the
|
|
1141
|
+
page is not isolated.
|
|
1142
|
+
|
|
1143
|
+
**Behaviour:** the hardware back button reaches overlays through the new optional
|
|
1144
|
+
`BF_OVERLAY_HOST` token (`registerBack()`, `documentScrolls`), which `provideBfShell()` provides
|
|
1145
|
+
(back handlers on `BfBackButtonService`, no scroll lock). Without `provideBfShell()` overlays
|
|
1146
|
+
register no hardware back handler, and the scroll lock follows `documentScrolls` instead of
|
|
1147
|
+
`BF_SHELL_CONFIG`. Migration: an app that used `/mobile` overlays without `provideBfShell()` and
|
|
1148
|
+
relied on Android back closing them provides `BF_OVERLAY_HOST` itself.
|
|
1149
|
+
|
|
1150
|
+
JSDoc of `motion.ts` for the API reference: `@summary`/`@since` on the exported symbols; `@param`
|
|
1151
|
+
docs. No behaviour change.
|
|
1152
|
+
|
|
1153
|
+
JSDoc of `overlay-animations.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1154
|
+
`@param` docs. No behaviour change.
|
|
1155
|
+
|
|
1156
|
+
JSDoc of `overlay-host.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1157
|
+
`@param` docs. No behaviour change.
|
|
1158
|
+
|
|
1159
|
+
JSDoc of `overlay-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1160
|
+
`@param` docs. No behaviour change.
|
|
1161
|
+
|
|
1162
|
+
JSDoc of `overlay-stack.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1163
|
+
symbols; `@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
1164
|
+
|
|
1165
|
+
JSDoc of `overlay.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1166
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
1167
|
+
|
|
1168
|
+
`BfOverlayStack` no longer injects the CDK `OverlayContainer`: outside a shell it keeps every body
|
|
1169
|
+
child that holds a `.cdk-overlay-container` active, found by that class, so `bf-stack-outlet` and
|
|
1170
|
+
`bf-tabs`, which register their hosts as page roots, pin no CDK overlay code. No behaviour change:
|
|
1171
|
+
the library's overlays and a consumer's own CDK overlay pushed directly stay usable, and a second
|
|
1172
|
+
container (a custom `OverlayContainer` beside the default) stays active too.
|
|
1173
|
+
|
|
1174
|
+
Initial focus (`autoFocus: 'first-tabbable'`) skips the library's own container controls
|
|
1175
|
+
(`bf-sheet`'s hidden Close and grabber button, marked with the internal `data-bf-overlay-chrome`);
|
|
1176
|
+
they stay in the Tab order. Documented on `autoFocus`; the marker is not API.
|
|
1177
|
+
|
|
1178
|
+
**Behaviour:** Android web back closes the top overlay. For every non-passive overlay, where the
|
|
1179
|
+
window has `CloseWatcher` (Chromium 120+) and the host does not handle the system back itself,
|
|
1180
|
+
the core creates a watcher on open and destroys it on close; its close request runs the same path
|
|
1181
|
+
as `Escape` (`onEscape`, else close when `dismissOnEscape` is not false). An overlay that stays
|
|
1182
|
+
open (not dismissible, or below the top one) creates a new watcher, so it keeps claiming the next
|
|
1183
|
+
back press. `Escape` stays a single dismissal: the core cancels the key, so it never reaches a
|
|
1184
|
+
watcher as a second close request. See Migration 35. A non-dismissible overlay (`dismissOnEscape: false` without
|
|
1185
|
+
`onEscape`) therefore swallows Android web back while it is open, as the `BfOverlayService` JSDoc
|
|
1186
|
+
now says.
|
|
1187
|
+
|
|
1188
|
+
New `BfOverlayHost.handlesSystemBack?: boolean`: true when the host already routes the system back
|
|
1189
|
+
to `registerBack`; the overlays then create no `CloseWatcher`. `provideBfShell()` sets it on
|
|
1190
|
+
native Android.
|
|
1191
|
+
|
|
1192
|
+
### dialog
|
|
1193
|
+
|
|
1194
|
+
New exported `BfConfirmSize` (`'sm' | 'md'`), the type of `BfConfirmOptions.size`,
|
|
1195
|
+
`BfDialogRole` (`'dialog' | 'alertdialog'`), the type of `BfDialogConfig.role`, and
|
|
1196
|
+
`BfConfirmButtonRole` (`'cancel' | 'destructive'`), the type of `BfConfirmButton.role`;
|
|
1197
|
+
`BfDialogConfig` `autoFocus` takes the new `BfOverlayAutoFocus`.
|
|
1198
|
+
|
|
1199
|
+
New `bf-dialog` and `BfDialogService` in `@bitforgehq/ui/overlay`: a centred modal dialog for
|
|
1200
|
+
desktop and web apps with no Capacitor dependency. `open(component | template, config)` returns
|
|
1201
|
+
a typed `BfDialogRef<R, C>` (`afterClosed`, `closed()`, `componentInstance`, `close(result)`);
|
|
1202
|
+
the content injects `BfDialogRef` and `BF_DIALOG_DATA`, a template gets `{ $implicit: ref, data }`.
|
|
1203
|
+
`BfDialogConfig`: `data`, `size` (`sm` 24, `md` 32 (default), `lg` 48, `xl` 64 rem, capped at the
|
|
1204
|
+
viewport minus the gutters, or `fullscreen`), `role` (`dialog`/`alertdialog`), `ariaLabel`,
|
|
1205
|
+
`ariaDescribedBy`, `dismissOnBackdrop` (default true for `dialog`, false for `alertdialog`),
|
|
1206
|
+
`dismissOnEscape`, `autoFocus`, `restoreFocus`, `panelClass`, `injector`. `bf-dialog` lays out a
|
|
1207
|
+
`title` (the `h2` the panel is labelled by), `[bfSlot=header]`, a scrolling body (focusable only
|
|
1208
|
+
while it overflows, then a region named by the title when there is one) and an end-aligned
|
|
1209
|
+
`[bfSlot=footer]`, with a close button (`closeButton`, `closeLabel`, default the `close` label)
|
|
1210
|
+
last in the tab order. `confirm({ title, message?, buttons?, size?, injector? })` opens an alert
|
|
1211
|
+
dialog and resolves with the pressed button's `id ?? label` (default buttons Cancel and OK, ids
|
|
1212
|
+
`cancel` and `ok`); focus starts on the cancel button, `Escape` answers with it or is ignored
|
|
1213
|
+
without one. A navigation to another URL path closes a dialog with no result and a `confirm()`
|
|
1214
|
+
with the cancel button's answer (else `undefined`); `closeOnNavigation: false` (on `BfDialogConfig`
|
|
1215
|
+
and `confirm()`) keeps it open. Title and message ids come from the library's shared id counter,
|
|
1216
|
+
so they never repeat an id of a `bf-dialog` rendered in the page. Focus is trapped and restored,
|
|
1217
|
+
the page behind is `inert` and document scroll is locked (APG dialog (modal) and alert dialog
|
|
1218
|
+
patterns).
|
|
1219
|
+
|
|
1220
|
+
JSDoc of `dialog-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1221
|
+
`@param` docs. No behaviour change.
|
|
1222
|
+
|
|
1223
|
+
JSDoc of `dialog.component.ts` for the API reference: `@slot` docs for `header`, `default`,
|
|
1224
|
+
`footer`. No behaviour change.
|
|
1225
|
+
|
|
1226
|
+
JSDoc of `dialog.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1227
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
1228
|
+
|
|
1229
|
+
The `bf-dialog` host carries `data-bf-dialog-id` (its title id, also without a rendered title), so
|
|
1230
|
+
`BfDialogHarness` finds the overlay panel the card sits in, also under a stacked dialog.
|
|
1231
|
+
|
|
1232
|
+
The docs say that Android back in a browser with `CloseWatcher` closes the dialog like `Escape`
|
|
1233
|
+
(and that `dismissOnEscape: false` swallows it); see `### overlay core` and Migration 35.
|
|
1234
|
+
|
|
1235
|
+
### desktop-toast
|
|
1236
|
+
|
|
1237
|
+
The card's action button output is `actionClicked` (an action emits `clicked`, a sub-part's
|
|
1238
|
+
action `<part>Clicked`); `BfDesktopToastService` binds it.
|
|
1239
|
+
|
|
1240
|
+
New `BfDesktopToastService` and `bf-desktop-toast` in `@bitforgehq/ui/overlay`: toasts for desktop
|
|
1241
|
+
and web apps with no Capacitor dependency. `show({ message, action?, duration?, injector? })`
|
|
1242
|
+
returns a `BfToastRef` and follows the `/mobile` toast's rules: 3000 ms by default, persistent
|
|
1243
|
+
(`duration` 0) with an action, a later `show()` replaces every persistent toast (`'dismiss'`),
|
|
1244
|
+
`Escape` inside a toast dismisses it, focus returns only when it was inside the closing toast, one
|
|
1245
|
+
polite `LiveAnnouncer` announcement per toast, everything ends with the injector. Up to three
|
|
1246
|
+
cards are visible at once in one passive overlay pane, bottom right (bottom left in RTL) 24 px in
|
|
1247
|
+
plus the safe area, the newest nearest the edge; later toasts wait. The pointer over the stack or
|
|
1248
|
+
focus inside it pauses every visible toast. The stack is a `region` named by the new
|
|
1249
|
+
`notifications` label ("Notifications"); no backdrop, no focus move, no scroll lock, nothing
|
|
1250
|
+
`inert`. Cards fade in with an 8 px slide (a fade under reduced motion). A navigation never closes
|
|
1251
|
+
a toast. `F6` from the page moves focus to the action of the newest toast that has one, and `F6`
|
|
1252
|
+
inside the stack moves it back (the key is left to the browser otherwise). In a right-to-left page
|
|
1253
|
+
the end inset reads the left safe area (`--spacing-safe-left`).
|
|
1254
|
+
`provideBfDesktopToast(config)` sets `position` (`'bottom-end'` (default), `'bottom-center'`,
|
|
1255
|
+
`'top-end'` or `'top-center'`), `maxVisible` (default 3) and `ariaLabel` (the region name; empty
|
|
1256
|
+
uses the label), over `BF_DESKTOP_TOAST_DEFAULTS`.
|
|
1257
|
+
|
|
1258
|
+
JSDoc of `desktop-toast.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1259
|
+
symbols; `@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
1260
|
+
|
|
1261
|
+
### menu
|
|
1262
|
+
|
|
1263
|
+
New `changed` output on `bfMenuItemCheckbox`: the new `checked` state right after the user toggles
|
|
1264
|
+
the item (after `triggered`, beside the model's `checkedChange`), so it has the one `changed` of
|
|
1265
|
+
every value control.
|
|
1266
|
+
|
|
1267
|
+
New `bf-menu` and its parts in `@bitforgehq/ui/overlay`, on the Angular CDK menu (APG menu button
|
|
1268
|
+
and menu patterns): `bfMenuItem` on a `<button>` or a link (`<a>` with `href` or `routerLink`),
|
|
1269
|
+
`bfMenuItemCheckbox` and `bfMenuItemRadio` (`checked`, CSS-drawn check mark and dot),
|
|
1270
|
+
`bfMenuGroup` (radio items exclusive), `bfMenuDivider` and `[bfMenuTriggerFor]` (on any element,
|
|
1271
|
+
or on an item for a submenu with a CSS chevron, mirrored in RTL). Items take `bfMenuItemDisabled`
|
|
1272
|
+
(focusable, `aria-disabled`) and `bfMenuItemTypeahead` and emit `triggered`; the trigger takes
|
|
1273
|
+
`bfMenuTriggerData` and `bfMenuPlacement` (`below-start` (default), `below-end`, `above-start`,
|
|
1274
|
+
`above-end`, flipping on overflow; submenus open beside their item) and emits `bfMenuOpened` and
|
|
1275
|
+
`bfMenuClosed`. `bf-menu` takes `size` (item rows `sm` 32, `md` 36, `lg` 44 px, 44 px under a
|
|
1276
|
+
coarse pointer; unset follows `provideBfDefaults()` `controlSize`), `ariaLabel` and
|
|
1277
|
+
`ariaLabelledby`; without either it is named by its trigger. Keyboard: `Enter`, `Space`,
|
|
1278
|
+
`ArrowDown` open on the first item, `ArrowUp` on the last; arrows wrap, `Home`/`End`, type-ahead,
|
|
1279
|
+
`Escape` and `Tab` close with focus back on the trigger, `ArrowRight`/`ArrowLeft` open and close
|
|
1280
|
+
submenus (swapped in RTL); `Space` also follows a link item (once, ignoring auto-repeat), and a
|
|
1281
|
+
disabled link item never navigates, also with `routerLink`. `bfMenuItemCheckbox` emits
|
|
1282
|
+
`checkedChange` with the state it flipped to, so `[(checked)]` binds it two-way. An open submenu
|
|
1283
|
+
stays open while the pointer moves diagonally towards it (CDK menu aim). Under forced colours the
|
|
1284
|
+
focused item is drawn in `Highlight`, a focused disabled one in `GrayText` on `Canvas` with a
|
|
1285
|
+
`Highlight` outline. A navigation to another URL path closes an open menu (query- and
|
|
1286
|
+
fragment-only changes do not); the trigger watches the URL only while its menu is open.
|
|
1287
|
+
|
|
1288
|
+
JSDoc of `menu-item.directive.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1289
|
+
symbols; `@binding` docs for the host-directive bindings `bfMenuItemDisabled`,
|
|
1290
|
+
`bfMenuItemTypeahead`, `triggered`, `checked`. No behaviour change.
|
|
1291
|
+
|
|
1292
|
+
JSDoc of `menu-trigger.directive.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1293
|
+
symbols; `@binding` docs for the host-directive bindings `bfMenuTriggerFor`, `bfMenuTriggerData`,
|
|
1294
|
+
`bfMenuOpened`, `bfMenuClosed`. No behaviour change.
|
|
1295
|
+
|
|
1296
|
+
JSDoc of `menu.component.ts` for the API reference: `@slot` docs for `default`. No behaviour change.
|
|
1297
|
+
|
|
1298
|
+
The two effects that write into the CDK host directives (`bfMenuItemCheckbox`'s `checked`,
|
|
1299
|
+
`bfMenuTrigger`'s `menuPosition` and `ngOnChanges`) carry an `effect-justified` comment: the CDK
|
|
1300
|
+
exposes no input they could derive from. No behaviour change.
|
|
1301
|
+
|
|
1302
|
+
### menu-button
|
|
1303
|
+
|
|
1304
|
+
New `bf-menu-button` in `@bitforgehq/ui/overlay`: a native `<button>` that opens a `bf-menu`
|
|
1305
|
+
(`menu`, a `TemplateRef`) with `aria-haspopup="menu"` and `aria-expanded`, styled like
|
|
1306
|
+
`bf-button` (`variant` `outline` (default), `text`, `tonal`; a CSS caret) or, with `iconOnly`,
|
|
1307
|
+
like `bf-icon-button`. `size` (unset follows `controlSize`), `placement`, `label` (the
|
|
1308
|
+
`aria-label`, required with `iconOnly`), `disabled`, and `opened`/`closed` outputs. Its classes
|
|
1309
|
+
come from `bf-button`'s and `bf-icon-button`'s own class maps (internal `ɵ` exports of
|
|
1310
|
+
`@bitforgehq/ui`), so the looks never drift.
|
|
1311
|
+
|
|
1312
|
+
JSDoc of `menu-button.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1313
|
+
symbols; `@slot` docs for `icon-start`, `default`. No behaviour change.
|
|
1314
|
+
|
|
1315
|
+
**Behaviour:** a pressed state (F-045): the button and icon-only forms take `bf-button`'s and `bf-icon-button`'s pressed fills from their shared class maps. None while disabled; under forced colours the system colours win.
|
|
1316
|
+
|
|
1317
|
+
### toast
|
|
1318
|
+
|
|
1319
|
+
The toast engine (timing, hold, replace, focus return, announcements, teardown) moved into
|
|
1320
|
+
`@bitforgehq/ui/overlay`, shared with the new desktop toast; `BfToastService` renders one toast at
|
|
1321
|
+
a time on it exactly as before. `BfToastRef`, `BfToastDismissReason` and `BfToastAction` are now
|
|
1322
|
+
also exported from `@bitforgehq/ui/overlay`; the `/mobile` exports stay (not deprecated) and are
|
|
1323
|
+
the same types and class. No behaviour change: a navigation never closes a toast (it opts out of
|
|
1324
|
+
the core's new `closeOnNavigation`).
|
|
1325
|
+
|
|
1326
|
+
JSDoc of `toast.component.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1327
|
+
behaviour change.
|
|
1328
|
+
|
|
1329
|
+
JSDoc of `toast.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1330
|
+
`@default` on the option properties the code applies; `@param` docs. No behaviour change.
|
|
1331
|
+
|
|
1332
|
+
**Behaviour:** `--bf-toast-keyboard` is written in the render phase (`afterRenderEffect`) instead
|
|
1333
|
+
of an `effect`; internal timing only, no API change.
|
|
1334
|
+
|
|
1335
|
+
### overlay entry
|
|
1336
|
+
|
|
1337
|
+
New secondary entry `@bitforgehq/ui/overlay`: the overlay core with no Capacitor and no router
|
|
1338
|
+
dependency (peers `@angular/cdk` and `rxjs`). `BfOverlayService`, `BfOverlayRef`,
|
|
1339
|
+
`BfComponentOverlayRef`, `BF_OVERLAY_DATA`, `BfOverlayStack`, their option types and the
|
|
1340
|
+
animations (`fadeIn`, `fadeOut`, `fadeUp`, `fadeDown`, `scaleIn`, `slideUp`, `slideDown`) moved
|
|
1341
|
+
here from `@bitforgehq/ui/mobile`, joined by `BF_OVERLAY_HOST`/`BfOverlayHost`,
|
|
1342
|
+
`OVERLAY_REDUCED_DURATION`, `prefersReducedMotion` (still exported from `/mobile` too) and
|
|
1343
|
+
`BfOverlayController` (for entries that extend `BfOverlayRef`). `/mobile` builds its sheet, modal,
|
|
1344
|
+
toast, action sheet and alert dialog on it.
|
|
1345
|
+
|
|
1346
|
+
**Type:** the `/mobile` exports of the moved names are deprecated aliases of the `/overlay` ones
|
|
1347
|
+
(the same classes, tokens and functions), removed in 0.4.0. Migration: import them from
|
|
1348
|
+
`@bitforgehq/ui/overlay` instead of `@bitforgehq/ui/mobile`.
|
|
1349
|
+
|
|
1350
|
+
**Migration — every moved core import:**
|
|
1351
|
+
|
|
1352
|
+
| Old (`@bitforgehq/ui/mobile`, deprecated) | New (`@bitforgehq/ui/overlay`) |
|
|
1353
|
+
| ----------------------------------------- | ------------------------------ |
|
|
1354
|
+
| `BF_OVERLAY_DATA` | `BF_OVERLAY_DATA` |
|
|
1355
|
+
| `BfComponentOverlayRef` | `BfComponentOverlayRef` |
|
|
1356
|
+
| `BfOverlayOptions` | `BfOverlayOptions` |
|
|
1357
|
+
| `BfOverlayPosition` | `BfOverlayPosition` |
|
|
1358
|
+
| `BfOverlayRef` | `BfOverlayRef` |
|
|
1359
|
+
| `BfOverlayService` | `BfOverlayService` |
|
|
1360
|
+
| `BfOverlayStack` | `BfOverlayStack` |
|
|
1361
|
+
| `BfOverlayStackOptions` | `BfOverlayStackOptions` |
|
|
1362
|
+
| `BfOverlayTemplateContext` | `BfOverlayTemplateContext` |
|
|
1363
|
+
| `BfStackedOverlay` | `BfStackedOverlay` |
|
|
1364
|
+
| `OVERLAY_DURATION` | `OVERLAY_DURATION` |
|
|
1365
|
+
| `OVERLAY_EASING` | `OVERLAY_EASING` |
|
|
1366
|
+
| `fadeDown` | `fadeDown` |
|
|
1367
|
+
| `fadeIn` | `fadeIn` |
|
|
1368
|
+
| `fadeOut` | `fadeOut` |
|
|
1369
|
+
| `fadeUp` | `fadeUp` |
|
|
1370
|
+
| `scaleIn` | `scaleIn` |
|
|
1371
|
+
| `slideDown` | `slideDown` |
|
|
1372
|
+
| `slideUp` | `slideUp` |
|
|
1373
|
+
|
|
1374
|
+
`/mobile` keeps these as deprecated aliases (same class/token/function identity) until 0.4.0; new
|
|
1375
|
+
code imports them from `@bitforgehq/ui/overlay` directly. `prefersReducedMotion` and
|
|
1376
|
+
`OVERLAY_REDUCED_DURATION` are new in `/overlay` and stay exported from `/mobile` too (not
|
|
1377
|
+
deprecated — no import change needed). `BfToastRef`, `BfToastDismissReason` and `BfToastAction`
|
|
1378
|
+
also moved, but `/mobile`'s export of them is the same class/type, not deprecated (see `toast`
|
|
1379
|
+
above). An app that adds `@bitforgehq/ui/overlay` installs `@angular/cdk` and `rxjs` (peers of
|
|
1380
|
+
`/overlay`); `/mobile` needs those plus `@angular/router` and `@capacitor/core` (unchanged).
|
|
1381
|
+
|
|
1382
|
+
**Behaviour changes bundled with the move** (each detailed under its own `###` above; one line
|
|
1383
|
+
each here for a single migration pass):
|
|
1384
|
+
|
|
1385
|
+
- Outside a shell (no `provideBfShell()`), the page behind an open non-passive overlay is now
|
|
1386
|
+
`inert` (`### overlay core`, Migration 20).
|
|
1387
|
+
- The hardware back button now reaches overlays only through the optional `BF_OVERLAY_HOST`
|
|
1388
|
+
token, provided by `provideBfShell()`; without it, no back handler is registered
|
|
1389
|
+
(`### overlay core`, Migration 21).
|
|
1390
|
+
- Overlays close by default when the URL path changes (`closeOnNavigation`, default true; toasts
|
|
1391
|
+
never close) (`### overlay core`, Migration 19).
|
|
1392
|
+
- `bf-alert`'s role follows the new `urgent` input, not `variant`: add `urgent` to a danger alert
|
|
1393
|
+
that must interrupt (`### alert`, Migration 15).
|
|
1394
|
+
- `bf-alert`'s dismiss button is named after the alert's title, not a bare "Dismiss" (`### alert`,
|
|
1395
|
+
Migration 16).
|
|
1396
|
+
- Alert dialogs and action sheets focus the `cancel`-role button first, not the first button
|
|
1397
|
+
(`### alert-dialog`, `### action-sheet`, Migration 17).
|
|
1398
|
+
- `BfAlertDialogService.open()` returns `Promise<string | undefined>` (was `Promise<string>`): a
|
|
1399
|
+
navigation may resolve `undefined` when there is no cancel button (`### alert-dialog`, Migration
|
|
1400
|
+
18).
|
|
1401
|
+
|
|
1402
|
+
### testing entry
|
|
1403
|
+
|
|
1404
|
+
New secondary entry `@bitforgehq/ui/testing` (additive), for consumer specs under Vitest (the
|
|
1405
|
+
Angular CLI default) or Jest. It imports only `@angular/core`, `@angular/cdk/testing` and
|
|
1406
|
+
`@bitforgehq/ui` (never `/overlay`, `/mobile` or Capacitor; lint- and consumer-smoke-checked), so
|
|
1407
|
+
it needs `@angular/cdk` as a dev dependency; the CDK stays an optional peer.
|
|
1408
|
+
|
|
1409
|
+
- `installBfJsdomShims(options?)` (`jsdom-shims.ts`) installs, only where missing,
|
|
1410
|
+
`Element.prototype.animate` (an `Animation` whose `finished` resolves at once), `PointerEvent`
|
|
1411
|
+
(a `MouseEvent` with `pointerId`, `isPrimary`, `pointerType`, `width`, `height`, `pressure`)
|
|
1412
|
+
and a `matchMedia` stub (every query false; `reducedMotion: true` reports
|
|
1413
|
+
`(prefers-reduced-motion: reduce)` as matching; `matchMedia: false` skips it). It is idempotent
|
|
1414
|
+
and returns a function that removes exactly what it installed (a replaced global is restored
|
|
1415
|
+
from its descriptor, getters included; a global it cannot set is left alone). The shims moved here from the
|
|
1416
|
+
library's own `setup-jest.ts`; the library, docs, web-demo and mobile-demo Jest setups now call
|
|
1417
|
+
it. The shimmed `Animation` is a small class implementing the interface in full (no cast, the
|
|
1418
|
+
same shape as `overlay-animations.ts`'s own `BfFinishedAnimation`); its `finished` resolves to
|
|
1419
|
+
the animation itself, matching the real Web Animations API, not `undefined` as before.
|
|
1420
|
+
- `provideBfTesting({ defaults?, labels? })` (`provide-bf-testing.ts`) passes the options to
|
|
1421
|
+
`provideBfDefaults()`/`provideBfLabels()` and, when the test environment is torn down, removes
|
|
1422
|
+
`dark` and `cdk-global-scrollblock` from `<html>`, `inert` from the children of `<body>` and every
|
|
1423
|
+
child of a `.cdk-overlay-container`. The cleanup needs TestBed's default
|
|
1424
|
+
`teardown: { destroyAfterEach: true }`.
|
|
1425
|
+
- CDK component harnesses (`harnesses/*.harness.ts`), each with `static with({ label })` (the
|
|
1426
|
+
shared `BfHarnessFilters`, the CDK's `selector`/`ancestor` plus `label`): `BfSelectHarness`
|
|
1427
|
+
(label, value label, option labels, `selectOption(label)`, disabled, read-only, required,
|
|
1428
|
+
error and help text, focus, blur), `BfRadioGroupHarness` and `BfSegmentedHarness` (label, option
|
|
1429
|
+
labels, selected label, `select(label)`, disabled, error text), `BfDialogHarness` (title, text,
|
|
1430
|
+
role, `clickButton(label)`, `close()`, `pressEscape()`; through
|
|
1431
|
+
`TestbedHarnessEnvironment.documentRootLoader()`), `BfMenuButtonHarness` (`open()`, `isOpen()`,
|
|
1432
|
+
`getMenu()`), `BfMenuHarness` (`getItems(filter?)`, `clickItem(label, ...submenuPath)`),
|
|
1433
|
+
`BfMenuItemHarness` (text, disabled, checked, submenu, `click()`, `getSubmenu()`) and
|
|
1434
|
+
`BfDesktopToastHarness` (message, `clickAction()`, `pressEscape()`). They work by label, so a
|
|
1435
|
+
spec never needs the native option values of the typed choice controls. Choosing in a disabled
|
|
1436
|
+
select, or a disabled option, segment, radio group or segmented control, throws and leaves the
|
|
1437
|
+
value alone, as a user's click would. `BfDialogHarness.getRole()` reads the dialog's own panel
|
|
1438
|
+
(found through the host's `data-bf-dialog-id`), `null` for a `bf-dialog` rendered in the page. Plain native controls
|
|
1439
|
+
(input, textarea, checkbox, toggle, button) get none: query their native element.
|
|
1440
|
+
|
|
1441
|
+
### provide-bf-shell-testing
|
|
1442
|
+
|
|
1443
|
+
New in `@bitforgehq/ui/mobile` (additive): `provideBfShellTesting({ routes?, config? })`, the test
|
|
1444
|
+
providers for an app on the shell: `provideRouter(routes ?? [], withComponentInputBinding(),
|
|
1445
|
+
withBfShellRouting())`, `provideBfShell({ swipeBack: false, hapticsOnTabChange: false, ...config })`,
|
|
1446
|
+
an in-memory `BF_THEME_STORAGE` (a theme change never reaches `localStorage`) and the same document
|
|
1447
|
+
cleanup as `provideBfTesting()` (which needs TestBed's default `destroyAfterEach: true`). It lives
|
|
1448
|
+
in `/mobile` so that `/testing` never loads Capacitor.
|
|
1449
|
+
|
|
1450
|
+
### tabs
|
|
1451
|
+
|
|
1452
|
+
**Breaking:** (ARIA contract) `bf-tabs` provides `BF_TAB_LINKS`, so the `bf-tab-bar` projected
|
|
1453
|
+
directly into it is a `nav` of links with `aria-current` (`page` at the tab's root, `true` on a
|
|
1454
|
+
child page) instead of a `tablist` (the tabs switch routes, not in-page panels); an in-page bar on
|
|
1455
|
+
a tab's page keeps its `tablist`. New `href(id)`: the tab's root URL, `/<tabsPath>/<id>` through
|
|
1456
|
+
`LocationStrategy` (base href included), used as each tab's `href`, so Ctrl/Cmd-click opens a tab
|
|
1457
|
+
in a new browser tab; a plain click still restores the tab's last page. New `isRoot(id)`: whether
|
|
1458
|
+
the current URL is that root. Tab moves between tabs (in Safari and iOS WKWebView only with "Press
|
|
1459
|
+
Tab to highlight each item", else Option+Tab); the arrow keys no longer do (they stopped after one
|
|
1460
|
+
step before). See Migration 11.
|
|
1461
|
+
|
|
1462
|
+
JSDoc of `tab-context.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1463
|
+
`@param` docs. No behaviour change.
|
|
1464
|
+
|
|
1465
|
+
JSDoc of `tabs.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs. No
|
|
1466
|
+
behaviour change.
|
|
1467
|
+
|
|
1468
|
+
**Behaviour:** the bar is driven through `BF_TAB_BAR_CONTROLLER` instead of written into (F-021).
|
|
1469
|
+
`current()` (read-only) is the tab of the in-flight navigation's target after redirects once they
|
|
1470
|
+
are applied (`router.currentNavigation()`), else of
|
|
1471
|
+
the settled URL (`router.lastSuccessfulNavigation()`), keeping the last tab while neither is under
|
|
1472
|
+
`tabsPath`: a clicked tab shows at once and a navigation a guard refuses drops back by itself.
|
|
1473
|
+
`hidden()` is `keyboard.visible() || keyboard.imminent()`, `hideTransition()` is true on iOS, and
|
|
1474
|
+
`select(id)`/`reselect(id)` are the former `changed`/`reselected` handlers. `tabKey()` and
|
|
1475
|
+
`isRoot(id)` read the router's navigation signals; the `router.events` subscription and the three
|
|
1476
|
+
effects that wrote into the bar are gone. `--bf-tab-bar-inset` (and `--bf-tab-bar-inset-global` on
|
|
1477
|
+
`<html>`) is written in the render phase (`afterRenderEffect`). See Migration 24.
|
|
1478
|
+
|
|
1479
|
+
**Behaviour:** `--bf-tab-bar-inset` (and `--bf-tab-bar-inset-global`) use the bar's measured height
|
|
1480
|
+
instead of the fixed 4 rem (F-051, F-054): an `afterRenderEffect` observes the projected bar's
|
|
1481
|
+
`nav` with a `ResizeObserver` and writes
|
|
1482
|
+
`calc(<height>px + 8px + max(var(--spacing-safe-bottom), 12px) + 8px)`, or `0px` while the bar is
|
|
1483
|
+
hidden; until the first measurement it uses the bar's 64 px minimum. The observer is disconnected on
|
|
1484
|
+
destroy. So the page's bottom padding follows a bar grown by large text or slimmed at compact
|
|
1485
|
+
heights.
|
|
1486
|
+
|
|
1487
|
+
### action
|
|
1488
|
+
|
|
1489
|
+
New `BfActionDirective` (`a[bfAction], button[bfAction]`): marks the one interactive element of a
|
|
1490
|
+
`bf-card` or `bf-list-item` (`data-bf-action`) and stretches its hit area over the card or row with
|
|
1491
|
+
an empty `::after` (`absolute inset-0`), the stretched-link pattern, and gives a `button` the
|
|
1492
|
+
pointer cursor a link has. It registers with the nearest card or row (an internal token), which
|
|
1493
|
+
draws the pointer, hover and focus ring for its own action only. Under forced colours a
|
|
1494
|
+
transparent outline on the `::after` becomes a system-coloured focus indicator around the whole
|
|
1495
|
+
card or row (the ring, a box-shadow, is dropped there). See `card` and `list-item`.
|
|
1496
|
+
Additive.
|
|
1497
|
+
|
|
1498
|
+
JSDoc of `action.directive.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1499
|
+
template handlers and framework hooks marked `@internal`, so the reference leaves them out. No
|
|
1500
|
+
behaviour change.
|
|
1501
|
+
|
|
1502
|
+
### badge
|
|
1503
|
+
|
|
1504
|
+
**Behaviour:** the label and the count pill use `rounded-control` (`--radius-control`, `9999px` by
|
|
1505
|
+
default, so they render as before).
|
|
1506
|
+
|
|
1507
|
+
JSDoc of `badge.component.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1508
|
+
`@slot` docs for `default`. No behaviour change.
|
|
1509
|
+
|
|
1510
|
+
New `countLabel = input('')` (F-039): with `count` set, it renders visually hidden after the
|
|
1511
|
+
number (`<span class="sr-only">`), so the pill reads "99+ unread messages". Ignored without
|
|
1512
|
+
`count`.
|
|
1513
|
+
|
|
1514
|
+
### field
|
|
1515
|
+
|
|
1516
|
+
**Behaviour:** the label of a focused field is `primary-text` (was `primary`).
|
|
1517
|
+
|
|
1518
|
+
New export `bfFieldControlClass(state: { size: BfSize; invalid: boolean; disabled: boolean;
|
|
1519
|
+
readonly: boolean }): string` (and its `BfFieldControlState` type) from `@bitforgehq/ui`: the class
|
|
1520
|
+
list `bf-input` renders on its native `<input>` in the same state (box, height and type per size,
|
|
1521
|
+
padding, focus ring, `border-strong` or, while `invalid`, `danger-border`), so a custom control
|
|
1522
|
+
inside `bf-field` looks like the library's own fields without copying its classes. The disabled
|
|
1523
|
+
and read-only styles come from the control's native `disabled`/`readonly` attributes. `bf-input`
|
|
1524
|
+
builds its own list from the same pieces, and a spec asserts the two agree for every size and
|
|
1525
|
+
state. The field docs and the `@example` bind the control's `aria-describedby` to the field's
|
|
1526
|
+
`describedBy()` through a template reference (`#field`); the new `field/custom-control` example
|
|
1527
|
+
does so for a native input styled by `bfFieldControlClass()`.
|
|
1528
|
+
|
|
1529
|
+
JSDoc of `field-classes.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1530
|
+
`@param` docs. No behaviour change.
|
|
1531
|
+
|
|
1532
|
+
JSDoc of `field.component.ts` for the API reference: `@slot` docs for `default`. No behaviour
|
|
1533
|
+
change.
|
|
1534
|
+
|
|
1535
|
+
**Behaviour:** the help paragraph renders always (when set) and the error paragraph under it;
|
|
1536
|
+
help text no longer disappears on error (F-020). `describedBy()` lists both ids,
|
|
1537
|
+
`"<controlId>-help <controlId>-error"`, `"<controlId>-help"`, `"<controlId>-error"` or `null`. A new
|
|
1538
|
+
`error` that arrives while a pointer button is held down anywhere in the page shows one task after
|
|
1539
|
+
the release (or at once on a key press, or a pointer move with no button down): the press on a
|
|
1540
|
+
Save button blurs the field and marks it touched on mousedown, and the message pushed Save about
|
|
1541
|
+
22 px down before the mouseup, so the click was lost in every browser. The field holds its layout
|
|
1542
|
+
through the press instead of reserving an empty line under every field. See Migration 25.
|
|
1543
|
+
|
|
1544
|
+
`fieldDescribedBy(id, errorText, helpText)` (internal, `ɵfieldDescribedBy`) returns
|
|
1545
|
+
`"<id>-help <id>-error"`, `"<id>-help"`, `"<id>-error"` or `null`; the new internal
|
|
1546
|
+
`ɵfieldErrorMessageId` gives the controls' `aria-errormessage`.
|
|
1547
|
+
|
|
1548
|
+
While a press holds a new error back, its paragraph is already rendered, visually hidden
|
|
1549
|
+
(`sr-only`), so the controls' `aria-describedby` and `aria-errormessage` always point at an element
|
|
1550
|
+
that exists and assistive tech reads the error when focus returns to the field; the same paragraph
|
|
1551
|
+
turns visible on release. Only the primary mouse button, a touch or a pen starts a hold (a
|
|
1552
|
+
right-click never does). The self-rendering controls (checkbox, toggle, radio group, segmented)
|
|
1553
|
+
behave the same way.
|
|
1554
|
+
|
|
1555
|
+
### progress
|
|
1556
|
+
|
|
1557
|
+
**Behaviour:** the default `primary` tone fills with `primary-strong` (was `primary`), so the fill
|
|
1558
|
+
reaches 3:1 on its `surface-sunken` track for any configured primary.
|
|
1559
|
+
|
|
1560
|
+
JSDoc of `progress.component.ts` for the API reference: `@summary`/`@since` on the exported symbols.
|
|
1561
|
+
No behaviour change.
|
|
1562
|
+
|
|
1563
|
+
**Behaviour:** `value` is optional (`input<number | undefined>(undefined)`, was required); without
|
|
1564
|
+
it and without `indeterminate` the bar renders 0 %. The redundant `role="progressbar"` attribute on
|
|
1565
|
+
the native `<progress>` is dropped (it is a progressbar natively). New `valueText = input('')`,
|
|
1566
|
+
rendered as `aria-valuetext` when set (F-039). New `indeterminate` (boolean): the native bar
|
|
1567
|
+
renders without `value` (indeterminate for assistive tech) and a 40 % stripe in the tone's fill
|
|
1568
|
+
runs across the track (`animate-progress-indeterminate`), standing still at the start under
|
|
1569
|
+
`motion-reduce:`. The host is a one-cell `grid` (was `block`) with `overflow-hidden
|
|
1570
|
+
rounded-full`, so the stripe shares the track's cell and stays in it.
|
|
1571
|
+
|
|
1572
|
+
The indeterminate stripe runs from the inline start: under `dir="rtl"` it takes
|
|
1573
|
+
`--animate-progress-indeterminate-rtl` (mirrored keyframes), and stands still under reduced motion
|
|
1574
|
+
in both directions.
|
|
1575
|
+
|
|
1576
|
+
### icon
|
|
1577
|
+
|
|
1578
|
+
JSDoc of `icon.component.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1579
|
+
behaviour change.
|
|
1580
|
+
|
|
1581
|
+
**Behaviour:** `bf-icon` binds `[attr.fill]` on every element it renders (F-065), so the filled
|
|
1582
|
+
details of Lucide icons show (`Palette`'s dots and 9 other distinct icons); elements without a Lucide
|
|
1583
|
+
`fill` get no attribute and inherit the `none` of the `<svg>`. `tools/lucide-attributes.test.ts`
|
|
1584
|
+
fails when an installed Lucide icon sets an attribute the renderer does not bind. See Migration 27.
|
|
1585
|
+
|
|
1586
|
+
### content
|
|
1587
|
+
|
|
1588
|
+
New exported `BfContentScrollEvent` (`{ scrollTop: number }`), the payload of `scrolled` (was an
|
|
1589
|
+
inline object type of the same shape).
|
|
1590
|
+
|
|
1591
|
+
JSDoc of `content.component.ts` for the API reference: `@slot` docs for `default`; `@param` docs. No
|
|
1592
|
+
behaviour change.
|
|
1593
|
+
|
|
1594
|
+
**Behaviour:** `--bf-header-height` is written in the render phase (`afterRenderEffect`), and the
|
|
1595
|
+
scroller registers in `BfPageContext` once after the first render (`afterNextRender`) instead of
|
|
1596
|
+
from an `effect`; it is still cleared on destroy. Internal timing only, no API change.
|
|
1597
|
+
|
|
1598
|
+
`fullscreen` pads the top safe area only while the page's `BfPageContext.safeAreaTop()` is true;
|
|
1599
|
+
inside a page-sheet `bf-modal` (false) it adds no top padding (Migration 29).
|
|
1600
|
+
|
|
1601
|
+
**Behaviour:** in the page a `bf-stack-outlet` shows, the scroller is the `main` landmark
|
|
1602
|
+
(`role="main"`, while `BfPageContext.mainLandmark()` is true). While it overflows and holds nothing
|
|
1603
|
+
focusable it is a tab stop (`tabindex="0"`, with an inset focus ring), so a keyboard can scroll it
|
|
1604
|
+
in Safari and WKWebView (which never focus a scroll container on their own; Chromium does); a
|
|
1605
|
+
control appearing inside takes it out of the Tab order again (re-checked per frame on resize and
|
|
1606
|
+
content changes, including `hidden` and `inert` toggles). Hidden, inert and unrendered controls do
|
|
1607
|
+
not count, and overflow means more than 2 px (the iOS body's 1 px rubber-band extra is not
|
|
1608
|
+
overflow). See Migration 36.
|
|
1609
|
+
|
|
1610
|
+
### header
|
|
1611
|
+
|
|
1612
|
+
JSDoc of `header.component.ts` for the API reference: `@slot` docs for `start`, `default`, `end`. No
|
|
1613
|
+
behaviour change.
|
|
1614
|
+
|
|
1615
|
+
The toolbar pads the top safe area (`pt-safe-top`) only while the page's
|
|
1616
|
+
`BfPageContext.safeAreaTop()` is true; inside a page-sheet `bf-modal` (false) it adds none
|
|
1617
|
+
(Migration 29).
|
|
1618
|
+
|
|
1619
|
+
**Behaviour:** large text (F-051). The bar has a minimum height instead of a fixed one
|
|
1620
|
+
(`min-h-11` in iOS mode, `min-h-16` in Material mode) and grows with its content; the header
|
|
1621
|
+
measures the bar title as `--bf-header-title-height` (the same `ResizeObserver` pass as the slot
|
|
1622
|
+
insets) and keeps the bar at least that tall, since the centred iOS title box is absolutely
|
|
1623
|
+
positioned. The bar title wraps to at most two lines (`line-clamp-2 wrap-anywhere`, centred in iOS
|
|
1624
|
+
mode) instead of truncating. The `start` slot may shrink (`min-w-0 shrink`) and the `end` slot
|
|
1625
|
+
never does (`shrink-0`), so actions stay whole. In iOS mode, where the centred title is taken out
|
|
1626
|
+
of the row's flow, the `start` slot is capped by `--bf-header-start-max` only when the title does
|
|
1627
|
+
not fit on one line beside the slots at their full width: the title may then claim up to half the
|
|
1628
|
+
bar, and only a back button's text (`bf-back-button`'s `[data-bf-back-text]`) gives way. The cap
|
|
1629
|
+
is never below the slot's other content or 44 px, so two start buttons stay whole, and the title
|
|
1630
|
+
inset reads the slot's `scrollWidth` too. It is measured from the bar's width, a hidden one-line
|
|
1631
|
+
copy of the title and the slot's full and minimum widths, so it never depends on the capped slot
|
|
1632
|
+
itself. A projected title (no `title` input) gets no cap. The large title's font and line height are capped
|
|
1633
|
+
at 200 % (`min(var(--text-display), 80px)`, `min(var(--text-display--line-height), 88px)`), in px
|
|
1634
|
+
because iOS Dynamic Type scales the root.
|
|
1635
|
+
|
|
1636
|
+
**Behaviour:** the collapse range is the large title block's rendered height, bounded to 44–200 px
|
|
1637
|
+
(was a fixed 56 px, while the iOS large title renders about 70 px; a block taller than 200 px
|
|
1638
|
+
collapses its whole height over 200 px of scroll), re-read whenever it resizes; the snap goes to
|
|
1639
|
+
it; `data-bf-scrolled` switches on at full collapse, as before, now at that height (with 0.5 px of
|
|
1640
|
+
tolerance for a fractional snap landing). See Migration 31.
|
|
1641
|
+
|
|
1642
|
+
**Behaviour:** compact heights (F-054). Under `(max-height: 500px)` (phone landscape, split
|
|
1643
|
+
screen; read with `matchMedia` and followed live, the listener removed on destroy) the header
|
|
1644
|
+
renders no large title: the title is the bar's `h1`, the bar is at least 44 px in both modes and
|
|
1645
|
+
nothing collapses or snaps.
|
|
1646
|
+
|
|
1647
|
+
**Behaviour:** without a large title (`largeTitle` false, or a compact height) the scroll never
|
|
1648
|
+
moves `--bf-header-progress` and never snaps; a detail page used to snap to 0 or 56 px when a
|
|
1649
|
+
scroll stopped in its first 56 px.
|
|
1650
|
+
|
|
1651
|
+
### keyboard-assist
|
|
1652
|
+
|
|
1653
|
+
New exported `BfKeyboardAssistBehavior` (`'smooth' | 'instant'`), the type of
|
|
1654
|
+
`bfKeyboardAssistBehavior` (was an inline union with the same values).
|
|
1655
|
+
|
|
1656
|
+
JSDoc of `keyboard-assist.directive.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1657
|
+
symbols. No behaviour change.
|
|
1658
|
+
|
|
1659
|
+
**Behaviour:** the paddings are measured in the render phase's early read and written in its write
|
|
1660
|
+
phase (`afterRenderEffect`) instead of an `effect`, and the settled correction runs after that
|
|
1661
|
+
render's write (measured in the early read phase, scrolled in the write phase, and only while the
|
|
1662
|
+
keyboard is still shown, also after a deferral); the will-show and settled reactions stay
|
|
1663
|
+
`effect`s, since they schedule timers. Internal timing only, no API change.
|
|
1664
|
+
|
|
1665
|
+
### overscroll-stretch
|
|
1666
|
+
|
|
1667
|
+
JSDoc of `overscroll-stretch.directive.ts` for the API reference: `@summary`/`@since` on the
|
|
1668
|
+
exported symbols. No behaviour change.
|
|
1669
|
+
|
|
1670
|
+
### motion
|
|
1671
|
+
|
|
1672
|
+
JSDoc of `motion.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1673
|
+
behaviour change.
|
|
1674
|
+
|
|
1675
|
+
### horizontal-scroll
|
|
1676
|
+
|
|
1677
|
+
JSDoc of `horizontal-scroll.directive.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1678
|
+
symbols. No behaviour change.
|
|
1679
|
+
|
|
1680
|
+
**Deprecated:** `bfHorizontalScroll` (`BfHorizontalScrollDirective`) is not needed; removed in
|
|
1681
|
+
0.4.0. `touch-action` is not inherited, and a touch intersects the values only up to its nearest
|
|
1682
|
+
scroll container, so an `overflow-x-auto` row pans on its own under the outlet's
|
|
1683
|
+
`touch-action: pan-y`, as a device check on the iOS Simulator and the Android emulator confirmed
|
|
1684
|
+
(both scrolled without the directive). The docs and JSDoc now say so; the directive keeps setting
|
|
1685
|
+
`touch-action: pan-x pan-y` and `data-bf-horizontal-scroll` until it is removed. See Migration 37.
|
|
1686
|
+
|
|
1687
|
+
### nav-controller
|
|
1688
|
+
|
|
1689
|
+
JSDoc of `nav-controller.service.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1690
|
+
symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
|
|
1691
|
+
leaves them out. No behaviour change.
|
|
1692
|
+
|
|
1693
|
+
The `router.events` subscription says in a comment why it stays: the direction hint belongs to one
|
|
1694
|
+
navigation id's start and end. No behaviour change.
|
|
1695
|
+
|
|
1696
|
+
### page
|
|
1697
|
+
|
|
1698
|
+
JSDoc of `page-context.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1699
|
+
behaviour change.
|
|
1700
|
+
|
|
1701
|
+
JSDoc of `page.component.ts` for the API reference: `@slot` docs for `default`. No behaviour change.
|
|
1702
|
+
|
|
1703
|
+
`BfPageContext` gains `safeAreaTop` (a `signal(true)`): whether the page reaches the top of the
|
|
1704
|
+
screen, so its header and a `fullscreen` content pad the top safe area. `bf-modal`'s page-sheet
|
|
1705
|
+
presentation sets it to false.
|
|
1706
|
+
|
|
1707
|
+
`BfPageContext` gains `mainLandmark` (a `signal(true)`): whether the page's `bf-content` is the
|
|
1708
|
+
`main` landmark. A `bf-page` outside a `bf-stack-outlet` (a docs illustration) or nested in
|
|
1709
|
+
another page sets it to false, so the document keeps one `main` (Migration 36).
|
|
1710
|
+
|
|
1711
|
+
### stack-outlet
|
|
1712
|
+
|
|
1713
|
+
**Deprecated:** `stackChange` → `stackChanged`, `transitionStart` → `transitionStarted` and
|
|
1714
|
+
`transitionEnd` → `transitionEnded`. The new outputs emit the same `BfStackChange` and
|
|
1715
|
+
`BfStackTransitionEvent` objects at the same moments; the old names still emit them right after
|
|
1716
|
+
until 0.4.0 (see Migration 23).
|
|
1717
|
+
|
|
1718
|
+
JSDoc of `stack-outlet.component.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1719
|
+
symbols; `@param` docs; template handlers and framework hooks marked `@internal`, so the reference
|
|
1720
|
+
leaves them out. No behaviour change.
|
|
1721
|
+
|
|
1722
|
+
Setup in `docs.md`: `provideBfShell()` provides the configuration and the reuse strategy and starts
|
|
1723
|
+
the platform services; `BfNavController` and the overlay services are root-provided and
|
|
1724
|
+
tree-shakable.
|
|
1725
|
+
|
|
1726
|
+
The `router.events` subscription says in a comment why it stays: the stack pairs each navigation
|
|
1727
|
+
id's start with its own end. No behaviour change.
|
|
1728
|
+
|
|
1729
|
+
**Behaviour:** the iOS edge swipe-back arms only for `pointerType` `touch`: a mouse drag or a
|
|
1730
|
+
text selection from the left edge, and a pen, never pop the page (a desktop web page with the mode
|
|
1731
|
+
forced to `ios`). See Migration 39.
|
|
1732
|
+
|
|
1733
|
+
New `maxLivePages` (through `provideBfShell()`, default `Infinity`): above it, the outlet destroys
|
|
1734
|
+
the components of the deepest pages of a stack and keeps a record of each (route, key, history
|
|
1735
|
+
position, navigation id, URL and the `bf-content` scroll position); the top `maxLivePages` pages
|
|
1736
|
+
stay alive. A pop back to such a page creates it again from the re-activated route in a new frame,
|
|
1737
|
+
restores its scroll after the first render and runs the pop transition and lifecycle. Deep pages
|
|
1738
|
+
come back fresh: nested outlets inside a destroyed page lose their stacks. With `maxLivePages: 1`
|
|
1739
|
+
the swipe-back does not arm (the page below is a record). The deprecated `stackChange`,
|
|
1740
|
+
`transitionStart` and `transitionEnd` keep emitting after their replacements.
|
|
1741
|
+
|
|
1742
|
+
The host's `touch-action: pan-y` comment and the docs follow the Pointer Events model: page
|
|
1743
|
+
scrollers, horizontal rows included, keep their own panning (see `### horizontal-scroll`).
|
|
1744
|
+
|
|
1745
|
+
### stack-reuse-strategy
|
|
1746
|
+
|
|
1747
|
+
JSDoc of `stack-reuse-strategy.ts` for the API reference: `@summary`/`@since` on the exported
|
|
1748
|
+
symbols; `@param` docs. No behaviour change.
|
|
1749
|
+
|
|
1750
|
+
### transitions
|
|
1751
|
+
|
|
1752
|
+
JSDoc of `transitions.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1753
|
+
`@param` docs. No behaviour change.
|
|
1754
|
+
|
|
1755
|
+
### view-lifecycle
|
|
1756
|
+
|
|
1757
|
+
JSDoc of `view-lifecycle.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1758
|
+
`@param` docs; template handlers and framework hooks marked `@internal`, so the reference leaves
|
|
1759
|
+
them out. No behaviour change.
|
|
1760
|
+
|
|
1761
|
+
### haptics
|
|
1762
|
+
|
|
1763
|
+
JSDoc of `haptics.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1764
|
+
`@param` docs. No behaviour change.
|
|
1765
|
+
|
|
1766
|
+
### keyboard
|
|
1767
|
+
|
|
1768
|
+
JSDoc of `keyboard.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1769
|
+
`@param` docs. No behaviour change.
|
|
1770
|
+
|
|
1771
|
+
### platform
|
|
1772
|
+
|
|
1773
|
+
JSDoc of `platform.service.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1774
|
+
`@param` docs. No behaviour change.
|
|
1775
|
+
|
|
1776
|
+
New token `BF_THEME_STORAGE` (`InjectionToken<Pick<Storage, 'getItem' | 'setItem'>>`, additive):
|
|
1777
|
+
`BfPlatformService` reads and writes the theme choice (key `bf-theme`) through it. Its default is
|
|
1778
|
+
`localStorage` when available, otherwise an in-memory map (before: every read and write went to
|
|
1779
|
+
`localStorage` directly and was skipped where it threw). Provide another store to keep the choice
|
|
1780
|
+
elsewhere; `provideBfShellTesting()` provides an in-memory one.
|
|
1781
|
+
|
|
1782
|
+
**Behaviour:** `BfPlatformService` applies the `dark` class on `<html>` and stores the theme
|
|
1783
|
+
synchronously at construction, so a freshly provided shell has the class before any change
|
|
1784
|
+
detection; the `effect` only keeps later changes in sync. No API change.
|
|
1785
|
+
|
|
1786
|
+
**Behaviour:** on native iOS and Android with `provideBfShell()` (and `syncSystemBars` not
|
|
1787
|
+
`false`), `BfPlatformService` calls Capacitor's `SystemBars.setStyle()` at launch and on every
|
|
1788
|
+
change of `dark()` (and of the system theme), so the bar icons stay readable in the in-app theme;
|
|
1789
|
+
a failure goes to `console.error` (Migration 30). New exported `bfSystemBarsStyle(dark, platform,
|
|
1790
|
+
webViewMajor)`: `Dark` (light icons) for a dark theme, else `Light`, and `Default` on Android
|
|
1791
|
+
WebView < 140 (parsed from `Chrome/<n>` in the user agent), which pads the page below the bars so
|
|
1792
|
+
that they show the activity's window background, which follows the system theme. Give the window
|
|
1793
|
+
the page colour (`android:windowBackground`, see the `/mobile` guide) so the bars match the page
|
|
1794
|
+
when both themes agree. The WebView ≥ 140 case assumes `viewport-fit=cover` and the default
|
|
1795
|
+
`SystemBars.insetsHandling: 'css'`.
|
|
1796
|
+
|
|
1797
|
+
### text-scale
|
|
1798
|
+
|
|
1799
|
+
JSDoc of `text-scale.service.ts` for the API reference: `@summary`/`@since` on the exported symbols.
|
|
1800
|
+
No behaviour change.
|
|
1801
|
+
|
|
1802
|
+
### shell
|
|
1803
|
+
|
|
1804
|
+
New exported `BfShellConfigMode` (`'ios' | 'md' | 'auto'`), `BfShellDynamicType`
|
|
1805
|
+
(`false | BfShellDynamicTypeLimit`) and `BfShellDynamicTypeLimit` (`{ maxScale: number }`), the
|
|
1806
|
+
types of `BfShellConfig.mode` and `dynamicType` (were inline types with the same values).
|
|
1807
|
+
|
|
1808
|
+
JSDoc of `provide-bf-shell.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1809
|
+
`@param` docs. No behaviour change.
|
|
1810
|
+
|
|
1811
|
+
JSDoc of `shell-config.ts` for the API reference: `@summary`/`@since` on the exported symbols;
|
|
1812
|
+
`@default` on the option properties the code applies. No behaviour change.
|
|
1813
|
+
|
|
1814
|
+
**Behaviour:** `provideBfShell()` lists only the shell config, the route reuse strategy,
|
|
1815
|
+
`BF_OVERLAY_HOST` and the environment initializer (which still starts the platform, back-button,
|
|
1816
|
+
keyboard, haptics and text-scale services). `BfNavController`, the platform services and the
|
|
1817
|
+
overlay services (`BfOverlayStack`, `BfOverlayService`, `BfSheetService`, `BfModalService`,
|
|
1818
|
+
`BfToastService`, `BfActionSheetService`, `BfAlertDialogService`) are root-provided and
|
|
1819
|
+
tree-shakable, so a shell app that never injects an overlay service bundles no overlay code (the
|
|
1820
|
+
consumer smoke's new `mobile-shell` build holds no `cdk-overlay-pane` or `cdk-overlay-backdrop`
|
|
1821
|
+
string). Under
|
|
1822
|
+
`provideBfShell()` in a lazy route's environment injector, the overlay services now resolve from
|
|
1823
|
+
the root injector (Migration 22).
|
|
1824
|
+
|
|
1825
|
+
New `BfShellConfig.syncSystemBars` (default `true`; also in `BF_SHELL_DEFAULTS`): the native
|
|
1826
|
+
system bars follow the in-app theme (see `### platform`); `false` leaves them to the app.
|
|
1827
|
+
|
|
1828
|
+
New `BfShellConfig.backAtRoot?: BfBackAtRoot` (`'minimize' | 'exit'`, default `'minimize'`; new
|
|
1829
|
+
exported type; also in `BF_SHELL_DEFAULTS`): what the Android back button does when nothing can go
|
|
1830
|
+
back. See `### back-button service` and Migration 34.
|
|
1831
|
+
|
|
1832
|
+
New `BfShellConfig.maxLivePages?: number` (default `Infinity`, also in `BF_SHELL_DEFAULTS`): how
|
|
1833
|
+
many pages of each stack stay alive in every `bf-stack-outlet` (see `### stack-outlet`). No
|
|
1834
|
+
behaviour change by default.
|
|
1835
|
+
|
|
1836
|
+
`provideBfShell()`'s `BF_OVERLAY_HOST` sets `handlesSystemBack` on native Android, where
|
|
1837
|
+
Capacitor's `backButton` event already reaches the overlays, so they create no `CloseWatcher`
|
|
1838
|
+
there.
|
|
1839
|
+
|
|
1840
|
+
### back-button service
|
|
1841
|
+
|
|
1842
|
+
**Behaviour:** when no handler claims the back press (every shown stack at its first page),
|
|
1843
|
+
`BfBackButtonService` calls `App.minimizeApp()`, so the app moves to the background and a
|
|
1844
|
+
relaunch resumes warm; with `provideBfShell({ backAtRoot: 'exit' })` it calls `App.exitApp()` as
|
|
1845
|
+
before. It reads `BF_SHELL_CONFIG` (optional; `'minimize'` without it), and a failure goes to
|
|
1846
|
+
`console.error`. See Migration 34.
|
|
1847
|
+
|
|
1848
|
+
### toast-ref
|
|
1849
|
+
|
|
1850
|
+
JSDoc of `toast-ref.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1851
|
+
behaviour change.
|
|
1852
|
+
|
|
1853
|
+
### choice
|
|
1854
|
+
|
|
1855
|
+
JSDoc of `choice.ts` for the API reference: `@summary`/`@since` on the exported symbols. No
|
|
1856
|
+
behaviour change.
|
|
1857
|
+
|
|
1858
|
+
### avatar
|
|
1859
|
+
|
|
1860
|
+
JSDoc of `avatar.component.ts` for the API reference: `@summary`/`@since` on the exported symbols.
|
|
1861
|
+
No behaviour change.
|
|
1862
|
+
|
|
1863
|
+
**Behaviour:** the circle is at least 4.5ex of its initials (`min-w-[4.5ex] min-h-[4.5ex]`), so at
|
|
1864
|
+
Android font scale 2.0 (a text zoom that doubles the rendered initials, and `ex` with them, but not
|
|
1865
|
+
em, rem or px lengths) it grows with
|
|
1866
|
+
them instead of cutting them off, e.g. the `sm` avatar in a header's `end` slot (F-051). At the
|
|
1867
|
+
default size and with iOS Dynamic Type every size is larger than that minimum (with Manrope: sm
|
|
1868
|
+
30 px against 26.7, md 40 against 31.5, lg 84 against 63), so nothing changes.
|
|
1869
|
+
|
|
1870
|
+
### empty-state
|
|
1871
|
+
|
|
1872
|
+
New exported `BfEmptyStateHeadingLevel` (`2 | 3`), the type of `headingLevel` (was an inline union
|
|
1873
|
+
with the same values).
|
|
1874
|
+
|
|
1875
|
+
JSDoc of `empty-state.component.ts` for the API reference: `@slot` docs for `icon`, `actions`. No
|
|
1876
|
+
behaviour change.
|
|
1877
|
+
|
|
1878
|
+
### list
|
|
1879
|
+
|
|
1880
|
+
JSDoc of `list.component.ts` for the API reference: `@slot` docs for `default`. No behaviour change.
|
|
1881
|
+
|
|
1882
|
+
### docs site
|
|
1883
|
+
|
|
1884
|
+
Examples and demos use the convention's output names: `(changed)` on `bf-chip`, `bf-tab-bar` and
|
|
1885
|
+
`bfMenuItemCheckbox`, `(stackChanged)` on `bf-stack-outlet`, `(refreshRequested)` on
|
|
1886
|
+
`bf-refresher`. The API reference index gains an `@bitforgehq/ui/number-input` group for its
|
|
1887
|
+
two new types.
|
|
1888
|
+
|
|
1889
|
+
The theme configurator derives contrast-safe tokens for any primary: `primary-strong` steps darker
|
|
1890
|
+
(light) or lighter (dark) until 4.5:1 on `primary-soft`, `primary-soft-hover`, `bg` and `surface`
|
|
1891
|
+
(`#fde047` gave 2.03:1 before); the new `primary-text` steps until 4.5:1 on `bg` and `surface`;
|
|
1892
|
+
`on-primary` is white only at ≥ 4.5:1 (was 3:1), else black, and the hover keeps 4.5:1 under it;
|
|
1893
|
+
status colours step until 4.5:1 on their soft tint, `bg` and `surface`, and `danger-border` until
|
|
1894
|
+
3:1. New "Control radius" slider (0–24 px) with a "Pill" switch (on by default) emitting
|
|
1895
|
+
`--radius-control`; a stored config from before (no control radius) reads as a pill.
|
|
1896
|
+
`tools/theme-contrast.test.ts` asserts every brand, status and ring pair for the shipped theme
|
|
1897
|
+
and for six primaries (`#fde047`, `#0b0b0b`, `#2b26ba`, `#10b981`, `#ef4444`, `#a855f7`) in both
|
|
1898
|
+
modes, plus the state indicators against what they sit on (checkbox mark and toggle knob
|
|
1899
|
+
on the primary fill, radio dot in its ring, progress fill on its track).
|
|
1900
|
+
|
|
1901
|
+
New guides: "Density and sizes" (`/density`: the `BfSize` scale per widget, a live 20-field
|
|
1902
|
+
Signal Forms form in all three steps, each a sub-tree with its own `provideBfDefaults()`, setups for
|
|
1903
|
+
a desktop app, a sub-tree and a Capacitor app, and the hit-area rule; its filter row is a named
|
|
1904
|
+
`role="group"`, and the page states that its density-scaled layout gaps are the demo's own
|
|
1905
|
+
convention, read from `BF_DEFAULTS`, which `provideBfDefaults()` never applies to an app's layout)
|
|
1906
|
+
and "Labels and language"
|
|
1907
|
+
(`/labels`: tables of every built-in string and default validation message rendered from
|
|
1908
|
+
`BF_DEFAULT_LABELS`, so they cannot drift, a live German sub-tree under `provideBfLabels()`, and the
|
|
1909
|
+
"ss, never ß" rule for German copy); both in the navigation, search, `llms.txt` and the sitemap.
|
|
1910
|
+
The Forms guide gains "Typed values" (numeric select ids, `null` for none, `compareWith`,
|
|
1911
|
+
`bf-number-input` with `min()`) and "Default messages"; its live example's team select binds
|
|
1912
|
+
numeric ids, and its Reactive Forms example types its controls `number | null` and shows default
|
|
1913
|
+
messages without any `errorMessage`. The consumer-smoke job also builds and tests a dense (`sm`)
|
|
1914
|
+
desktop Signal Forms form with German default messages, typed values and links.
|
|
1915
|
+
|
|
1916
|
+
The desktop overlay pages (`dialog`, `desktop-toast`, `menu`, `menu-button`) run their live demos
|
|
1917
|
+
in the page itself; the dialog demo adds "Release notes" (a body that scrolls) and "Share…" (a
|
|
1918
|
+
`bf-menu-button` in the dialog footer), reports the `confirm()` answer id, and every overlay demo
|
|
1919
|
+
marks its triggers and result line with `data-testid`. The install line of a component page now
|
|
1920
|
+
names the peers of its entry point: `bun add @bitforgehq/ui @angular/cdk` for
|
|
1921
|
+
`@bitforgehq/ui/overlay`, `bun add @bitforgehq/ui @angular/cdk @angular/router @capacitor/core rxjs`
|
|
1922
|
+
for `@bitforgehq/ui/mobile`. The `/mobile` overlay pages (alert dialog, toast, modal, sheet, action
|
|
1923
|
+
sheet) gain a "Desktop" section pointing to `bf-dialog`, `BfDialogService.confirm()`,
|
|
1924
|
+
`BfDesktopToastService` and `bf-menu`. New end-to-end proof at 1280 × 800
|
|
1925
|
+
(`e2e/docs/overlay-desktop.spec.ts`): wheeling over an open dialog or its backdrop leaves the page
|
|
1926
|
+
where it was and it scrolls again once closed; a long dialog scrolls only its body; the page is
|
|
1927
|
+
`inert`, focus is trapped and restored; `confirm()` focuses Cancel and ignores the backdrop;
|
|
1928
|
+
history back and a link item close the dialog or menu; `Escape` in a menu inside a dialog closes
|
|
1929
|
+
only the menu; a toast sits bottom right, is announced, never locks or isolates the page, pauses
|
|
1930
|
+
under the pointer and hands focus back on `Escape`; the full menu-button keyboard path. axe also
|
|
1931
|
+
scans the open dialog, confirm, menu, submenu and toast stack at `moderate` impact, light and dark.
|
|
1932
|
+
|
|
1933
|
+
Every example is a compiled file, and the Code tab shows exactly the file the Preview renders
|
|
1934
|
+
(F-079). The demos moved to `projects/docs/src/app/examples/<name>/overview-example.component.ts`
|
|
1935
|
+
(`<Name>OverviewExampleComponent`, `app-example-<name>-overview`) and the guide examples to
|
|
1936
|
+
`examples/guides/{forms,density,labels}/`; each loads in its own chunk through the generated
|
|
1937
|
+
`examples/example-registry.ts`, and the generated `examples/example-sources.ts` holds every file
|
|
1938
|
+
verbatim for the Code tabs. The new `app-example` card renders one example by id: in the page, in
|
|
1939
|
+
the phone frame (a `/mobile` component's Code tab is now its mobile-demo showcase page), or as code
|
|
1940
|
+
only with no tab list (the Forms guide's Reactive Forms example, a consumer-smoke fixture file).
|
|
1941
|
+
The Density and Labels guide examples gain a Code tab. `docs.md` sections can place examples with
|
|
1942
|
+
`<!-- example: <id> -->` markers. At 320 px the Preview column takes the full width (it shrinks to
|
|
1943
|
+
the demo only from `sm` up, inside `p-4 sm:p-10`), so no widget page's demo frame overflows
|
|
1944
|
+
(F-094, list-item); `e2e/docs/widgets.spec.ts` checks every widget page.
|
|
1945
|
+
|
|
1946
|
+
Every example in the component docs is a compiled file now (F-092): the fenced `html` and `ts`
|
|
1947
|
+
blocks of every `docs.md` became `examples/<name>/<slug>-example.component.ts` files, each with a
|
|
1948
|
+
`renderExample()` spec, placed with `<!-- example: <id> -->` markers. A component page shows each
|
|
1949
|
+
as its own Preview | Code card; `/mobile` examples are compiled but code-only (`@preview none`),
|
|
1950
|
+
as are the few that would cover the page (the floating tab bar, a fixed action bar, an app config
|
|
1951
|
+
or a route guard). Markers work in every section (the overview, Theming, Accessibility and the
|
|
1952
|
+
notes too), as in the agent pages. A docs file keeps only `css`, `bash`, `json` and `text` fences.
|
|
1953
|
+
`tools/docs-contract.test.ts` enforces it (no other fence, no `<bf-` in a fence, every marker
|
|
1954
|
+
resolves, every docs file has a marker, no two components share their usage sections), and the
|
|
1955
|
+
new `tools/attr-drift.test.ts` parses every example template, every `@example` and every inline
|
|
1956
|
+
`<…>` code span with Angular's template parser and fails when an attribute or binding on a library
|
|
1957
|
+
element names no input, output, model or alias of it (or a native attribute), so renaming an input
|
|
1958
|
+
breaks `bun run test:tools` wherever the docs still use the old name. The generator reads a
|
|
1959
|
+
co-located `<component>.docs.md` before the folder's `docs.md` (`bf-tab`, `bf-list-item`). An
|
|
1960
|
+
example chunk that fails to load (a stale deploy) shows "Couldn't load the example" and logs the
|
|
1961
|
+
error instead of "No demo"; the `app-example` card no longer copies its `id` input onto the host as
|
|
1962
|
+
a DOM id (it names its example in `data-example`). The drift check also fails on library markup
|
|
1963
|
+
that does not parse (it checks the markup a TypeScript `@example` carries in its comments and
|
|
1964
|
+
template literals).
|
|
1965
|
+
|
|
1966
|
+
A deep link to a section (`/components/input#with-signal-forms`) keeps its heading in view while
|
|
1967
|
+
the cards above it load: for 3 s every resize of the page scrolls to it again, until the user
|
|
1968
|
+
scrolls (wheel, touch, a scrollbar drag), clicks or presses a key, or the page navigates on.
|
|
1969
|
+
|
|
1970
|
+
A component page no longer downloads `ai/MANIFEST.json` (784 kB, most of it example sources the
|
|
1971
|
+
site bundles anyway): `bun run ai:docs` also writes `projects/docs/public/manifest/<name>.json`, the
|
|
1972
|
+
component's manifest entry without `examples[].source`, and the page fetches only its own
|
|
1973
|
+
(`manifest/button.json`, at most 25 kB raw). `ai/MANIFEST.json` is unchanged for agents;
|
|
1974
|
+
`tools/examples.test.ts` checks every site entry against it.
|
|
1975
|
+
|
|
1976
|
+
API tables come from the checker (F-082). The columns are Property, Type, Default and
|
|
1977
|
+
Description; badges mark a required member, a two-way `model()` (`[(value)]`), a member that
|
|
1978
|
+
`[formField]` sets (`invalid`, `touched`, `errors`, …) and a deprecated one. The type cell links
|
|
1979
|
+
every public symbol it names (`BfSize` to `/api/bf-size`, a component class to its page) and
|
|
1980
|
+
shows the expanded union under an alias. Component pages gain a Slots table (each slot once, with
|
|
1981
|
+
its doc) and a Methods section (signature, doc and a parameter table). New API reference:
|
|
1982
|
+
`/api` lists every service, directive, function, token, class, interface, type and constant by
|
|
1983
|
+
entry point and kind, and `/api/<slug>` renders one from `ai/REFERENCE.json` (fetched on demand):
|
|
1984
|
+
its import line, a directive's usage docs with live examples, the signature, a union's values,
|
|
1985
|
+
the members (a directive's inputs and outputs, an interface's properties with their defaults),
|
|
1986
|
+
the methods, what it uses and what uses it. The overlay services' pages link their options
|
|
1987
|
+
types, whose Options tables show the defaults. Titles and descriptions come from the bundled
|
|
1988
|
+
`REFERENCE.index.json`; the sitemap lists every reference page. `bfOverscrollStretch`'s
|
|
1989
|
+
hand-written input table is gone: its reference page shows the generated one. `/api/<component class slug>`
|
|
1990
|
+
(`/api/bf-button-component`) redirects to the component's page.
|
|
1991
|
+
|
|
1992
|
+
Every component page (and a directive's reference page) gains a "When to use" section right after
|
|
1993
|
+
the Overview, saying when to pick the widget and what to use instead, with links to the related
|
|
1994
|
+
component and `/api` pages (F-087). Every interactive widget's Accessibility section ends with a
|
|
1995
|
+
"Keyboard" table (`| Key | Action |`, keys as `<kbd>`) of what the code does: native elements list
|
|
1996
|
+
the browser's keys, the menu and dialog keep 3c's tables. A "Desktop" section before Theming
|
|
1997
|
+
covers hover, the pointer, `focus-visible` and density on desktop for every interactive widget,
|
|
1998
|
+
and what to use on desktop for every `/mobile` page (3c's overlay "Desktop" sections are kept).
|
|
1999
|
+
The page order is fixed: header, primary example, Overview, When to use, the usage sections,
|
|
2000
|
+
Desktop, Theming, Accessibility (with Keyboard), API, Notes, Migration notes. The `bf-tabs` page's
|
|
2001
|
+
"Keyboard" section (the soft keyboard) is now "Soft keyboard" (`#soft-keyboard`).
|
|
2002
|
+
`tools/docs-contract.test.ts` fails on a missing or misplaced section: When to use in every docs
|
|
2003
|
+
file, first, with an "instead" and a `/components` or `/api` link; a Keyboard table for every
|
|
2004
|
+
component or directive that is interactive by the extracted API (a model or an output, or a
|
|
2005
|
+
`button`, `a`, `input`, `select`, `textarea` or `tabindex` in its template) unless it is on a short
|
|
2006
|
+
allow-list with a reason (`bf-content`, `bf-page`, `bf-stack-outlet`, `bf-refresher`); Desktop for
|
|
2007
|
+
every interactive and every `/mobile` widget; and every `/components/<name>` and `/api/<slug>`
|
|
2008
|
+
link in the docs must name a page that exists. Links to the site's own pages in rendered markdown
|
|
2009
|
+
(root-relative or on `https://bitforge-ui.web.app`) resolve against the app's base href, so they
|
|
2010
|
+
work on a mirror served under a sub-path and in local builds, and stay real links (middle-click,
|
|
2011
|
+
copy link). A heading inside the prose (`### Keyboard`) carries its slug as an id, so
|
|
2012
|
+
`/components/menu#keyboard` lands on it at any width. The interactive rule also counts a template
|
|
2013
|
+
element that another interactive library widget matches by selector (transitively: the action
|
|
2014
|
+
sheet's `bf-list-item` and `bf-button` rows) and host key, click and focus listeners
|
|
2015
|
+
(`bfAction`'s focus and blur); `bf-modal`, `bf-sheet`, `bf-tabs` and `bf-header`, whose keys come
|
|
2016
|
+
from the overlay core or projected content, are listed by hand, and `bfAction`'s keys are checked
|
|
2017
|
+
on the card and list-item pages.
|
|
2018
|
+
|
|
2019
|
+
How-to guides. `/testing` covers the CDK dev dependency, the Vitest setup file (the CLI default)
|
|
2020
|
+
or a Jest config with `installBfJsdomShims()` and the `transformIgnorePatterns` the library needs,
|
|
2021
|
+
`provideBfTesting()` and `provideBfShellTesting()`, zoneless waiting, the harnesses in the page
|
|
2022
|
+
and in the overlay container, and a sheet under the shell's test providers; every file it shows is
|
|
2023
|
+
a consumer-smoke fixture spec that passes under Vitest. `/mobile` is now the Capacitor how-to
|
|
2024
|
+
("Mobile: Capacitor setup"): peers, providers, the root host's height, `index.html`,
|
|
2025
|
+
`capacitor.config.ts`, the Android manifest, a tabbed app (routes, `bf-tabs`, `bf-stack-outlet`,
|
|
2026
|
+
`bf-page`), keyboard and Dynamic Type, and running on a device. Its files are fixture files under
|
|
2027
|
+
`tools/fixtures/consumer/src/mobile/`, which consumer-smoke's `mobile-full` step builds as one app;
|
|
2028
|
+
then the phone frame ("Try it") and a Reference of every `/mobile` export, linked. `/theming`
|
|
2029
|
+
("Theming and dark mode") shows the theme configurator inline beside a live preview, where the
|
|
2030
|
+
generated block goes, dark mode (`dark`, `color-scheme`, the body classes), following the OS and
|
|
2031
|
+
remembering the choice, app-wide and sub-tree token overrides, the radius tokens and fonts; the
|
|
2032
|
+
configurator's controls are now `app-theme-configurator-panel`, and the floating launcher shows a
|
|
2033
|
+
visible "Theme" label from `sm` (its accessible name stays "Theme configurator"). `/overlays`
|
|
2034
|
+
("Dialogs, menus and toasts on desktop") has live examples for confirming a delete, a Signal Forms
|
|
2035
|
+
form in a dialog, row actions in a menu and undo in a toast, and says when a Capacitor app uses the
|
|
2036
|
+
`/mobile` overlays instead. `/troubleshooting` lists seven known failures with cause, fix and link.
|
|
2037
|
+
The guide steps live in `consts/testing.const.ts` and `consts/capacitor.const.ts`, which the site
|
|
2038
|
+
and `UI_GUIDE.md` both render. The Install row of a `/testing` reference page reads
|
|
2039
|
+
`bun add @bitforgehq/ui && bun add -d @angular/cdk`. `fixture/…` example ids may hold dots
|
|
2040
|
+
(`fixture/testing/sheet.spec`).
|
|
2041
|
+
The Testing guide's Jest path: install `jest`, `jest-environment-jsdom`, `jest-preset-angular` and
|
|
2042
|
+
`@types/jest`, set `tsconfig.spec.json`'s `types` to `["jest"]`, and call
|
|
2043
|
+
`setupZonelessTestEnv()` before `installBfJsdomShims()` (a bare import of
|
|
2044
|
+
`jest-preset-angular/setup-env/zoneless` initialises nothing, so every spec failed with "Need to
|
|
2045
|
+
call TestBed.initTestEnvironment() first"); its `transformIgnorePatterns` keeps Angular's locale
|
|
2046
|
+
files (`@angular/common/locales/.*\.js$`). consumer-smoke now runs the testing specs, plus a
|
|
2047
|
+
`de-CH` locale spec, under Jest with the guide's `jest.config.mjs` and setup file written from the
|
|
2048
|
+
same consts. The Capacitor guide says `@capacitor/keyboard` is needed, since the required
|
|
2049
|
+
`capacitor.config.ts` imports `KeyboardResize` from it.
|
|
2050
|
+
|
|
2051
|
+
The site navigation follows Diátaxis, from one source, `consts/nav.const.ts` (`NAV_GROUPS`, which
|
|
2052
|
+
replaces `guides.const.ts`): Get started (Install, Tutorial, Concepts), Guides (Forms and
|
|
2053
|
+
validation, Density, Labels and translation, Theming and dark mode, Dialogs, menus and toasts,
|
|
2054
|
+
Testing, Capacitor apps, Troubleshooting), Components, Foundations, Reference (API, For agents,
|
|
2055
|
+
Compatibility) and Changelog. No existing path changed. The header links Get started, Guides,
|
|
2056
|
+
Components, Reference and Changelog and marks the current group; the sidebar and the phone drawer
|
|
2057
|
+
show every group as a disclosure, open while it holds the current page; the footer mirrors the
|
|
2058
|
+
groups; search, the sitemap and `llms.txt` list the same pages. The home page is desktop first: a
|
|
2059
|
+
new subhead and six feature cards (forms and density, desktop overlays, theming, testing, agent
|
|
2060
|
+
docs, then the Capacitor shell), and the hero offers the `bun add @bitforgehq/ui` line with
|
|
2061
|
+
the version and a copy button. New pages: `/tutorial` ("Build a settings page", five compiled steps: the
|
|
2062
|
+
form, validation with the default messages, saving with a desktop toast, a "Delete account"
|
|
2063
|
+
confirm dialog and dark mode; consumer-smoke builds the five steps as one app against the packed
|
|
2064
|
+
library), `/concepts` (entry points and their peers, tree shaking, tokens, signals and Signal
|
|
2065
|
+
Forms, desktop versus Capacitor, client-only for 0.x, labels), `/compatibility` (the peer ranges,
|
|
2066
|
+
generated from `package.json`, the toolchain and Angular's supported browsers) and `/changelog`
|
|
2067
|
+
(this file, with a list of sections per version; anchors use the version number only, such as
|
|
2068
|
+
`#v0-3-0` and `#v0-3-0-migration`, so links survive the release). Each tutorial step is `SettingsComponent` (`app-settings`),
|
|
2069
|
+
saved verbatim as `src/app/settings.component.ts`, with step 5's
|
|
2070
|
+
`theme-preference.state.service.ts` beside it; consumer-smoke writes exactly those files and the
|
|
2071
|
+
page's `app.ts` into the fixture app and builds every step. The `/overlay` Install row now reads
|
|
2072
|
+
`@angular/cdk rxjs`, from the same entry point table as Concepts and Compatibility. The page keeps
|
|
2073
|
+
5 rem of scroll padding at the bottom, so the Theme launcher never covers a focused element.
|
|
2074
|
+
`deploy:docs` runs `ai:docs:check` first.
|
|
2075
|
+
|
|
2076
|
+
Every route is prerendered (F-086): `ng build docs` (`outputMode: 'static'`, no SSR server)
|
|
2077
|
+
writes one HTML file per path of `app/site-routes.ts` (home, the navigation pages, one page per
|
|
2078
|
+
component and per reference symbol from the package indexes, and `/404`), the one list the
|
|
2079
|
+
sitemap, the hosting check and the e2e suite read too. Each page carries its own `<title>`,
|
|
2080
|
+
description, canonical link and Open Graph tags, and its content: a component page's body is
|
|
2081
|
+
prerendered and hydrates when the browser is idle, and the entry it was rendered with comes with
|
|
2082
|
+
the page (no second request, no loading state first). Unknown paths answer 404 with the "Page not
|
|
2083
|
+
found" page: `firebase.json` drops the catch-all rewrite (`trailingSlash: false`, `no-cache` on
|
|
2084
|
+
pages), and `tools/finalize-docs.ts` moves `404/index.html` to `404.html` and fails the build when
|
|
2085
|
+
a route has no page or no head of its own; an unknown component name or reference slug falls
|
|
2086
|
+
through to the `**` route, so it hydrates the 404 page as prerendered. Example cards load their
|
|
2087
|
+
Preview (the example chunk, or the phone frame) only as they near the viewport, and render and
|
|
2088
|
+
highlight their code only once the Code tab is first shown. Until then, and while its demo loads,
|
|
2089
|
+
a card's Preview keeps the height it last rendered at (`examples/example-heights.json`, measured
|
|
2090
|
+
at 412 and 1280 px by `bun tools/measure-examples.ts`), so nothing below it moves: CLS on
|
|
2091
|
+
`/components/input` drops from 0.101 to 0.074. A reference page fetches its own
|
|
2092
|
+
symbol, `reference/<slug>.json` (written by `ai:docs`; the largest is 8 kB), instead of the whole
|
|
2093
|
+
280 kB `REFERENCE.json`, which stays whole for agents. A custom theme from the configurator goes
|
|
2094
|
+
on before the first paint (its derived tokens are kept under `bf-docs-theme-tokens`), as dark mode
|
|
2095
|
+
already did, and `ThemeStateService` touches no storage on the server. The header's theme toggle
|
|
2096
|
+
renders both icons and `styles.css` shows the one `html.dark` calls for, so a dark-mode reader
|
|
2097
|
+
sees the sun icon from the first paint, not only after hydration. Every prerendered page
|
|
2098
|
+
preloads the fonts. The e2e suite fails on any Angular hydration message (`NG05xx`). A prerendered `/changelog`
|
|
2099
|
+
keeps its versions as prerendered and never hydrates them (`@defer (hydrate never)`), so
|
|
2100
|
+
`CHANGELOG.md` is neither fetched again nor inlined in the page (561 kB to 361 kB, 149 kB to
|
|
2101
|
+
85 kB gzip); its section links are same-page anchors there, and its code fences stay
|
|
2102
|
+
unhighlighted.
|
|
2103
|
+
|
|
2104
|
+
Every page names a default Open Graph image, `og-image.png` (1200 × 630, with its alt text, as a
|
|
2105
|
+
`summary_large_image` card), so link previews show the brand instead of a bare title. It is
|
|
2106
|
+
rendered from `tools/og-image.html` (the docs tokens, Manrope, the favicon mark and the home hero's
|
|
2107
|
+
copy) by `bun tools/og-image.ts`, run by hand when the brand changes.
|
|
2108
|
+
|
|
2109
|
+
Code is highlighted in a web worker (`services/highlight.worker.ts`), so compiling Shiki's
|
|
2110
|
+
grammars and tokenizing a page's code never blocks the main thread: Lighthouse's total blocking
|
|
2111
|
+
time on the install, forms and mobile pages drops from 270–580 ms to at most 10 ms. Without
|
|
2112
|
+
`Worker` it highlights on the main thread as before. A request the worker answers with an error
|
|
2113
|
+
is highlighted on the main thread alone; a worker that crashes or leaves a request unanswered for
|
|
2114
|
+
10 s is started once more, then highlighting stays on the main thread. `finalize-docs.ts` fails a
|
|
2115
|
+
build whose worker bundle imports a chunk the build did not write, and `tools/highlight-worker.test.ts`
|
|
2116
|
+
fails a dynamic `import()` in the worker's module graph.
|
|
2117
|
+
|
|
2118
|
+
`bun run docs:hosting-check` serves the build in the Firebase hosting emulator (port 4402, a
|
|
2119
|
+
`demo-` project, no login) and checks every sitemap URL (200, its own title, description and
|
|
2120
|
+
canonical link), that unknown paths answer 404 with the "Page not found" page, and every link in
|
|
2121
|
+
`llms.txt` (F-083). `e2e/serve.ts` and Lighthouse answer like Hosting (`tools/static-resolve.ts`).
|
|
2122
|
+
Lighthouse now also audits `/components/input`, `/install` and `/forms` (F-091), from port 4410,
|
|
2123
|
+
keeping the median of three runs; a page may hold a conscious lower threshold in
|
|
2124
|
+
`PAGE_THRESHOLDS`, and none needs one.
|
|
2125
|
+
|
|
2126
|
+
The deploy no longer ships `index.csr.html` (`finalize-docs.ts` deletes the unused
|
|
2127
|
+
client-rendering shell), the `/mobile-demo/` rewrite takes extension-less paths only, so a missing
|
|
2128
|
+
demo script is a 404 instead of the demo's HTML, and the hosting check proves both, stops at once
|
|
2129
|
+
when port 4402 already answers, and loads an embedded demo route.
|
|
2130
|
+
|
|
2131
|
+
The ⌘K palette searches the full text of the site (F-084): every page and its sections, every
|
|
2132
|
+
component with its usage sections (the Keyboard tables included), inputs, outputs and methods,
|
|
2133
|
+
every reference symbol and its members, and "Not in Bitforge UI" hints for what the library does
|
|
2134
|
+
not provide (tooltip, table or data grid, date picker, combobox or autocomplete, rich text and
|
|
2135
|
+
charts), each pointing at what to use instead. Synonyms (dark mode, dropdown, modal, snackbar,
|
|
2136
|
+
validation, tailwind, capacitor) lead to the page that answers them. Results are grouped as Pages,
|
|
2137
|
+
Components, API, Sections and Hints, best match first; a section opens at its heading, and every
|
|
2138
|
+
result is a real link under the base href, so a modifier click opens it in a new tab. The index,
|
|
2139
|
+
`search-index.json`, is built after the prerender by `tools/gen-search-index.ts` from
|
|
2140
|
+
`MANIFEST.json`, `REFERENCE.json`, the prerendered pages and `consts/search-hints.const.ts`, and
|
|
2141
|
+
searched in the browser with MiniSearch. It is fetched once, when the palette first opens; until
|
|
2142
|
+
it arrives the palette filters page and component titles as before. `docs:hosting-check` checks it
|
|
2143
|
+
is served with its five-minute cache. A result is named by its title, the "Not in Bitforge UI"
|
|
2144
|
+
badge and its page, with its text as the description, so a screen reader reads a short name on
|
|
2145
|
+
each arrow press; group names are read in plain case; a pointer press keeps focus in the field;
|
|
2146
|
+
the field is `aria-expanded` only while there are results; a failed index fetch is retried on the
|
|
2147
|
+
next open; and a hint pointing at a page or section that is not indexed fails the build.
|
|
2148
|
+
|
|
2149
|
+
A deep link opened from inside the site (a search result, a link to `/components/sheet#snap-points`)
|
|
2150
|
+
lands on its heading even when the content above it grows first: a scroll the browser makes as the
|
|
2151
|
+
page changes height no longer ends the hold on the heading, only a scroll of the user's does.
|
|
2152
|
+
|
|
2153
|
+
The header's large-title collapse is proven in real browser engines (F-071): the new
|
|
2154
|
+
`e2e/header.spec.ts` runs on `iphone` (WebKit, iOS mode) and `pixel` (Chromium, Material mode)
|
|
2155
|
+
against the mobile demo's header showcase and reads the rendered page. At rest the large title is
|
|
2156
|
+
opaque and nothing is under the bar; half-way down its height (held, so the scroll-end snap waits)
|
|
2157
|
+
it is part-faded, the toolbar title not fully shown, no `data-bf-scrolled`, and the scroller's
|
|
2158
|
+
range is unchanged; nearly collapsed there is still no `data-bf-scrolled`; at the large title's
|
|
2159
|
+
height it is gone, the toolbar title shows and `data-bf-scrolled` switches on; back at the top the
|
|
2160
|
+
rest state returns. Halving the collapse range or switching the scroll-under trigger back to
|
|
2161
|
+
`scrollTop > 0` fails it.
|
|
2162
|
+
|
|
2163
|
+
The docs site is checked in WebKit too (F-073): the new `docs-webkit` Playwright project runs the
|
|
2164
|
+
page, axe and search specs in Desktop Safari against the same build, so a regression only Safari
|
|
2165
|
+
shows fails CI. axe blocks at `moderate` impact on every page (F-077), with an allow-list that takes
|
|
2166
|
+
only third-party or iframe content, with a reason, and is empty; it also scans, in both themes, the
|
|
2167
|
+
⌘K palette empty, with results and with none, the phone navigation drawer at 390 px, the floating
|
|
2168
|
+
and the inline theme configurator, the Code tab of `/components/button` and `/components/sheet`,
|
|
2169
|
+
`/`, `/components/button` and `/mobile` under reduced motion, and the desktop overlays. Fixes it
|
|
2170
|
+
found: the site header is labelled "Bitforge UI", so its banner stays unique next to the embedded
|
|
2171
|
+
demo's; the palette's results scroller is in the tab order, so a keyboard can scroll a long list;
|
|
2172
|
+
the extra tab bars on `/components/tab-bar` are named ("Sections", "Feed"); and the phone frame
|
|
2173
|
+
writes its iframe `src` only when it changes, so hydrating `/mobile` no longer loads the embedded
|
|
2174
|
+
demo a second time (WebKit logged the aborted module scripts as errors). New e2e: a hard-loaded
|
|
2175
|
+
`/changelog#<version>` lands on its heading, and the changelog's "On this page" lists the versions,
|
|
2176
|
+
jumps to one and follows the scroll.
|
|
2177
|
+
|
|
2178
|
+
The docs deploy from CI (F-083): `.github/workflows/docs.yml` verifies every push to `main` (the
|
|
2179
|
+
library build, agent docs, tools tests, the docs build, the docs e2e in Chromium and WebKit, the
|
|
2180
|
+
hosting check), then
|
|
2181
|
+
deploys that tested build to Firebase Hosting once the `FIREBASE_SERVICE_ACCOUNT_BITFORGE_UI` secret
|
|
2182
|
+
exists (without it the job prints a notice and skips) and to the GitHub Pages mirror once the
|
|
2183
|
+
`DOCS_PAGES_ENABLED` variable is `true`. `ci.yml` also runs the hosting check, and its Lighthouse
|
|
2184
|
+
step takes the median of five runs per page; `tools/lighthouse.ts` audits a page that misses a
|
|
2185
|
+
threshold once more before it fails. README "Docs deployment" lists the owner's steps. Both
|
|
2186
|
+
deploy jobs run only for `main` (a manual run on another branch verifies without deploying), the
|
|
2187
|
+
Firebase and Bun actions are pinned to commit SHAs, and the Pages job serves its sub-path build
|
|
2188
|
+
under `/BitforgeUI/` and checks its pages, base hrefs, linked assets and 404 before
|
|
2189
|
+
uploading (`tools/pages-check.ts`). The palette's results scroller is a group named "Search
|
|
2190
|
+
results".
|
|
2191
|
+
|
|
2192
|
+
The docs initial bundle grew from 517.98 kB on main to 563.06 kB (139.45 kB transfer; +8.7 %),
|
|
2193
|
+
across the nav, prerender/hydration and search work above: the eager nav const and sidebar logic
|
|
2194
|
+
of the new site chrome (Task 7, ~+7 kB); `provideClientHydration(withEventReplay(),
|
|
2195
|
+
withIncrementalHydration())`, the SEO service and the deep-link fragment hold that ships with
|
|
2196
|
+
prerendering (Task 8); and the ⌘K search palette's trigger and result list (Task 9; MiniSearch and
|
|
2197
|
+
`search-index.json` themselves stay in the palette's own lazy chunk, loaded only on first open, not
|
|
2198
|
+
in this total). Lighthouse still scores ≥ 90 on every audited page. The `initial` budget warning
|
|
2199
|
+
(`angular.json`, docs project) is now 580 kB, down from 600 kB, so the next regression is caught
|
|
2200
|
+
with less headroom.
|
|
2201
|
+
|
|
2202
|
+
The docs site's markdown pipe renders only markup that markdown itself generates (F-093): raw
|
|
2203
|
+
HTML, block or inline, renders as text (a bare `<kbd>` excepted, which the Keyboard tables use),
|
|
2204
|
+
and a link or image whose URL is not `http:`, `https:`, `mailto:`, relative or a `#fragment`
|
|
2205
|
+
renders as its text alone, also in inline mode (member descriptions in the API tables), and also
|
|
2206
|
+
when the scheme hides behind a character reference (`javascript:`) or a tab. Together with
|
|
2207
|
+
the generator's raw-HTML refusal, an `<img onerror>` in a JSDoc line can no longer run on the site.
|
|
2208
|
+
|
|
2209
|
+
New "Rendering" section on the Compatibility page: 0.x is client-rendered only, and what the
|
|
2210
|
+
server-render smoke (`tools/ssr-smoke.ts`) shows: all 27 fixtures (every root widget, `bf-icon`,
|
|
2211
|
+
`bf-number-input`, two `/mobile` pages and an `/overlay` dialog) render on the server without an
|
|
2212
|
+
error and 18 render identically twice; the list of the other 9 is built from the smoke's recorded
|
|
2213
|
+
results (`SSR_RECORDED`), and hydration stays unsupported and untested. The README and the UI guide
|
|
2214
|
+
gain a "Compatibility" section with the same text.
|
|
2215
|
+
|
|
2216
|
+
New examples: the `bf-chip-group` page (`chip-group/overview`, `single`, `multiple`),
|
|
2217
|
+
`badge/count-label`, `progress/value-text` and `progress/indeterminate`, `field/help-and-error`
|
|
2218
|
+
and `textarea/max-rows`. The Foundations page lists the four pressed tokens, and the theme
|
|
2219
|
+
configurator derives `--color-primary-pressed` and `--color-primary-soft-pressed` (the fill 0.12
|
|
2220
|
+
darker in light mode, lightness 0.88 in dark; the soft tint at 0.86 / 0.40), with
|
|
2221
|
+
`primary-strong` searched to 4.5:1 on the soft pressed tint as well. The demo host spec queues
|
|
2222
|
+
`requestAnimationFrame` for every test.
|
|
2223
|
+
|
|
2224
|
+
The Foundations page gains Focus, Motion and Z-index sections (F-061, F-062), and
|
|
2225
|
+
`docs/design/foundations.md` the same three tables. Focus states both treatments with their exact
|
|
2226
|
+
classes: controls `focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring`
|
|
2227
|
+
(keyboard focus only, 4 px) and text-entry controls
|
|
2228
|
+
`focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-ring` (any focus, because the
|
|
2229
|
+
caret field must show where typing goes), both ≥ 3:1 (WCAG 1.4.11) and the system outline under
|
|
2230
|
+
forced colours. Motion lists `--bf-duration-fast`, `--bf-duration-overlay`, `--bf-duration-push`,
|
|
2231
|
+
`--ease-ios`, `--ease-md` and `--ease-settle` with their users and the reduced-motion behaviour;
|
|
2232
|
+
Z-index lists `--bf-z-dim`, `--bf-z-sticky`, `--bf-z-tabbar` and `--bf-z-overlay`. The spacing
|
|
2233
|
+
steps list `--spacing-card-lg`. `tokens.const.spec.ts` fails when a `--bf-duration-*`, `--ease-*`
|
|
2234
|
+
or `--bf-z-*` declared in `theme.css` is missing from the tables, and checks the quoted focus
|
|
2235
|
+
classes against the class maps. New example `icon/filled` (Lucide icons with filled details).
|
|
2236
|
+
Focus also lists the local additions: the `bf-segmented` ring offset
|
|
2237
|
+
(`focus-visible:ring-offset-2 focus-visible:ring-offset-surface`) and its selected item's own
|
|
2238
|
+
forced-colours ring, and the outline variants' `focus-visible:border-primary`. Motion notes
|
|
2239
|
+
`DURATION_MD` (220 ms), the one duration without a CSS token because only Web Animations started
|
|
2240
|
+
from TypeScript use it (the Material fade-up, the refresher settle, the overscroll release).
|
|
2241
|
+
|
|
2242
|
+
The `/mobile` guide gains the step "Match the system bars to the theme" (what the shell does,
|
|
2243
|
+
the `android:windowBackground` and `colors.xml` snippet, and the outcome per Android WebView
|
|
2244
|
+
version). "Run it on a device" now says that `cap run ios` cannot deploy on Xcode 27 (no
|
|
2245
|
+
Simulator.app) and shows the `xcodebuild`/`simctl` fallback, and passes `--target` to both
|
|
2246
|
+
`cap run` commands. The labels guide lists `expand`, `collapse` and `sheetPosition`. The system-bars step states the WebView ≥ 140 preconditions (`viewport-fit=cover`, `insetsHandling: 'css'`).
|
|
2247
|
+
|
|
2248
|
+
New examples: `header/long-title` (a long title wrapping in a grown bar, the back text giving way)
|
|
2249
|
+
under the header's new "Large text and compact heights" section, and `list-item/long-text` (a
|
|
2250
|
+
wrapping row and a `truncate` row) under the row's new "Long text" section.
|
|
2251
|
+
|
|
2252
|
+
The shell's `main` is named "Documentation" (`aria-label`), so it stays distinct from the embedded
|
|
2253
|
+
phone demo's own `main` landmark (axe `landmark-unique` checks across frames).
|
|
2254
|
+
|
|
2255
|
+
New examples: `sheet/autofocus` (`autoFocus: 'first-field'`), `stack-outlet/bounded`
|
|
2256
|
+
(`maxLivePages`) and `stack-outlet/scroller` (a horizontal chips row in a stacked page, no
|
|
2257
|
+
directive).
|
|
2258
|
+
|
|
2259
|
+
Every install surface shows `bun add @bitforgehq/ui`: the home hero, Install step 4 and each
|
|
2260
|
+
component and reference page's Install row with the entry point's peers; the generated
|
|
2261
|
+
`ai/UI_GUIDE.md`, `llms-full.txt` and package README follow. The header shows the version,
|
|
2262
|
+
`v0.3.0`. The repository is `https://github.com/bitforge/BitforgeUI`, and the GitHub Pages
|
|
2263
|
+
mirror serves under `/BitforgeUI/`.
|
|
2264
|
+
|
|
2265
|
+
### agent docs
|
|
2266
|
+
|
|
2267
|
+
`UI_GUIDE.md` has a new API conventions section: the output, value-control, name and type rules,
|
|
2268
|
+
and the deprecated 0.2 output names with their replacements.
|
|
2269
|
+
|
|
2270
|
+
`UI_GUIDE.md`: Configuration covers when to set which density (desktop nothing, dense `sm`,
|
|
2271
|
+
Capacitor `fieldSize: 'lg'`), the hit-area rule and German labels; Forms covers
|
|
2272
|
+
`bf-number-input`, typed values (`T | null`, `compareWith`) and typed Reactive controls with
|
|
2273
|
+
default messages; a new Links item covers `a[bfButton]`, `bfAction` on cards and list rows, and
|
|
2274
|
+
the `nav` of links inside `bf-tabs`. `llms.txt` lists the two new guides. The package README gains
|
|
2275
|
+
a "Density and labels" paragraph.
|
|
2276
|
+
|
|
2277
|
+
**type**: `MANIFEST.json` entries gain `primaryExample` (an example id: `<name>/overview`, or
|
|
2278
|
+
`mobile-demo/showcase/<name>` for a `/mobile` component) and `examples` (`{ id, title, file, source,
|
|
2279
|
+
preview }[]`: the primary example, then those the `docs.md` references). Migration for agents: read
|
|
2280
|
+
`examples[0].source` for a complete, compiled usage file; `example` stays the short `@example`
|
|
2281
|
+
snippet. The `ai/components/*.md` pages replace "Quick example" with "Example", the primary example's
|
|
2282
|
+
full source as a `ts` block, and expand every `<!-- example: <id> -->` marker into a `ts` block of
|
|
2283
|
+
that file.
|
|
2284
|
+
|
|
2285
|
+
**type**: in `MANIFEST.json`, a usage, Theming, Accessibility or notes body now holds `<!-- example: <id> -->`
|
|
2286
|
+
markers where it held fenced `html`/`ts` snippets, and `examples` lists every example they place.
|
|
2287
|
+
Migration for agents: resolve a marker through the entry's `examples` (by `id`) for the full
|
|
2288
|
+
compiled file, or read the `ai/components/*.md` page, where every marker is already expanded.
|
|
2289
|
+
`bf-tab` and `bf-list-item` have their own `overview` and `usage`, no longer copies of `bf-tab-bar`
|
|
2290
|
+
and `bf-list`.
|
|
2291
|
+
|
|
2292
|
+
**type**: the API now comes from the TypeScript checker and the Angular template parser
|
|
2293
|
+
(`tools/api-extract.ts`), not from initializer text. `MANIFEST.json` gains `schemaVersion: 2`.
|
|
2294
|
+
Members gain `expandedType` (the union with its aliases resolved, e.g. `'sm' | 'md' | 'lg' |
|
|
2295
|
+
undefined` for `BfSize | undefined`, only when it differs from `type`), `twoWay` (a `model()`,
|
|
2296
|
+
bind it `[(name)]`), `formControlled` (a `FormUiControl` member that `[formField]` sets) and
|
|
2297
|
+
`alias` (the public binding name of an aliased member). `slots` are `{ name, doc }[]` (they were
|
|
2298
|
+
`string[]`), each named once (the header's two default projections are one slot) with its
|
|
2299
|
+
`@slot` doc. Components gain `methods` (`{ name, signature, params, returns, doc }[]`). Identifier
|
|
2300
|
+
defaults resolve to their literal (`bf-stack-outlet`'s `name` is `'primary'`, not
|
|
2301
|
+
`PRIMARY_OUTLET`), and `bf-number-input`'s `id` is typed `string` (was `any`). Migration for
|
|
2302
|
+
agents: read `slots[i].name` where you read `slots[i]`; read `expandedType ?? type` for the
|
|
2303
|
+
allowed values; never bind a `formControlled` member next to `[formField]`.
|
|
2304
|
+
|
|
2305
|
+
**type**: new `ai/REFERENCE.json` (every public symbol that is not a component: services,
|
|
2306
|
+
directives, classes, functions, tokens, constants, interfaces and types, with `signature`,
|
|
2307
|
+
`values`, `members`, `methods`, `selector`, `references`, `usedBy`, `alsoExportedFrom` and, for a
|
|
2308
|
+
directive with its own `docs.md`, `docs`), `ai/REFERENCE.index.json` (`{ name, slug, kind,
|
|
2309
|
+
entryPoint, summary }`) and one `ai/reference/<slug>.md` page per symbol (`BfSheetService` →
|
|
2310
|
+
`bf-sheet-service`, a constant keeps its underscores: `BF_LABELS` → `bf_labels`). The option
|
|
2311
|
+
interfaces carry the defaults the services apply (`@default`), and a table shows a property's
|
|
2312
|
+
default in place of "required" (`BfShellConfig.mode` reads `'auto'`). Migration for agents: look a
|
|
2313
|
+
service, directive, token or type up in `REFERENCE.index.json`, then read its
|
|
2314
|
+
`ai/reference/<slug>.md` page or its `REFERENCE.json` entry; a deprecated `/mobile` alias of an
|
|
2315
|
+
overlay symbol is listed on the overlay symbol under `alsoExportedFrom`.
|
|
2316
|
+
|
|
2317
|
+
**type**: every component line of `llms.txt` links its agent page first, then its site page:
|
|
2318
|
+
`- [bf-button](<origin>/ai/components/button.md): <summary> ([docs](<origin>/components/button))`,
|
|
2319
|
+
and a new `## Reference` section lists every reference page by entry point in the same form.
|
|
2320
|
+
`llms-full.txt` appends the reference pages after the component pages. Migration for agents:
|
|
2321
|
+
follow the first link of a line for the markdown page, the `docs` link for the rendered page.
|
|
2322
|
+
|
|
2323
|
+
The generator refuses to run on raw HTML outside code spans and fences (`<kbd>` excepted) in any
|
|
2324
|
+
JSDoc, `docs.md` or this CHANGELOG, naming the file and line (F-093, generator part): the site
|
|
2325
|
+
renders that markdown as HTML. The one offender, `bf-alert`'s `dismissLabel` doc, now says it in
|
|
2326
|
+
words. `tools/docs-contract.test.ts` requires an 8–15 word `@summary` and a `@since` on every
|
|
2327
|
+
exported symbol, a doc on every member, method, parameter and slot, and every export of every
|
|
2328
|
+
`public-api.ts` in `MANIFEST.json` or `REFERENCE.json`.
|
|
2329
|
+
|
|
2330
|
+
The reference covers `@bitforgehq/ui/testing` too: `REFERENCE.json`, `REFERENCE.index.json`,
|
|
2331
|
+
`ai/reference/` and the `## Reference` section of `llms.txt` gain its group, and the site's `/api`
|
|
2332
|
+
index lists it after `/mobile`.
|
|
2333
|
+
|
|
2334
|
+
**type**: `MANIFEST.json` entries, the site's `manifest/<name>.json` files and a directive's
|
|
2335
|
+
`REFERENCE.json` `docs` gain `whenToUse` and `desktop` (markdown, optional: the "When to use" and
|
|
2336
|
+
"Desktop" sections of its `docs.md`, which no longer appear in `usage`). The Keyboard table stays
|
|
2337
|
+
inside `accessibility` under `### Keyboard`. The `ai/components/*.md` pages follow the site's
|
|
2338
|
+
order (the example first, then a new "Overview" heading with the other selectors, the description
|
|
2339
|
+
and the overview, then When to use, the usage sections, Desktop, Theming, Accessibility, the API,
|
|
2340
|
+
Notes and Migration notes). A root-relative site link in a `docs.md` (`/components/menu`,
|
|
2341
|
+
`/api/bf-size`, `/density`) is made absolute (`https://bitforge-ui.web.app/components/menu`)
|
|
2342
|
+
everywhere the generator writes it: `MANIFEST.json`, `REFERENCE.json`, the site's
|
|
2343
|
+
`manifest/<name>.json` and the `ai/*.md` pages, so every link an agent reads can be followed. The
|
|
2344
|
+
site maps those links back onto its own base href. Migration for agents: read
|
|
2345
|
+
`whenToUse` to choose between widgets and `desktop` for desktop behaviour; a `usage` section named
|
|
2346
|
+
"Desktop" (the `/mobile` overlays) is now `desktop`.
|
|
2347
|
+
|
|
2348
|
+
`UI_GUIDE.md` gains a `## Testing` section and a step-by-step setup in `## Mobile shell`, both
|
|
2349
|
+
generated from the docs site's guide consts (`<!-- testing -->` and `<!-- mobile-setup -->` in
|
|
2350
|
+
`tools/ui-guide.template.md`), with each file shown in full. The hand-written mobile setup text
|
|
2351
|
+
(the `capacitor.config.ts` block, `index.html`, text size and Android manifest bullets) is replaced
|
|
2352
|
+
by those steps; the content is the same, plus the root host's `block h-dvh`, the providers, a
|
|
2353
|
+
tabbed app and running on a device. `llms.txt` lists the four new guides.
|
|
2354
|
+
|
|
2355
|
+
**type**: `llms.txt` follows the site navigation: its `## Guides` section becomes one section per group, in
|
|
2356
|
+
order (`## Get started`, `## Guides`, `## Components` with a `###` per component category under it,
|
|
2357
|
+
`## Foundations`, `## Reference` with the site's reference pages before the symbols by entry
|
|
2358
|
+
point, `## Changelog`), and it lists the new Tutorial, Concepts, Compatibility and Changelog
|
|
2359
|
+
pages. Migration for agents: find Install under `## Get started`, For agents under
|
|
2360
|
+
`## Reference` and Foundations under `## Foundations`, no longer under `## Guides`; a component
|
|
2361
|
+
category is now a `###` heading (`### Actions`), no longer a `##`. `bun run ai:docs` also writes the docs site's
|
|
2362
|
+
`consts/compat.ts` (`UI_PEERS`) from `package.json`.
|
|
2363
|
+
|
|
2364
|
+
### tooling
|
|
2365
|
+
|
|
2366
|
+
`bun run ai:docs` enforces one API naming convention (`PROJECT_GUIDE.md` "API conventions",
|
|
2367
|
+
F-016): outputs are past-tense events (no `on` prefix, no declared `xChange`), actions emit
|
|
2368
|
+
`clicked` (menu items keep `triggered`, the CDK's event, as a named exemption), every value control
|
|
2369
|
+
has exactly one `changed`, every component that renders a native input, select or textarea takes
|
|
2370
|
+
`label`, `ariaLabel` and `ariaLabelledby`, other names are `<part>Label` or (on an image) `alt`,
|
|
2371
|
+
and every union or object type of a public input, model or output, of a `Bf…Options`/`Bf…Config`
|
|
2372
|
+
property or of an interface they name is a named type exported from the entry (the written type,
|
|
2373
|
+
else the checker's, so a cast is caught). The pure check is `tools/api-conventions.ts`;
|
|
2374
|
+
`gen-ai-docs.ts` collects its facts from the extracted API, the checker and the template parser and
|
|
2375
|
+
refuses to run on a breach, listing `selector.member: rule`, and on an extraction with no component
|
|
2376
|
+
or output, a missing class or an unresolved signal type.
|
|
2377
|
+
|
|
2378
|
+
New `bun run typecheck` type-checks every spec (the `tsconfig.spec.json` of `ui`, `web-demo`,
|
|
2379
|
+
`mobile-demo` and `docs`), the e2e suite and `tools/` with `tsc`, because Jest compiles specs with
|
|
2380
|
+
`isolatedModules` and never type-checks them. CI (`verify`) and `release.yml` run it right after
|
|
2381
|
+
the library build. The errors it found are fixed without relaxing a compiler option: bracket access
|
|
2382
|
+
on `Keyframe` in `transitions.spec.ts`, the `v`-flag pattern built with `new RegExp(…, 'v')` in
|
|
2383
|
+
`input.component.spec.ts`, a throwing `defined()` narrowing helper in `derive-tokens.spec.ts`,
|
|
2384
|
+
typed fixtures for the generic `bf-select`, `bf-radio-group` and `bf-segmented` specs, pattern
|
|
2385
|
+
labels in the dialog and toast harness specs, the router's third `canMatch` argument in two docs
|
|
2386
|
+
guard specs, and `EnvironmentProviders` accepted by the docs `renderExample()` helper.
|
|
2387
|
+
|
|
2388
|
+
`stack-outlet.component.spec.ts` waits for no wall-clock time: the deferred transition start and
|
|
2389
|
+
the swipe's 600 ms back fallback run on Jest's fake clock (`onFakeClock()`), and the one remaining
|
|
2390
|
+
real `setTimeout` is a commented 0 ms macrotask yield for the fake history's popstate order. The
|
|
2391
|
+
web and mobile demo Jest projects fail below 60 % branch and 70 % function, line and statement
|
|
2392
|
+
coverage (they were at 0). `tools/release-tag.test.ts` pins that any pre-release suffix
|
|
2393
|
+
(`v1.2.3-rc.1`) publishes to `next` and that build metadata (`v1.2.3+build.5`) is rejected with the
|
|
2394
|
+
shape message; the `TAG` comment and the README's "Releasing" state both rules.
|
|
2395
|
+
|
|
2396
|
+
New `tools/effect-usage.test.ts`: fails on an `effect()` in `src`, `mobile` or `overlay` that
|
|
2397
|
+
writes DOM or another object's state, unless the line carries `// effect-justified: <reason>`
|
|
2398
|
+
(three allowed: the shell's `<html>` class and the menu directives' writes into their own CDK
|
|
2399
|
+
host directives). It also flags property assignments on anything but `this`, lifecycle hooks and
|
|
2400
|
+
method calls with arguments on injected or queried objects. New `tools/ssr-smoke.ts` and `tools/ssr-smoke.test.ts`
|
|
2401
|
+
(in `test:tools`, which runs the smoke in a child process): renders a fixture per root widget,
|
|
2402
|
+
`/icons`, `/number-input`, `/mobile` and `/overlay` with `renderApplication()`, checks output each
|
|
2403
|
+
widget's own template renders (the native control, its role and state, the dialog panel), and
|
|
2404
|
+
compares which render and which are stable across two renders with the recorded `SSR_RECORDED`.
|
|
2405
|
+
|
|
2406
|
+
The API convention treats `valueText` (a progress bar's `aria-valuetext`) as content, like
|
|
2407
|
+
`helpText`, not as a name input. The server-render smoke renders a `bf-chip-group` fixture
|
|
2408
|
+
(28 fixtures, 19 of them stable) and matches the native `<progress>` without its dropped role. `tools/measure-examples.ts`
|
|
2409
|
+
honours the `--port <n>` its usage line documents (it always used 4408).
|
|
2410
|
+
|
|
2411
|
+
The mobile demo's `initial` budget warning (`angular.json`, the `mobile-demo` production and
|
|
2412
|
+
`embedded` configurations) is 630 kB, up from 610 kB. Its initial total grew from 606.72 kB at the
|
|
2413
|
+
start of this release's mobile-and-polish work to 622.10 kB (main 488.49 kB, styles 125.23 kB):
|
|
2414
|
+
+10.3 kB came with the pressed states, contextual badge and progress text, help next to errors and
|
|
2415
|
+
the bounded textarea (`@bitforgehq/ui` JS +7.5 kB, CSS +4.0 kB, because `theme.css` generates the
|
|
2416
|
+
classes of every entry), the rest from the API naming aliases, the signal refactors and the icon
|
|
2417
|
+
`fill` bindings. The docs app stays at 570.40 kB of its 580 kB.
|
|
2418
|
+
|
|
2419
|
+
It is now 650 kB, up from 630 kB: the initial total grew from 622.10 kB to 636.44 kB (main
|
|
2420
|
+
488.5 → 499.8 kB, styles 125.2 → 128.2 kB) with the mobile features of the same release: +5.7 kB
|
|
2421
|
+
for the sheet snap control, its Close button and announcements and the system bars, +4.8 kB for
|
|
2422
|
+
the measured header and the px tab bar, and +3.8 kB for the CloseWatcher Escape path, the bounded
|
|
2423
|
+
stacks and the `main` landmark. None of it is an accidental inclusion: every demo page is lazy,
|
|
2424
|
+
and the library code in `main` is the one `@bitforgehq/ui`, `/mobile` and `/overlay` module each
|
|
2425
|
+
that esbuild keeps whole in the initial chunk. The docs app stays at 573.38 kB of its 580 kB.
|
|
2426
|
+
|
|
2427
|
+
Every `@deprecated` tag reads `<version> use <X>; removed in <version>`: the overlay core aliases of
|
|
2428
|
+
`/mobile` name their `@bitforgehq/ui/overlay` replacement (`use BfOverlayService from
|
|
2429
|
+
'@bitforgehq/ui/overlay'`), and the size aliases (`BfFieldSize`, `BfButtonSize`,
|
|
2430
|
+
`BfSegmentedSize`) and the card's `interactive`/`clicked` state the 1.0.0 removal Migrations 5 and
|
|
2431
|
+
12 promise. `tools/api-conventions.test.ts` checks the form over every non-spec `.ts` file of the
|
|
2432
|
+
library.
|
|
2433
|
+
|
|
2434
|
+
New `tools/no-inline-svg.test.ts` (F-066): no `<svg` in the non-spec `.ts` and `.html` files of
|
|
2435
|
+
`projects/ui/{src,mobile,overlay,number-input,testing}`, the docs site (chrome, pages and
|
|
2436
|
+
examples), both demos and the OG image template; `projects/ui/icons` (`bf-icon`) is the only SVG
|
|
2437
|
+
renderer, with no exception. New `tools/lucide-attributes.test.ts` (F-065): collects every
|
|
2438
|
+
attribute each element type carries across all `lucide` icons and fails on one the `@case` blocks
|
|
2439
|
+
of `icon.component.ts` do not bind. The server-render smoke matches the spinner's ring instead of
|
|
2440
|
+
its SVG.
|
|
2441
|
+
|
|
2442
|
+
`tools/effect-usage.test.ts` allow-lists `BfPlatformService`'s `SystemBars.setStyle()` call (the
|
|
2443
|
+
native bars are external to any view). `tools/guides.test.ts` checks that the mobile demo's
|
|
2444
|
+
Android window background matches the guide's snippet and the theme's `bg` tokens.
|
|
2445
|
+
`tools/docs-contract.test.ts` now derives `BfSheetComponent` as an interactive widget (its grabber
|
|
2446
|
+
button and hidden Close) instead of listing it by hand.
|
|
2447
|
+
|
|
2448
|
+
New `e2e/large-text.spec.ts` (iphone and pixel): on Today, Projects, a pushed task and Me, at root
|
|
2449
|
+
font sizes 100 %, 200 % and 235 % and at a simulated Android font scale 2.0 (every `--text-*` token
|
|
2450
|
+
doubled, lengths unchanged, as Android's text zoom does), no header slot leaves the viewport or the
|
|
2451
|
+
bar, every tab label shows its text within two lines inside the bar, `--bf-tab-bar-inset-global`
|
|
2452
|
+
equals the bar's measured height plus its offsets (±1 px), and no list title is cut with an
|
|
2453
|
+
ellipsis unless its row sets `truncate`; in landscape (844×390, 915×412) Today and Projects render
|
|
2454
|
+
no large title and leave at least 60 % of the height to content. `e2e/header.spec.ts` now expects
|
|
2455
|
+
the full collapse, and the scroll-under state, at the large title's rendered height (the title
|
|
2456
|
+
still showing at 56 px).
|
|
2457
|
+
|
|
2458
|
+
### packaging
|
|
2459
|
+
|
|
2460
|
+
`theme.css` registers `@source './fesm2022/*.mjs'` instead of the whole `fesm2022` folder, so
|
|
2461
|
+
Tailwind scans the FESM code and skips the source maps beside it; the maps stay in the package for
|
|
2462
|
+
debugging. The consumer smoke's `widgets` build checks that its stylesheet still has
|
|
2463
|
+
`.bg-primary-soft`, a class only library code uses, and `tools/theme-source.test.ts` pins the
|
|
2464
|
+
glob.
|
|
2465
|
+
|
|
2466
|
+
A pre-release heading without entries of its own (`## X.Y.Z-next.N`) is followed by one line
|
|
2467
|
+
naming the section that applies, `Pre-release of X.Y.Z: its changes are listed under "X.Y.Z
|
|
2468
|
+
(unreleased)" above.`, so a `next` installer reading the shipped CHANGELOG finds the notes.
|
|
2469
|
+
`tools/changelog.test.ts` checks that line, that the top heading is `(unreleased)` or dated with
|
|
2470
|
+
the `projects/ui/package.json` version, and that `### Migration` opens the top version and every
|
|
2471
|
+
unreleased version with breaks.
|
|
2472
|
+
|
|
2473
|
+
First npm release: `@bitforgehq/ui` 0.3.0 is published to the `latest` dist-tag
|
|
2474
|
+
(`bun add @bitforgehq/ui`). `package.json`'s `repository.url` points at the new home,
|
|
2475
|
+
`git+https://github.com/bitforge/BitforgeUI.git` (private). The never-published `0.3.0-next.1` and
|
|
2476
|
+
`0.2.0-next.1` headings are folded into their versions, and the 0.2.0 and 0.1.0 headings read
|
|
2477
|
+
`(not published)`. `tools/release-tag.test.ts` pins that `v0.3.0` resolves to `latest`. The
|
|
2478
|
+
release checklist and README "Releasing" describe the 0.3.0 publish from `bitforge/BitforgeUI`
|
|
2479
|
+
(the `NPM_TOKEN` secret and `npm` environment, the `FIREBASE_SERVICE_ACCOUNT_BITFORGE_UI` secret,
|
|
2480
|
+
the `v0.3.0` tag, `latest` without provenance).
|
|
2481
|
+
|
|
2482
|
+
## 0.2.0 (not published)
|
|
2483
|
+
|
|
2484
|
+
### overlay core
|
|
2485
|
+
|
|
2486
|
+
New overlay foundation of `@bitforgehq/ui/mobile` on the Angular CDK `Overlay` (no CDK dialog or
|
|
2487
|
+
bottom-sheet). `BfOverlayService.open(componentOrTemplate, options)` attaches the content in a
|
|
2488
|
+
global-positioned panel (`position`: `center` default, `bottom`, `top`, `fullscreen`; class
|
|
2489
|
+
`bf-overlay-panel` plus `panelClass`; scroll strategy as in the "Fixed" note below) over a `bf-overlay-backdrop` backdrop
|
|
2490
|
+
(a black scrim, `rgb(0 0 0 / 0.4)`, `0.6` under `.dark`, a new rule in `theme.css`; it fades out
|
|
2491
|
+
with the panel's exit, instantly under reduced motion); the panel gets `role` (default `dialog`,
|
|
2492
|
+
none for passive overlays), `aria-modal="true"`, `aria-label`/`aria-labelledby`/`aria-describedby`
|
|
2493
|
+
and `tabindex="-1"`, a CDK focus trap
|
|
2494
|
+
(`trapFocus`), focus on its first tabbable element (else the panel) and focus back to the
|
|
2495
|
+
previously focused element on close (`restoreFocus`). `Escape` (`dismissOnEscape`), a backdrop
|
|
2496
|
+
click (`dismissOnBackdrop`) and the hardware back button (priority `backPriority`, default 100,
|
|
2497
|
+
newest overlay first) close it. Only the topmost non-passive overlay (`BfOverlayStack.isTop()`)
|
|
2498
|
+
answers `Escape`, and it always claims the key: one that does not close on it (`dismissOnEscape:
|
|
2499
|
+
false`) swallows it, or runs `onEscape` instead, so it never reaches an overlay underneath. The content injects `BF_OVERLAY_DATA` and `BfOverlayRef`
|
|
2500
|
+
(`close(result?)`, `afterClosed: Promise<T | undefined>`, `closed` signal, `id`,
|
|
2501
|
+
`updatePosition()`); components get a `BfComponentOverlayRef` with `componentRef`; a template
|
|
2502
|
+
receives `{ $implicit: ref, data }`. `enter`/`exit` take WAAPI animations (default `fadeIn`/`fadeOut`;
|
|
2503
|
+
`close()` awaits the exit before detaching); `slideUp`, `slideDown`, `fadeIn`, `fadeOut` and `scaleIn`
|
|
2504
|
+
run 260 ms `cubic-bezier(0.32, 0.72, 0, 1)` and fall back to a 150 ms fade under
|
|
2505
|
+
`prefers-reduced-motion`. A `passive` overlay (toasts) has no backdrop, focus handling or back
|
|
2506
|
+
handler and leaves the page interactive. `BfOverlayStack` tracks the modal-like overlays (`count`,
|
|
2507
|
+
`depth` for `countsAsModal`) and, while any is open, sets `inert` and `--bf-modal-depth` on every
|
|
2508
|
+
registered page root; the outermost `bf-stack-outlet` registers its host (`registerRoot`) on init
|
|
2509
|
+
and unregisters on destroy. `provideBfShell()` provides both services.
|
|
2510
|
+
|
|
2511
|
+
Fixed: outside the shell (no `BF_SHELL_CONFIG`, e.g. a desktop page), a non-passive overlay now
|
|
2512
|
+
uses the CDK `block()` scroll strategy, so the document no longer scrolls behind an open dialog,
|
|
2513
|
+
sheet or modal (the "scroll bleed" at 1280 px). Passive overlays (toasts) and every overlay inside
|
|
2514
|
+
the shell, whose document never scrolls, keep the `noop()` strategy.
|
|
2515
|
+
|
|
2516
|
+
### sheet
|
|
2517
|
+
|
|
2518
|
+
New `BfSheetService.open(componentOrTemplate, { snapPoints, initial, dismissible, data, ariaLabel })`
|
|
2519
|
+
in `@bitforgehq/ui/mobile`, a bottom sheet on the overlay core (`position: 'bottom'`, back priority
|
|
2520
|
+
100, not counted as a modal; slides up on open and down on close). It returns a `BfSheetRef`
|
|
2521
|
+
(extends `BfOverlayRef`) with `snapTo(index)` and a `snapIndex` signal; the content injects the
|
|
2522
|
+
caller's `data` as `BF_OVERLAY_DATA` and the ref as `BfSheetRef` and `BfOverlayRef` (a template
|
|
2523
|
+
gets `{ $implicit: ref, data }`). The `bf-sheet` container is a `bg-surface rounded-t-card
|
|
2524
|
+
shadow-floating` card with `pb-safe-bottom`, a 36 × 5 px `bg-control-off` grabber (`aria-hidden`,
|
|
2525
|
+
`ButtonText` under forced colors)
|
|
2526
|
+
in a 24 px `touch-none` handle row and a content region; its height is `--bf-sheet-height`, a snap
|
|
2527
|
+
point (fraction of `BfKeyboardService.restHeight()`, else `innerHeight`; default `[0.5, 0.92]`)
|
|
2528
|
+
re-applied on resize. Dragging the handle row, any `[data-bf-sheet-drag]` element or the content
|
|
2529
|
+
(below the top point, or downward at the top point while scrolled to 0) moves the sheet with
|
|
2530
|
+
Pointer Events under pointer capture and a non-passive `touchmove` guard: rubber-banded (30 %)
|
|
2531
|
+
above the top point, released onto the nearest point or, faster than 0.3 px/ms, the next one in
|
|
2532
|
+
the fling's direction; a release more than 25 % of the lowest point's height below it, or a
|
|
2533
|
+
downward fling from it, dismisses. The content scrolls (`overflow-y-auto`) only at the top point,
|
|
2534
|
+
`overflow-hidden` below it, always `overscroll-contain`. `dismissible: false` ignores the backdrop,
|
|
2535
|
+
`Escape` and downward dismissal and swallows the hardware back button while open. Settling is a
|
|
2536
|
+
260 ms WAAPI animation, instant under `prefers-reduced-motion` (the drag still tracks the
|
|
2537
|
+
pointer). The panel is `role="dialog"` `aria-modal` with `aria-label` (default "Sheet"), focus
|
|
2538
|
+
moves in and returns on close.
|
|
2539
|
+
|
|
2540
|
+
Fixed: the handle row shows a `grab` cursor (`grabbing` while pressed) and the grabber darkens to
|
|
2541
|
+
`text-muted` on hover.
|
|
2542
|
+
|
|
2543
|
+
Fixed (behaviour change): the soft keyboard no longer covers a sheet's focused field and actions.
|
|
2544
|
+
While the keyboard overlays the sheet's bottom edge (iOS with `Keyboard.resize: 'none'`, a mobile
|
|
2545
|
+
browser), `bf-sheet` is lifted by the covered height (an inline `translate`, bottom safe-area
|
|
2546
|
+
padding dropped, since the keyboard covers the home indicator); while the keyboard is up at all
|
|
2547
|
+
(also Android, where the WebView resizes and nothing is lifted) its height is capped at the
|
|
2548
|
+
keyboard's top edge minus the safe-area top minus 8 px (`max-height`), and the content region
|
|
2549
|
+
scrolls at every snap point, with `bfKeyboardAssist` revealing the focused control 24 px above the
|
|
2550
|
+
keyboard. The lift follows the keyboard with a 250 ms `translate` transition on the iOS curve
|
|
2551
|
+
(instant under `prefers-reduced-motion`), and is left alone once the sheet is closing. The snap
|
|
2552
|
+
heights (`--bf-sheet-height`) are unchanged, and a drag of a capped sheet starts from its rendered
|
|
2553
|
+
(capped) height, so it moves from the first pixel. Hiding the keyboard removes the lift and the
|
|
2554
|
+
cap together and at once (the drop is not animated, so a sheet at a snap point taller than the cap
|
|
2555
|
+
never has its top edge above the screen), so the sheet rests at its snap height again, and content
|
|
2556
|
+
left scrolled while the keyboard was up returns to its top when the keyboard hides below the top
|
|
2557
|
+
snap point (a drag between snap points without the keyboard keeps the scroll offset, as before).
|
|
2558
|
+
|
|
2559
|
+
### modal
|
|
2560
|
+
|
|
2561
|
+
New `BfModalService.open(component, { data, presentation, dismissible, ariaLabel })` in
|
|
2562
|
+
`@bitforgehq/ui/mobile`, a page-level modal on the overlay core (`position: 'fullscreen'`, back
|
|
2563
|
+
priority 100, no backdrop dismiss). `presentation: 'sheet'` (default) renders `bf-modal` as a card
|
|
2564
|
+
inset 12 px from the top (`bg-bg rounded-t-card shadow-floating`) and, in iOS mode only, counts as
|
|
2565
|
+
a modal, driving `--bf-modal-depth` on every registered `bf-stack-outlet` root (see
|
|
2566
|
+
`### stack-outlet`) so the page behind scales down (Material mode leaves it unscaled);
|
|
2567
|
+
`'fullscreen'` renders it edge to edge (`bg-bg`, no radius or shadow) and does not count towards
|
|
2568
|
+
that depth. iOS mode slides the frame up on enter and down on exit; Material mode fades it up 16 px
|
|
2569
|
+
on enter and down on exit (new `fadeUp`/`fadeDown` helpers in `overlay-animations.ts`). It returns
|
|
2570
|
+
the `BfOverlayRef<T>` the content also injects (with the caller's `data` as `BF_OVERLAY_DATA`) and
|
|
2571
|
+
closes with a result through; unlike `bf-sheet` there is no drag-to-dismiss, so the content needs
|
|
2572
|
+
its own close control. `dismissible: false` (default true) ignores `Escape` and swallows the
|
|
2573
|
+
hardware back button while open instead of closing or falling through to the page behind. Like
|
|
2574
|
+
`bf-page`, `bf-modal` provides a fresh `BfPageContext` so the content's own `bf-header`
|
|
2575
|
+
(`largeTitle="false"`, typically a close `bf-icon-button` in `start`) and `bf-content` work
|
|
2576
|
+
inside it. `provideBfShell()` now also provides `BfSheetService` and `BfModalService`.
|
|
2577
|
+
|
|
2578
|
+
Fixed (behaviour change): a `presentation: 'sheet'` modal ends above the soft keyboard. While the
|
|
2579
|
+
keyboard overlays the card's bottom edge (iOS, a mobile browser), `bf-modal` gets an inline
|
|
2580
|
+
`padding-bottom` of the covered height, so the content's `bf-content` ends at the keyboard's top
|
|
2581
|
+
edge and its keyboard assist reveals the focused control there; it goes away with the keyboard. A
|
|
2582
|
+
`fullscreen` modal, and Android, where the WebView resizes, are unchanged.
|
|
2583
|
+
|
|
2584
|
+
### toast
|
|
2585
|
+
|
|
2586
|
+
New `BfToastService.show({ message, action?, duration, position })` in `@bitforgehq/ui/mobile`, a
|
|
2587
|
+
queued status pill on the overlay core opened `passive: true` (no backdrop, no focus trap, the page
|
|
2588
|
+
stays interactive and it never counts towards `BfOverlayStack.count()`). Only one toast shows at a
|
|
2589
|
+
time; later `show()` calls queue in order behind a timed toast and appear once it ends (a persistent
|
|
2590
|
+
one gives way, see the behaviour changes below). It returns a `BfToastRef` (`dismiss()`,
|
|
2591
|
+
`afterDismissed: Promise<'timeout' | 'action' | 'dismiss'>`) — the toast resolves `'timeout'` after
|
|
2592
|
+
`duration` ms (`0` keeps it up until `dismiss()` or its action; the default is 3000 without an
|
|
2593
|
+
action and 0 with one, see below), `'action'` when the optional action button is clicked (its
|
|
2594
|
+
`handler` runs first), and `'dismiss'` from `dismiss()`, whether the toast is showing or still
|
|
2595
|
+
queued. `bf-toast` is an ink `bg-text text-bg rounded-full shadow-floating` pill (message
|
|
2596
|
+
`line-clamp-2`, announced through `LiveAnnouncer`, see the "Changed" note below); the action renders
|
|
2597
|
+
as a plain underlined `text-bg` button rather than `bf-button`'s `text` variant, which has no
|
|
2598
|
+
contrast on the ink surface. `position: 'bottom'` (default) keeps 16 px clear of the floating tab
|
|
2599
|
+
bar (`--bf-tab-bar-inset-global` on `<html>`, see `### tabs`) and the soft keyboard, written live as
|
|
2600
|
+
`--bf-toast-keyboard`: `BfKeyboardService.height()` where the keyboard overlays the WebView (native
|
|
2601
|
+
iOS), `0` where the WebView already resizes (Android, the web); `position: 'top'` sits 16 px below
|
|
2602
|
+
the status bar instead, as a top margin (`mt-[calc(var(--spacing-safe-top)+var(--spacing)*4)]`)
|
|
2603
|
+
outside the pill: an earlier `pt-safe-top` on the host overrode the pill's own `py-3` (0 top padding
|
|
2604
|
+
on the web, about 50 px on a notched iPhone). Enter fades in with a 12 px slide from its anchored
|
|
2605
|
+
edge, exit is a plain fade; both collapse to a fade-only under `prefers-reduced-motion`.
|
|
2606
|
+
`provideBfShell()` now also provides `BfToastService`. The action button has a 44 px hit area
|
|
2607
|
+
(`min-h-hit px-3`) that reaches into the pill's padding (`-my-2.5 -mx-3`), so the pill keeps its 48
|
|
2608
|
+
px height and width.
|
|
2609
|
+
|
|
2610
|
+
Fixed: the action button shows a pointer cursor and fades to 80 % opacity on hover.
|
|
2611
|
+
|
|
2612
|
+
Changed: with the new tab-bar inset (see `### tabs`, which now includes 8 px above the bar) a
|
|
2613
|
+
bottom toast sits 16 px above the inset and so clears the floating tab bar by 24 px (was 16 px).
|
|
2614
|
+
|
|
2615
|
+
Behaviour change: a toast with an `action` no longer auto-dismisses by default (`duration`
|
|
2616
|
+
defaults to `0` with an action, still `3000` without one; an explicit `duration` wins), so an
|
|
2617
|
+
"Undo" stays reachable for keyboard, switch and screen-reader users (WCAG 2.2.1). Every toast's
|
|
2618
|
+
timer pauses while the pointer is over it or focus is inside it and resumes with the remaining
|
|
2619
|
+
time. When a toast that holds focus closes (action or `dismiss()`), focus returns with
|
|
2620
|
+
`preventScroll` to the element it came from (the first `focusin`'s `relatedTarget`, else the
|
|
2621
|
+
element focused when it opened), if still in the page; before, it fell to `<body>`.
|
|
2622
|
+
|
|
2623
|
+
Changed: toasts are announced through CDK `LiveAnnouncer` (`announce(text, 'polite')`, once per
|
|
2624
|
+
toast as it shows; the text is the message plus the action label). The `bf-toast` host no longer
|
|
2625
|
+
carries `role="status"`/`aria-live="polite"`: a live region inserted together with its text was
|
|
2626
|
+
not reliably announced. Tests locating the pill by `role="status"` should use `bf-toast`.
|
|
2627
|
+
|
|
2628
|
+
Behaviour change: persistent toasts (`duration` 0, the default with an action) never block the
|
|
2629
|
+
queue — the next `show()` dismisses one, visible or still queued, resolving `'dismiss'` (the
|
|
2630
|
+
Material snackbar model; a timed toast still queues the next one). `Escape` while focus is inside a
|
|
2631
|
+
toast dismisses it (`'dismiss'`) and is claimed there. Focus returns only while the toast still
|
|
2632
|
+
holds focus at close time, to the element focused last outside it; once focus has fallen to the
|
|
2633
|
+
page body it is not moved to the element that was focused when the toast opened. The service ends
|
|
2634
|
+
every toast (timers and document listeners included) when its injector is destroyed, and the
|
|
2635
|
+
announcement joins a message ending in `:`, `;` or `,` to the action label with a space.
|
|
2636
|
+
|
|
2637
|
+
### mobile entry
|
|
2638
|
+
|
|
2639
|
+
New secondary entry point `@bitforgehq/ui/mobile`, the Capacitor app shell for phase 2. Its first
|
|
2640
|
+
export is `BfPlatformService` (`providedIn: 'root'`): `isNative` and `platform` (`'ios' | 'android' | 'web'`)
|
|
2641
|
+
read once from `Capacitor.isNativePlatform()`/`Capacitor.getPlatform()`; `mode` (`'ios' | 'md'`)
|
|
2642
|
+
derives the shell mode from the platform; `theme` is a writable `signal<'system' | 'light' | 'dark'>`
|
|
2643
|
+
(default `'system'`) persisted to `localStorage` under `bf-theme`; `dark` follows the OS `prefers-color-scheme` while
|
|
2644
|
+
`theme` is `'system'` and otherwise reflects the explicit choice, applied as the `dark` class on
|
|
2645
|
+
`<html>` in an effect. `@angular/cdk`, `@angular/router` and `@capacitor/core` are optional peers
|
|
2646
|
+
of the package that an app importing `@bitforgehq/ui/mobile` must install (a web-only consumer that
|
|
2647
|
+
never imports this entry does not need them); `lucide` is a required peer of the package, since
|
|
2648
|
+
`bf-back-button` renders a Lucide icon (see `### packaging`). `@capacitor/app`,
|
|
2649
|
+
`@capacitor/keyboard` and `@capacitor/haptics` are optional peers for apps that use those
|
|
2650
|
+
plugins. Widgets in `@bitforgehq/ui`/`@bitforgehq/ui/icons` still may not import from
|
|
2651
|
+
`mobile/` (lint-enforced); the mobile entry may import widgets.
|
|
2652
|
+
|
|
2653
|
+
Added: `resolveShellMode(config, platform)` and `injectShellMode()` exports: the shell mode in
|
|
2654
|
+
effect (`config.mode`, or `BfPlatformService.mode()` for `'auto'`), the resolution the stack outlet,
|
|
2655
|
+
the modal service, `bf-header` and `bf-back-button` share, so apps can follow the same per-mode
|
|
2656
|
+
conventions (e.g. text vs icon header actions).
|
|
2657
|
+
|
|
2658
|
+
### stack-outlet
|
|
2659
|
+
|
|
2660
|
+
New `bf-stack-outlet`: a router outlet that keeps a stack of live pages instead of destroying the
|
|
2661
|
+
one you leave, implementing `RouterOutletContract` in place of `<router-outlet>`. A push slides
|
|
2662
|
+
the new page in and parks the previous one (`visibility: hidden`, `inert`, `aria-hidden`, detached
|
|
2663
|
+
change detector, scroll and state kept); a pop slides the top page out, destroys it and restores
|
|
2664
|
+
the page below with focus returned (`preventScroll`) to whatever had it. Push vs. pop is
|
|
2665
|
+
classified from the router's navigation trigger and the history positions the router records
|
|
2666
|
+
(`computed` resolution), so `replaceUrl`, redirects, refused guards, `history.go(-n)` and browser
|
|
2667
|
+
forward resolve correctly. A pop always goes back to the history entry before the page was pushed,
|
|
2668
|
+
in one step, even when the page added entries of its own (query-only or nested child
|
|
2669
|
+
navigations); a swipe whose history step does not pop the outlet slides the page back. Transitions follow `BfShellConfig.mode`: iOS is a 500 ms
|
|
2670
|
+
slide-and-parallax under a dimming layer with an edge-swipe-back (50 px left edge, Pointer Events,
|
|
2671
|
+
scrubs the pop's `currentTime`, completes on velocity or half-width, `Location.back()` on
|
|
2672
|
+
completion); Material is a 220 ms fade-up with no gesture; `prefers-reduced-motion` reduces both to
|
|
2673
|
+
a 150 ms cross-fade. `stackChange`, `transitionStart` and `transitionEnd` outputs report depth and
|
|
2674
|
+
transition state. `BfNavController` (root, `providedIn: 'root'`) exposes `forward()`, `back()`,
|
|
2675
|
+
`root()`, `canGoBack` and `depth` (see `### nav controller`; `back()` pops the top page in one
|
|
2676
|
+
history step); `withBfShellRouting()` sets
|
|
2677
|
+
`canceledNavigationResolution: 'computed'` so a refused back navigation stays in sync with the
|
|
2678
|
+
outlet. The outlet binds route params, query params and route data to its pages' inputs itself
|
|
2679
|
+
(`bindInputs`), so a parked page's `ActivatedRoute` staying stale after a pop is never a problem.
|
|
2680
|
+
Every primary outlet registers with `BfNavController` on init (`registerOutlet(outlet, level)`,
|
|
2681
|
+
`level` = number of stack outlets around it) and unregisters on destroy; it no longer registers
|
|
2682
|
+
a hardware back handler of its own.
|
|
2683
|
+
|
|
2684
|
+
Multi-stack: inside `bf-tabs` (optional `BF_TAB_CONTEXT`) the outlet keeps one stack per tab key
|
|
2685
|
+
(`Map<tabKey, StackEntry[]>`). A navigation onto another tab key is a `switch`: the whole current
|
|
2686
|
+
stack is parked (every page hidden, `inert`, change detection detached), and the target stack is
|
|
2687
|
+
restored — its page for the URL re-activated (attached, visible, focus to its title) with any
|
|
2688
|
+
pages above it dropped — or started with a new page; no transition and no gesture. A `root`
|
|
2689
|
+
navigation into another tab replaces only that tab's stack. `depth`/`canGoBack` reflect the active
|
|
2690
|
+
stack, pops stay within it, and `stackChange` now emits `{ depth, tabKey }` (`tabKey` null outside
|
|
2691
|
+
`bf-tabs`). New public methods `lastUrlOf(tabKey)` (URL of a tab's top page), `popToRoot()` (goes back in
|
|
2692
|
+
history to the active stack's first page in one step; false when its position is unknown) and
|
|
2693
|
+
`scrollActiveContentToTop(behavior?)` (through the active page's `BfPageContext` scroller, which
|
|
2694
|
+
the outermost `bf-page` now registers with the outlet).
|
|
2695
|
+
|
|
2696
|
+
Nested outlets: an outlet inside a page of another outlet (the outlet in `bf-tabs` on a tabs page
|
|
2697
|
+
hosted by the root outlet) keeps its stacks while the outer outlet parks that page. Each page gets
|
|
2698
|
+
a per-page host token that tells the outlets inside it whether the page is shown; when the router
|
|
2699
|
+
deactivates the inner outlet because the outer one pushed a page above its host (e.g.
|
|
2700
|
+
`/tabs/today/task/1` → `/gallery`), the inner outlet suspends instead of destroying its stacks,
|
|
2701
|
+
and when the outer outlet pops back it resumes the same page (same DOM nodes, every tab stack
|
|
2702
|
+
intact) without a transition. A history step that lands on another page of the inner stack pops
|
|
2703
|
+
within it as usual. The page shown by the inner outlet follows its host page's lifecycle: it gets
|
|
2704
|
+
`willLeave`/`didLeave` when the host is parked and `willEnter`/`didEnter` when it comes back
|
|
2705
|
+
(forwarded only on a real state change, so no duplicate events). An edge swipe is claimed by the
|
|
2706
|
+
innermost outlet that can pop — also while that outlet is mid-transition, where the swipe is
|
|
2707
|
+
ignored — so nested outlets never pop together.
|
|
2708
|
+
|
|
2709
|
+
Modal depth: the outlet's host now carries a scale rule keyed to `--bf-modal-depth` (written by
|
|
2710
|
+
`BfOverlayStack` on every registered outlet root while a page-sheet `bf-modal` is open — see
|
|
2711
|
+
`### modal`): `transform: scale(calc(1 - var(--bf-modal-depth, 0) * 0.08))
|
|
2712
|
+
translateY(calc(var(--bf-modal-depth, 0) * 8px))`, `border-radius: calc(var(--bf-modal-depth, 0) *
|
|
2713
|
+
12px)`, a 260 ms transition on both (skipped under `prefers-reduced-motion`), `transform-origin:
|
|
2714
|
+
top center`. At depth 0 the rule is a no-op; each stacked modal scales the page stack down another
|
|
2715
|
+
8 % and rounds its corners another 12 px.
|
|
2716
|
+
|
|
2717
|
+
### nav controller
|
|
2718
|
+
|
|
2719
|
+
`BfNavController` keeps a registry of the primary stack outlets and their nesting level; `depth`
|
|
2720
|
+
and `canGoBack` follow the **deepest active outlet** (the innermost one whose page is shown: the
|
|
2721
|
+
tab stack inside `bf-tabs`, or the root outlet once it pushed a page above the tabs page), and
|
|
2722
|
+
`back()` pops the deepest active outlet that has a page to pop, with that outlet's one-step
|
|
2723
|
+
history logic. It registers the priority-0 hardware back handler once (instead of one per
|
|
2724
|
+
outlet): it pops while an active outlet has more than one page and lets `BfBackButtonService`
|
|
2725
|
+
exit the app only when every active outlet is at depth ≤ 1 (a parked outlet cannot be popped, so
|
|
2726
|
+
it never blocks the exit). `setDepth()` and `registerPopTarget()` are replaced by
|
|
2727
|
+
`registerOutlet(outlet, level)` (called by `bf-stack-outlet`). Fixes a page pushed on the root
|
|
2728
|
+
outlet above the tabs page reporting depth 0 (hardware back exited the app) and destroying every
|
|
2729
|
+
tab stack, and `nav.root()` from inside the tabs leaving the root outlet without a pop target.
|
|
2730
|
+
|
|
2731
|
+
### page
|
|
2732
|
+
|
|
2733
|
+
New `bf-page`: the page frame (`absolute inset-0 flex flex-col bg-bg`) that `bf-stack-outlet`
|
|
2734
|
+
positions one of per stacked page. Injects the per-page `BfViewLifecycle` the outlet provides
|
|
2735
|
+
(falling back to a local, inert instance when used standalone, e.g. in a docs demo) and re-exposes
|
|
2736
|
+
it as `viewState` (`computed`) plus outputs `bfViewWillEnter`, `bfViewDidEnter`, `bfViewWillLeave`,
|
|
2737
|
+
`bfViewDidLeave`, delivered synchronously through `BfViewLifecycle.on()`: a new page gets
|
|
2738
|
+
`bfViewWillEnter` and `bfViewDidEnter` (also the first page, without animation and after
|
|
2739
|
+
`nav.root()`), and a popped or root-replaced page gets `bfViewWillLeave` and `bfViewDidLeave` before
|
|
2740
|
+
it is destroyed. Carries no `tabindex` itself; the outlet still owns push/pop focus
|
|
2741
|
+
management on its own wrapper.
|
|
2742
|
+
|
|
2743
|
+
`bf-page` now provides a per-page `BfPageContext` (`providers: [BfPageContext]`) whose `scroller`
|
|
2744
|
+
signal (`HTMLElement | null`) is where `bf-content` registers its scroll container on init and
|
|
2745
|
+
clears it on destroy, so `bf-header` can follow the page's scroll without querying the DOM. Its
|
|
2746
|
+
`headerHeight` signal (px, 0 by default) is where a `translucent` `bf-header` publishes its
|
|
2747
|
+
expanded height for `bf-content` to pad by.
|
|
2748
|
+
|
|
2749
|
+
Inside `bf-stack-outlet`, the outermost `bf-page` of a stacked page also registers its
|
|
2750
|
+
`BfPageContext` with the outlet (a nested `bf-page` leaves it), so the outlet can scroll the page's
|
|
2751
|
+
content to the top when its tab is re-selected.
|
|
2752
|
+
|
|
2753
|
+
### back-button
|
|
2754
|
+
|
|
2755
|
+
New `bf-back-button`: a `bf-icon-button` (`text` variant, chevron-left) that pops the primary
|
|
2756
|
+
stack via `BfNavController.back()`, or replaces it with `defaultHref` via `nav.root()` when there
|
|
2757
|
+
is nothing to pop back to (a deep link landing directly on the page). Visible when
|
|
2758
|
+
`nav.canGoBack()` or `defaultHref` is set, hidden otherwise. `label` (default `"Back"`) is the
|
|
2759
|
+
button's accessible name.
|
|
2760
|
+
|
|
2761
|
+
Changed: the anatomy follows the shell mode (resolved like `bf-stack-outlet`). iOS: a native
|
|
2762
|
+
button with a 24 px chevron-left and `label` as visible text (`text-heading`, regular) in
|
|
2763
|
+
`primary`, one 44 px tall control (`h-11 min-w-hit`, text truncated past 192 px), named by its
|
|
2764
|
+
text. Material: a bare 24 px arrow-left `bf-icon-button` (`text` variant) with `label` as its
|
|
2765
|
+
`aria-label`. `defaultHref` works the same in both.
|
|
2766
|
+
|
|
2767
|
+
New `BfBackButtonService` (`providedIn: 'root'`, registered and started at bootstrap by
|
|
2768
|
+
`provideBfShell()`'s environment initializer, together with `BfPlatformService`): the hardware
|
|
2769
|
+
back button queue. `register(priority, handler)` returns an unregister function; `dispatch()`
|
|
2770
|
+
(public, for tests and 2c's overlays) runs the registered handlers from highest to lowest priority
|
|
2771
|
+
until one returns true (on equal priority the newest registration runs first, so stacked overlays
|
|
2772
|
+
close top-down), then falls back to `App.exitApp()`; `BfNavController` registers the priority-0
|
|
2773
|
+
handler that pops the deepest active stack while any stack has more than one page. On native Android it subscribes to
|
|
2774
|
+
`App.addListener('backButton', …)` via `await import('@capacitor/app')` in a try/catch (never a
|
|
2775
|
+
static import, so a web or iOS bundle never loads the plugin); nothing is registered on iOS or
|
|
2776
|
+
web, where `Escape` closes overlays instead.
|
|
2777
|
+
|
|
2778
|
+
### horizontal-scroll
|
|
2779
|
+
|
|
2780
|
+
New `bfHorizontalScroll`: an attribute directive (`touch-pan-x touch-pan-y` host classes plus a
|
|
2781
|
+
`data-bf-horizontal-scroll` marker) that opts a chips row or carousel back into horizontal panning
|
|
2782
|
+
inside a `bf-stack-outlet` page. The outlet host is `touch-action: pan-y`, and its pages inherit
|
|
2783
|
+
that constraint down to their own scroll containers, so the edge swipe-back can claim horizontal
|
|
2784
|
+
drags. It only sets `touch-action`; the outlet's edge guard does
|
|
2785
|
+
not inspect the attribute, since the swipe only arms inside the 50 px left-edge zone, where it
|
|
2786
|
+
always wins regardless of what's under the finger.
|
|
2787
|
+
|
|
2788
|
+
### content
|
|
2789
|
+
|
|
2790
|
+
New `bf-content`: the page's single scroll container (host `block min-h-0 flex-1`, scroller
|
|
2791
|
+
`h-full overflow-y-auto overscroll-contain touch-pan-y pl-safe-left pr-safe-right` with
|
|
2792
|
+
`padding-bottom: var(--bf-tab-bar-inset, 0px)`, plus `pt-safe-top` when `fullscreen`, otherwise
|
|
2793
|
+
`padding-top` and `scroll-padding-top` of `var(--bf-header-height, 0px)`, a variable it writes on
|
|
2794
|
+
its scroller from `BfPageContext.headerHeight` while a `translucent` header is drawn over it). It applies
|
|
2795
|
+
`bfKeyboardAssist` to its scroller (`keyboardAssist`, default `true`), emits `scrolled`
|
|
2796
|
+
(`{ scrollTop }`) from a passive listener only while `scrollEvents` is true, offers
|
|
2797
|
+
`scrollToTop(behavior = 'smooth')`, `scrollToPoint(x, y, behavior)` and a `scrollElement` getter,
|
|
2798
|
+
and registers its scroller in the page's `BfPageContext`.
|
|
2799
|
+
|
|
2800
|
+
Changed: the projected content now sits in a plain block body inside the scroller. In iOS mode
|
|
2801
|
+
(`BfPlatformService.mode()`) the body is `min-h-[calc(100%+1px)]`, so the scroller always overflows
|
|
2802
|
+
by 1 px and WebKit rubber-bands short pages too (it only bounces an overflowing scroller); in
|
|
2803
|
+
Material mode the scroller carries `bfOverscrollStretch`, the Android edge stretch the WebView does
|
|
2804
|
+
not draw. `bf-refresher` without a page context now tracks its nearest scrolling ancestor (past
|
|
2805
|
+
the body wrapper) instead of its parent element. The stretch stays off in iOS WebKit (Safari or a
|
|
2806
|
+
home-screen app, where Capacitor reports platform `web` and the mode is Material, detected from an
|
|
2807
|
+
`iPhone`/`iPad`/`iPod` user agent or `MacIntel` with more than one touch point), which rubber-bands
|
|
2808
|
+
natively, so the two never stack.
|
|
2809
|
+
|
|
2810
|
+
Migration: the body's height is `auto`, so children that filled the scroller with `h-full` (a
|
|
2811
|
+
centred empty state) now collapse, and `min-h-full` or a `flex-1` inside a wrapper sized that way
|
|
2812
|
+
collapses too. Size such a child from its content or the viewport on your own wrapper
|
|
2813
|
+
(`min-h-[60dvh]`), not with `h-full`. The overscroll (the iOS bounce, the Android stretch and the
|
|
2814
|
+
refresher's iOS bounce mode) follows the engine (`BfPlatformService.mode()`), not
|
|
2815
|
+
`provideBfShell({ mode })`: Chrome never reports a negative `scrollTop`, so an Android app forced
|
|
2816
|
+
into the iOS look keeps the stretch and the material pull.
|
|
2817
|
+
|
|
2818
|
+
Fixed: the scroller's bottom padding also adds `--bf-header-collapse`, the height a solid
|
|
2819
|
+
`bf-header` has collapsed by (`pb-[calc(var(--bf-tab-bar-inset,0px)+var(--bf-header-collapse,0px))]`,
|
|
2820
|
+
replacing `pb-(--bf-tab-bar-inset,0px)`), so the scroll range stays constant while the large title
|
|
2821
|
+
collapses. The keyboard assist's inline `padding-bottom` still overrides it while the keyboard is up.
|
|
2822
|
+
|
|
2823
|
+
### header
|
|
2824
|
+
|
|
2825
|
+
New `bf-header`: a 44 px toolbar (`pt-safe-top`, `start` | title | `end` slots, `bfSlot="start"` /
|
|
2826
|
+
`bfSlot="end"`) and, with `largeTitle` (default `true`), a Meadow Sage large title
|
|
2827
|
+
(`text-display`) below it with an optional `subtitle` eyebrow (`text-caption text-text-muted`)
|
|
2828
|
+
above the title. Exactly one `h1` carries `data-bf-page-title`, the focus target `bf-stack-outlet`
|
|
2829
|
+
moves to after a push — the large title's when `largeTitle` is true, the toolbar's otherwise; with
|
|
2830
|
+
`largeTitle`, the toolbar shows a second, `aria-hidden="true"` `span` mirror of the title for the
|
|
2831
|
+
visual collapse only. `title` empty projects the default-slot content into whichever `h1` is
|
|
2832
|
+
active instead. Reads its sibling scroller from the page's `BfPageContext` (set by `bf-content`)
|
|
2833
|
+
through an `effect`: once non-null it attaches a passive `scroll` listener that maps the first
|
|
2834
|
+
56 px of scroll to `--bf-header-progress` (0…1, 3 decimals, written to the host only when it
|
|
2835
|
+
changes) and detaches on scroller change or destroy. That variable drives the large title's
|
|
2836
|
+
opacity and an 8 px upward translate, the toolbar mirror's fade-in and a `divider` hairline that
|
|
2837
|
+
fades in with it; the same listener sets the large title block's height to its natural height (measured
|
|
2838
|
+
after render and on `ResizeObserver` changes, never per frame) times `1 − progress`, so at
|
|
2839
|
+
progress 1 the header is exactly the toolbar. Scrolling schedules no Angular change detection;
|
|
2840
|
+
`prefers-reduced-motion` drops the translate. Solid (default), the header is in the page's flow on
|
|
2841
|
+
`bg-bg`; `translucent` draws it over the content (`absolute inset-x-0 top-0 z-(--bf-z-sticky)`,
|
|
2842
|
+
`bg-bg/85 backdrop-blur`) and publishes its expanded height in `BfPageContext.headerHeight`, which
|
|
2843
|
+
`bf-content` pads its scroller by. The large title block is inset by the 16 px screen gutter
|
|
2844
|
+
(`px-gutter`, was `px-5`), so it lines up with page content padded the same way.
|
|
2845
|
+
|
|
2846
|
+
Fixed: the large title no longer sits half-collapsed or collapses during a rubber-band. The
|
|
2847
|
+
progress is computed from `scrollTop` clamped to `[0, scrollHeight − clientHeight]`, so overscroll
|
|
2848
|
+
past either end never moves the header, and stays 0 on a page that cannot scroll the full 56 px
|
|
2849
|
+
(such as a short iOS page, whose body overflows by only 1 px). When a scroll ends part-way
|
|
2850
|
+
(`scrollend`, or 120 ms without a `scroll` event, whichever comes first) with no touch or pointer
|
|
2851
|
+
down on the scroller (passive `touchstart`/`touchend`/`touchcancel` and
|
|
2852
|
+
`pointerdown`/`pointerup`/`pointercancel` listeners), the header scrolls the scroller to 0 below
|
|
2853
|
+
half-way, else to 56 px (`behavior: 'smooth'`, `'instant'` under `prefers-reduced-motion`). The
|
|
2854
|
+
snap's own scroll never schedules another, and it is skipped while `BfKeyboardService.visible()`
|
|
2855
|
+
is true, on a page hidden by `visibility` (`checkVisibility({ visibilityProperty: true })`) and
|
|
2856
|
+
after destroy; its timer is cleared on scroller change and destroy. A mouse released outside the
|
|
2857
|
+
scroller is heard on the `document` (`pointerup`/`pointercancel`, listened to only while a press
|
|
2858
|
+
is tracked), and a press released without a scroll in between (a tap) schedules no snap.
|
|
2859
|
+
|
|
2860
|
+
Fixed: on a page whose rest scroll range lies between 56 px and 56 px plus the large title's
|
|
2861
|
+
height, the header flipped between open and part-collapsed on every frame (measured on a search
|
|
2862
|
+
page at 393 × 800: `scrollTop/max@progress` `40/72@0 → 35/35@0.714 → 40/72@0 …`): the collapse made
|
|
2863
|
+
the scroller taller, the range fell under 56 and the short-page rule reset the progress. The header
|
|
2864
|
+
now writes the collapsed height (`largeHeight × progress` in px, the exact height the block gave
|
|
2865
|
+
up; `0px` at rest, when translucent or without a large title) as `--bf-header-collapse` on the
|
|
2866
|
+
page's scroller together with `--bf-header-progress`, and removes it on destroy (or from a
|
|
2867
|
+
replaced scroller); `bf-content` pads the scroller's bottom by it, so `scrollHeight − clientHeight`
|
|
2868
|
+
stays the rest range and the short-page rule and the clamp read one stable value.
|
|
2869
|
+
|
|
2870
|
+
Fixed: the centred toolbar title sat in the space left between the `start` and `end` slots, so a
|
|
2871
|
+
back button or an avatar pushed it off the bar's centre. It is now laid out over the whole toolbar
|
|
2872
|
+
row (`absolute`, `pointer-events-none`, truncating) with symmetric insets: the header measures both
|
|
2873
|
+
slot containers with its `ResizeObserver` and writes `--bf-header-title-inset` (the 8 px row
|
|
2874
|
+
padding + the wider slot's width + 8 px; 16 px without slots) on its host, which the title box
|
|
2875
|
+
uses for `left` and `right`.
|
|
2876
|
+
|
|
2877
|
+
Changed: the bar follows the shell mode (`provideBfShell({ mode })`, the platform's for `'auto'`,
|
|
2878
|
+
resolved like `bf-stack-outlet`; the host carries `data-bf-mode`). iOS: a 44 px bar with the
|
|
2879
|
+
title (`text-heading font-semibold`, the closest step to UIKit's 17 pt semibold) centred on it.
|
|
2880
|
+
Material: a 64 px bar with the title (`text-heading font-normal`, 18 px, the closest step to
|
|
2881
|
+
Material's 22 dp regular title) leading after the `start` slot, 16 px from the edge without one,
|
|
2882
|
+
never centred. The large title stays left-aligned and collapses the same way in both modes.
|
|
2883
|
+
|
|
2884
|
+
Changed: scroll-under appearance. The scroll listener also sets `data-bf-scrolled` on the host
|
|
2885
|
+
while content is under the bar: with a large title once it has fully collapsed (progress 1; like
|
|
2886
|
+
UIKit, the large title is part of the scroll-edge bar, so a page too short to collapse never gets
|
|
2887
|
+
it), without one (`largeTitle` false) while `scrollTop` (clamped, so a rubber-band past the top does
|
|
2888
|
+
not count) is above 0, on any page length. iOS: opaque `bg-bg` at rest; scrolled under, `bg-bg/85` with `backdrop-blur-xl` and a 1 px
|
|
2889
|
+
`border`-coloured hairline under the whole header (the host's `::after`, so the header's height
|
|
2890
|
+
never changes; it replaces the `divider` hairline that faded in with the collapse). Material: no
|
|
2891
|
+
hairline ever; scrolled under, a `surface-muted` tint with a 150 ms colour transition
|
|
2892
|
+
(`motion-safe`) and no blur. `translucent` keeps its meaning (drawn over the content, which
|
|
2893
|
+
`bf-content` pads) and now takes the same per-mode surface instead of a permanent
|
|
2894
|
+
`bg-bg/85 backdrop-blur`.
|
|
2895
|
+
|
|
2896
|
+
Changed: the large title's exit is spelled in utilities instead of component CSS (the header has
|
|
2897
|
+
no `styles` any more): inside the collapsing clip the block fades (`opacity: 1 − progress`) and
|
|
2898
|
+
rises 8 px (`translateY(−8px × progress)`, `motion-safe` only, so reduced motion keeps the fade
|
|
2899
|
+
and drops the rise), and the toolbar mirror fades in (`opacity: progress`), all read from
|
|
2900
|
+
`--bf-header-progress`, never transitioned.
|
|
2901
|
+
|
|
2902
|
+
### keyboard-assist
|
|
2903
|
+
|
|
2904
|
+
New `bfKeyboardAssist` directive for a scroll container: on keyboard will-show it sets
|
|
2905
|
+
`padding-bottom` to the covered part of the container (`max(0, bottom − (restHeight −
|
|
2906
|
+
keyboardHeight))`) and `scroll-padding-bottom` to that plus `bfKeyboardAssistMargin` (default 24),
|
|
2907
|
+
then scrolls the focused `input`, `textarea` or `[contenteditable]` until it sits the margin above
|
|
2908
|
+
the keyboard — on the container's first `scroll` event after will-show or after 120 ms, whichever
|
|
2909
|
+
comes first (after WebKit's own focus scroll), with one instant correction at did-show and the same
|
|
2910
|
+
delayed reveal on `focusin` while the keyboard is open. The covered part is measured again at
|
|
2911
|
+
did-show and on every window or `visualViewport` resize while the keyboard is up, so on Android,
|
|
2912
|
+
where the WebView shrinks about 20 ms after will-show, the padding ends at 0. Both paddings and any
|
|
2913
|
+
pending reveal are removed on hide; non-text controls are ignored. `bfKeyboardAssistBehavior` is `'smooth'` (default)
|
|
2914
|
+
or `'instant'`; `[bfKeyboardAssist]="false"` turns it off.
|
|
2915
|
+
|
|
2916
|
+
Fixed: over-scroll on Android and padding left behind on parked pages. The covered part is now
|
|
2917
|
+
`max(0, bottom − BfKeyboardService.keyboardTop())`, so a WebView that has already shrunk at
|
|
2918
|
+
will-show gets no padding (it was padded by the whole keyboard, and the engine's focus reveal
|
|
2919
|
+
honoured the inflated `scroll-padding-bottom` and pushed the field under the header). A hidden host
|
|
2920
|
+
(`checkVisibility({ visibilityProperty: true })` false, so a parked page's `visibility: hidden`
|
|
2921
|
+
counts) gets no padding or reveal, drops an earlier padding and cancels a pending reveal;
|
|
2922
|
+
every window or `visualViewport` resize measures again whether or not the keyboard is up, so a
|
|
2923
|
+
padded scroller always ends at 0. The reveal now waits for the scroller to settle (100 ms after its
|
|
2924
|
+
last `scroll` event, 120 ms if none arrives, at most 400 ms) and scrolls once from the settled
|
|
2925
|
+
rects; the did-show correction also waits for quiet and is skipped while a reveal is pending.
|
|
2926
|
+
|
|
2927
|
+
Fixed: in a container too short for the focused control and both margins (a sheet or modal
|
|
2928
|
+
capped by the keyboard in phone landscape), the reveal and the did-show correction no longer
|
|
2929
|
+
ping-pong between the two edges and leave the control clipped or 2 px into the keyboard gap: the
|
|
2930
|
+
gap above the control shrinks first, then the one above the keyboard, and a control shorter than
|
|
2931
|
+
the container stays whole (a taller one keeps its bottom at the keyboard's top edge). With room
|
|
2932
|
+
for both margins nothing changes.
|
|
2933
|
+
|
|
2934
|
+
New `refresh()`: measures the covered part again and, while the keyboard is up, corrects the
|
|
2935
|
+
reveal once, for a container that moves with the keyboard outside the keyboard events (`bf-sheet`
|
|
2936
|
+
calls it when its lift transition ends, so a padding measured while the sheet was still sliding up
|
|
2937
|
+
does not stay behind).
|
|
2938
|
+
|
|
2939
|
+
### overscroll-stretch
|
|
2940
|
+
|
|
2941
|
+
New `bfOverscrollStretch` directive for a scroll container: Android-style edge stretch, since an
|
|
2942
|
+
Android WebView draws no overscroll effect for an inner scroller. A vertical touch drag past the
|
|
2943
|
+
top or bottom edge scales the container's first element child with `scaleY(1 + 0.14 × (1 −
|
|
2944
|
+
e^(−pull / 240)))` from that edge (`transform-origin: center top` / `center bottom`,
|
|
2945
|
+
`will-change: transform` during the gesture) and springs it back on release with the Web
|
|
2946
|
+
Animations API (220 ms, `SETTLE_EASING`; instant under `prefers-reduced-motion`), then clears its
|
|
2947
|
+
inline styles. Passive touch listeners only, never `preventDefault`; the axis is decided after
|
|
2948
|
+
8 px and a horizontal-dominant gesture is ignored; the top edge is skipped while an enabled
|
|
2949
|
+
`bf-refresher` (`[data-bf-refresher]`) is inside the container. `[bfOverscrollStretch]="false"`
|
|
2950
|
+
leaves it inert. The gesture follows the finger that started it (by `Touch.identifier`) and ends
|
|
2951
|
+
when that finger lifts; other fingers are ignored.
|
|
2952
|
+
|
|
2953
|
+
### keyboard service
|
|
2954
|
+
|
|
2955
|
+
New `BfKeyboardService` (`providedIn: 'root'`, registered and started at bootstrap by
|
|
2956
|
+
`provideBfShell()`): `height`, `visible`, `phase` (`'hidden' | 'showing' | 'shown'`),
|
|
2957
|
+
`overlaysContent` (true on native iOS), `restHeight`, `willShow` (a counter that increments on every
|
|
2958
|
+
will-show) and `hide()`. On native it loads `@capacitor/keyboard` with a dynamic import (a silent
|
|
2959
|
+
no-op without the plugin) and listens to will/did show/hide; either hide event moves straight to
|
|
2960
|
+
`hidden`, because iOS can deliver `keyboardDidHide` before `keyboardWillHide`. On the web it falls
|
|
2961
|
+
back to `visualViewport` resizes with an 80 px threshold; in Jest and SSR it is inert. `restHeight`
|
|
2962
|
+
comes from a fixed `100lvh` probe element appended to `document.body`, never `innerHeight`. The
|
|
2963
|
+
height is also written as `--bf-keyboard-height` on `<html>` (`0px` while hidden, removed on
|
|
2964
|
+
destroy).
|
|
2965
|
+
`hide()` calls `Keyboard.hide()` on native and blurs the focused element on the web.
|
|
2966
|
+
|
|
2967
|
+
Added `imminent`, a signal that is true from `focusin` on a text-entry control until its
|
|
2968
|
+
`focusout` or either keyboard hide event (never cleared by will-show): on Android the WebView
|
|
2969
|
+
shrinks before the plugin's `keyboardWillShow`, so chrome such as the tab bar hides on
|
|
2970
|
+
`visible() || imminent()`. When it turns true with the keyboard down it also clears itself after
|
|
2971
|
+
2500 ms if neither will-show nor a visible keyboard arrives, so a hardware keyboard (iPad Magic
|
|
2972
|
+
Keyboard, a simulator with the host keyboard), which fires no keyboard event, no longer keeps the
|
|
2973
|
+
tab bar hidden while a field has focus. Added `keyboardTop()`, the keyboard's top edge in viewport px
|
|
2974
|
+
(`restHeight() − height()` on native iOS, the bottom of `visualViewport` — else `innerHeight` —
|
|
2975
|
+
where the viewport shrinks, `restHeight()` while hidden). `isTextEntry(element)` is now exported.
|
|
2976
|
+
Fixed: the rest height is no longer measured at will-show, where the Android WebView has already
|
|
2977
|
+
shrunk (it read 554 instead of 866 px and made `bfKeyboardAssist` over-scroll); it is measured at
|
|
2978
|
+
construction and on window resizes while the keyboard is hidden and no text control is focused.
|
|
2979
|
+
|
|
2980
|
+
Fixed: on the web, a pinch-zoomed visual viewport (`visualViewport.scale` more than 0.01 from 1)
|
|
2981
|
+
is never read as a keyboard, so zooming a web build (zoom stays on there) no longer lifts sheets or
|
|
2982
|
+
pads scrollers; zooming in with the keyboard up reports it hidden until the page is back at 1×.
|
|
2983
|
+
|
|
2984
|
+
### refresher
|
|
2985
|
+
|
|
2986
|
+
New `bf-refresher`: pull-to-refresh, placed as the first child of `bf-content`. It tracks Pointer
|
|
2987
|
+
Events on the page's scroller (read from `BfPageContext`, falling back to its own parent element)
|
|
2988
|
+
only while `scrollTop === 0`. A press by itself changes nothing; only once a move clears an 8 px
|
|
2989
|
+
downward slop (out-vertical'ing any horizontal travel) does it claim the gesture, capture the
|
|
2990
|
+
pointer on the scroller, and start resisting the drag at `delta * 0.5` (capped at 72 px) into a
|
|
2991
|
+
40 px `bg-surface shadow-raised rounded-full` disc holding a `bf-spinner`, from a resting −48 px
|
|
2992
|
+
up to a 64 px threshold — so a plain click on the content underneath (a row, a button) still
|
|
2993
|
+
reaches it. Crossing the threshold ticks `BfHapticsService.impact('light')` once; releasing past
|
|
2994
|
+
it sets `refreshing` (a readonly signal), parks the disc at 0 and emits `refresh` with `{
|
|
2995
|
+
complete: () => void }` — releasing before the threshold animates the disc back to rest instead.
|
|
2996
|
+
`complete()` animates the disc away and clears `refreshing`, after which the next pull starts
|
|
2997
|
+
working again; `disabled` stops tracking pulls entirely, and further pulls are ignored while
|
|
2998
|
+
already `refreshing`. The park/settle/`complete()` transitions use the Web Animations API;
|
|
2999
|
+
`prefers-reduced-motion` only skips those — the disc still tracks the pointer 1:1 during the pull
|
|
3000
|
+
itself, since direct manipulation isn't motion. For touch, a non-passive `touchmove` guard,
|
|
3001
|
+
attached only while a press is tracked, prevents a downward drag at the top (and every move once
|
|
3002
|
+
claimed) so the browser does not take it as a pan and cancel the pointer stream; upward drags keep
|
|
3003
|
+
scrolling. Only the scroller's own `lostpointercapture` ends a pull, not the bubbling one from the
|
|
3004
|
+
row that held the touch's implicit capture. The disc (with its spinner) is `aria-hidden` and
|
|
3005
|
+
`visibility: hidden` until a pull is claimed (so its shadow never bleeds above the content); a
|
|
3006
|
+
separate, permanent visually hidden `role="status" aria-live="polite"` region is empty at rest and
|
|
3007
|
+
reads "Refreshing" while a refresh runs. It never touches the scroller's `overscroll-behavior`.
|
|
3008
|
+
|
|
3009
|
+
Changed: on iOS a touch rides the native bounce. In iOS mode (`BfPlatformService.mode()`) a touch
|
|
3010
|
+
press (`pointerType === 'touch'`, or a `touchstart`, tracked with passive listeners) is not
|
|
3011
|
+
claimed — no pointer capture, no `preventDefault`, no `touchmove` guard — so the scroller
|
|
3012
|
+
rubber-bands natively. While a finger is down, passive `scroll` events with a negative `scrollTop`
|
|
3013
|
+
show the disc at its resting −48 px, inside the gap the bounce opens, once `−scrollTop > 8`, with
|
|
3014
|
+
`opacity` = progress and `scale(0.6 + 0.4 × progress)` (progress = `min(1, −scrollTop / 64)`) and
|
|
3015
|
+
one light haptic at 1. Lifting the last finger at `−scrollTop ≥ 64` sets `refreshing`, settles the
|
|
3016
|
+
disc to 0 and emits `refresh`; otherwise the disc hides. A negative `scrollTop` with no finger down
|
|
3017
|
+
(a fling's overshoot) never shows it. Material mode and a mouse or pen in iOS mode keep the pull
|
|
3018
|
+
unchanged. The host carries `data-bf-refresher` while not `disabled`. A pen press in iOS mode,
|
|
3019
|
+
which also fires touch events, never starts a bounce, so its trailing `touchend` cannot hide the
|
|
3020
|
+
parked disc; the bounce writes the disc's styles only when it shows, changes or hides, not on
|
|
3021
|
+
every scroll; and a scroller change or destroy mid-bounce resets the disc's `opacity`, `transform`
|
|
3022
|
+
and `visibility`.
|
|
3023
|
+
|
|
3024
|
+
### haptics service
|
|
3025
|
+
|
|
3026
|
+
New `BfHapticsService` (`providedIn: 'root'`, registered by `provideBfShell()`): `impact('light' |
|
|
3027
|
+
'medium' | 'heavy')`, `selection()` and `notification('success' | 'warning' | 'error')`, all
|
|
3028
|
+
`Promise<void>`. On native it loads `@capacitor/haptics` with a dynamic import (cached after the
|
|
3029
|
+
first call) and forwards to `Haptics.impact()`, `Haptics.selectionChanged()` and
|
|
3030
|
+
`Haptics.notification()`; on the web, and when the plugin fails to load, every method resolves as
|
|
3031
|
+
a silent no-op without importing anything.
|
|
3032
|
+
|
|
3033
|
+
### tabs
|
|
3034
|
+
|
|
3035
|
+
New `bf-tabs` (`tabsPath` required input): the tabs page with one navigation stack per tab. It
|
|
3036
|
+
provides `BF_TAB_CONTEXT` (`tabKey` signal from `NavigationEnd.urlAfterRedirects`, `keyOf(url)` =
|
|
3037
|
+
the primary segment after `tabsPath`, parsed with `Router.parseUrl`, so query, fragment, matrix
|
|
3038
|
+
params and auxiliary routes are ignored) to the projected `bf-stack-outlet`, sets the
|
|
3039
|
+
projected `bf-tab-bar`'s `active` to the tab key, and handles its selections: another tab navigates
|
|
3040
|
+
with `nav.forward()` to where that tab was left (`outlet.lastUrlOf(key)`, else `/<tabsPath>/<key>`)
|
|
3041
|
+
and, once it has succeeded, `BfHapticsService.selection()` when `hapticsOnTabChange`, putting the
|
|
3042
|
+
bar back (without a haptic) if a guard refuses; the active tab again (`tabReselect`) at depth > 1 pops the stack to its existing root page
|
|
3043
|
+
(`outlet.popToRoot()`, one `Location.historyGo(-n)` step computed from the outlet's history
|
|
3044
|
+
positions, so the root keeps its DOM, scroll and state), falling back to
|
|
3045
|
+
`nav.root(['/', tabsPath, key])` when the position is unknown; at depth 1 it scrolls the active
|
|
3046
|
+
page's content to the top. It hides the bar while
|
|
3047
|
+
`BfKeyboardService.visible()` (`setHidden()`, animated on iOS only, instant on Android and the web) and writes
|
|
3048
|
+
`--bf-tab-bar-inset` on its host: `calc(64px + 24px + var(--spacing-safe-bottom))` while a bar is
|
|
3049
|
+
present and visible, `0px` otherwise. The same value is mirrored on `<html>` as
|
|
3050
|
+
`--bf-tab-bar-inset-global` for overlays, which render outside the host (the toast reads it), and
|
|
3051
|
+
removed on destroy; `bf-content` keeps reading `--bf-tab-bar-inset`, so a page outside `bf-tabs`
|
|
3052
|
+
gets no bottom padding.
|
|
3053
|
+
|
|
3054
|
+
Fixed: on Android the bar flashed above the keyboard, because the WebView shrinks (lifting the fixed
|
|
3055
|
+
bar) before `keyboardWillShow`. The bar now hides on `BfKeyboardService.visible() ||
|
|
3056
|
+
BfKeyboardService.imminent()`, i.e. already on `focusin` of a text control.
|
|
3057
|
+
|
|
3058
|
+
Fixed: an empty `tabsPath` (the tabs mounted at the root URL) built `['/', '', key]`, a doubled
|
|
3059
|
+
slash the router turned into a broken navigation. `onTabChange` and `onTabReselect` now build the
|
|
3060
|
+
command array from the non-empty segments of `tabsPath`, so `['/', key]` results when it is empty.
|
|
3061
|
+
|
|
3062
|
+
Changed: `--bf-tab-bar-inset` (and `--bf-tab-bar-inset-global`) follows the bar's new position:
|
|
3063
|
+
`calc(64px + 8px + max(var(--spacing-safe-bottom), 12px) + 8px)` (the bar's height, its offset of
|
|
3064
|
+
8 px above the safe area with a 12 px floor, and 8 px of room above the bar; 114 px on an iPhone
|
|
3065
|
+
with a home indicator, was 122 px), replacing `calc(64px + 24px + var(--spacing-safe-bottom))`.
|
|
3066
|
+
|
|
3067
|
+
Fixed: the inset counts the bar's height in `rem` (`calc(4rem + 8px + max(var(--spacing-safe-bottom),
|
|
3068
|
+
12px) + 8px)`), like the bar's own `h-16`. With iOS Dynamic Type (`BfTextScaleService`) the bar
|
|
3069
|
+
grows with the root font size; a fixed 64 px left the end of every page under the bar at the
|
|
3070
|
+
accessibility sizes (at AX3 the bar is 150 px tall). Unchanged at the default text size.
|
|
3071
|
+
|
|
3072
|
+
### tab-bar
|
|
3073
|
+
|
|
3074
|
+
New inputs `hidden` (default `false`) and `hideTransition` (default `true`), both with
|
|
3075
|
+
`booleanAttribute` (`hidden` intentionally shadows the global attribute, which is removed from the
|
|
3076
|
+
host so a static `<bf-tab-bar hidden>` slides out instead of becoming `display: none`): a hidden bar gets
|
|
3077
|
+
`translate-y-[calc(100%+40px)] opacity-0 pointer-events-none` and `aria-hidden="true"` plus
|
|
3078
|
+
`inert` on its `nav`, with `transition-[translate,opacity] duration-200` only while
|
|
3079
|
+
`hideTransition` (and never under `prefers-reduced-motion`: `motion-reduce:transition-none`). The
|
|
3080
|
+
floating bar sits on `z-(--bf-z-tabbar)`, `bottom-[calc(var(--spacing)*6+var(--spacing-safe-bottom))]`. New method `setHidden(hidden, transition?)`, the imperative counterpart for a
|
|
3081
|
+
parent that projects the bar (`bf-tabs`); the bar is hidden while the input or it is true, and
|
|
3082
|
+
`isHidden` exposes the result. New output `tabReselect`, emitted with the id when the already
|
|
3083
|
+
active tab is clicked (`tabChange` still fires only on a change; arrow keys never reselect).
|
|
3084
|
+
|
|
3085
|
+
Changed: the floating bar sits 8 px above the bottom safe area, with a 12 px floor where there is
|
|
3086
|
+
none, like other apps' floating bars: `bottom-[calc(max(var(--spacing-safe-bottom),12px)+8px)]`
|
|
3087
|
+
(42 px from the screen edge on an iPhone, 32 px with Android gesture navigation, 20 px without an
|
|
3088
|
+
inset), replacing `bottom-[calc(var(--spacing)*6+var(--spacing-safe-bottom))]` (58 px on an
|
|
3089
|
+
iPhone). Hidden, it slides down by `translate-y-[calc(100%+64px)]` (was `100% + 40px`), so it still
|
|
3090
|
+
leaves the screen with its shadow.
|
|
3091
|
+
|
|
3092
|
+
### action-sheet
|
|
3093
|
+
|
|
3094
|
+
New `BfActionSheetService.open({ title?, actions })` in `@bitforgehq/ui/mobile`, resolving
|
|
3095
|
+
`Promise<string | undefined>`. Renders `bf-action-sheet` on the overlay core (`position:
|
|
3096
|
+
'bottom'`, back priority 100, not counted as a modal, `role="dialog"` with `aria-label` = `title`
|
|
3097
|
+
or "Actions"; slides up on open and down on close). The panel (`bg-surface rounded-t-card
|
|
3098
|
+
shadow-floating w-screen px-3 pt-3 pb-[max(var(--spacing-safe-bottom),16px)]`, spanning the
|
|
3099
|
+
screen with at least 16 px under the cancel button) is an optional `title` caption over a `bf-list
|
|
3100
|
+
[inset]="false"` of the non-`cancel` `actions` as interactive rows (an icon from `icon` in the
|
|
3101
|
+
leading slot; `destructive`-role labels render `text-danger`, projected through the row's default
|
|
3102
|
+
slot since `bf-list-item` has no way to colour its own title), and a separate `tonal` `bf-button`
|
|
3103
|
+
(`fullWidth`) for the cancel action — the caller's first `cancel`-role entry if any (further ones are
|
|
3104
|
+
ignored), else a synthesized "Cancel". Destructive labels truncate like the other rows.
|
|
3105
|
+
Tapping a row resolves with its label; the cancel button, a backdrop tap, `Escape` and the
|
|
3106
|
+
hardware back button all resolve `undefined` (the overlay core's defaults, unchanged). The panel's
|
|
3107
|
+
`px-3` inset is deliberately tighter than the 16 px gutter: the rows add their own 14 px.
|
|
3108
|
+
`provideBfShell()` now also provides `BfActionSheetService`.
|
|
3109
|
+
|
|
3110
|
+
### alert-dialog
|
|
3111
|
+
|
|
3112
|
+
New `BfAlertDialogService.open({ title, message?, buttons? })` in `@bitforgehq/ui/mobile`,
|
|
3113
|
+
resolving `Promise<string>` with the tapped button's label. Renders `bf-alert-dialog` on the
|
|
3114
|
+
overlay core (`position: 'center'`, back priority 100, not counted as a modal, no backdrop
|
|
3115
|
+
dismiss, `role="alertdialog"` with `aria-labelledby` the generated title id and, when there is a
|
|
3116
|
+
message, `aria-describedby` its paragraph). The card (`w-75
|
|
3117
|
+
max-w-[calc(100vw-2*var(--spacing-gutter))] bg-surface rounded-card shadow-floating p-6 flex flex-col gap-4`) is an
|
|
3118
|
+
`h2.text-heading` title, an optional `text-body text-text-secondary` message and a right-aligned
|
|
3119
|
+
`buttons` row (default `[{ label: 'OK' }]`): `cancel`-role renders `text`, `destructive`-role
|
|
3120
|
+
`danger`, the first plain button `primary`, further plain ones `tonal`. Unlike every other
|
|
3121
|
+
overlay, `Escape` and the hardware back button do not close through the core's
|
|
3122
|
+
`dismissOnEscape`/back handling (the service passes `onEscape` and claims the back button itself):
|
|
3123
|
+
they
|
|
3124
|
+
resolve with the cancel button's label when `buttons` has one, and are otherwise claimed and
|
|
3125
|
+
ignored, since an alert with no cancel button can only be answered by tapping a button. Focus
|
|
3126
|
+
lands on the first button — the core's first-tabbable search, since the title and message hold no
|
|
3127
|
+
focusable content. `provideBfShell()` now also provides `BfAlertDialogService`.
|
|
3128
|
+
|
|
3129
|
+
### theme
|
|
3130
|
+
|
|
3131
|
+
Type tokens are in rem (same metrics at a 16 px root: `display` 2.5rem/2.75rem, `title`
|
|
3132
|
+
1.75rem/2.125rem, `heading` 1.125rem/1.625rem, `body` 0.9375rem/1.5rem, `caption`
|
|
3133
|
+
0.8125rem/1.125rem), so a user's font-size preference scales every style together. New type
|
|
3134
|
+
tokens for controls and labels: `text-label` (15/20), `text-label-sm` (14/20), `text-label-xs`
|
|
3135
|
+
(13/16), `text-micro` (12/16, weight 600), `text-nano` (11/14) and `text-avatar-lg` (26); the
|
|
3136
|
+
widgets' `text-[15px]`, `text-sm`, `text-[13px]`, `text-xs` and `text-[11px]` now use them. New
|
|
3137
|
+
spacing steps `gutter` (1rem), `card` (1.125rem), `control-sm/md/lg` (2/2.5/3rem), `hit`
|
|
3138
|
+
(2.75rem) and `box` (1.375rem), and the arbitrary px sizes became scale steps (`px-4.5`,
|
|
3139
|
+
`size-box`, `size-7.5`, `w-13`, `h-4.5`, `size-21`, `w-75`, `py-1.25`, `px-1.75`, `min-h-hit`).
|
|
3140
|
+
New `--radius-checkbox` (7px, `rounded-checkbox`), `--color-scrim` (`#000000` in both modes; the
|
|
3141
|
+
backdrop rule documents it), motion tokens `--ease-ios`, `--ease-md`, `--ease-settle` and plain
|
|
3142
|
+
`:root` variables `--bf-duration-fast/overlay/push` and `--bf-z-sticky/tabbar/dim/overlay`.
|
|
3143
|
+
|
|
3144
|
+
WCAG retunes (visual): the focus ring is a darker primary tint, `--color-ring` `#8784dc` (dark
|
|
3145
|
+
`#6362af`; was `#c9c8f5` / `#3d3e8a`), 3.03:1 on `bg` and 3.30:1 on `surface` (dark 3.48 / 3.03),
|
|
3146
|
+
so it passes WCAG 1.4.11 as the only focus indicator. Control boundaries move to `border-strong`,
|
|
3147
|
+
now `#8e8e82` (dark `#686a70`; was `#c9c4b5` / `#3a3d44`), 3.31:1 on `surface` (dark 3.02); `border`
|
|
3148
|
+
stays for decorative lines. `control-off` (toggle off track) is `#8d8f84` (dark `#686a70`; was
|
|
3149
|
+
`#d9d5c8` / `#3a3d44`), 3.28:1. `text-muted` is `#616b5b` (was `#66705f`): 5.58:1 on `surface`, 4.73
|
|
3150
|
+
on `surface-sunken`, 4.54 on `primary-soft`; placeholders and the select's empty value use it now.
|
|
3151
|
+
`text-subtle` is `#869181` (was `#95a08f`), 3.29:1, for icons and chevrons only, never text.
|
|
3152
|
+
`surface-muted` is `#efece5` (was `#f3f1ea`) so a hover row reads on `bg`; dark `surface-sunken`
|
|
3153
|
+
is `#18191c` (was `#0c0d0f`) so tracks (segmented, progress, neutral alert) read on the page. The
|
|
3154
|
+
docs theme configurator derives a ring of at least 3:1 from any primary.
|
|
3155
|
+
|
|
3156
|
+
`:root` sets `color-scheme: light` and `.dark` sets `color-scheme: dark`, so with `class="dark"`
|
|
3157
|
+
on `<html>` the browser's own parts (scrollbars, native form controls, the default canvas) turn
|
|
3158
|
+
dark with the palette. Before, only the tokens changed and those stayed light.
|
|
3159
|
+
|
|
3160
|
+
Behaviour change: because `:root` now sets `color-scheme: light`, it overrides an app's `<meta
|
|
3161
|
+
name="color-scheme" content="light dark">`. An app that relied on the browser's dark canvas and
|
|
3162
|
+
scrollbars following the system preference without `.dark` now stays light until it toggles
|
|
3163
|
+
`.dark` itself, for example from `matchMedia('(prefers-color-scheme: dark)')` as the docs site
|
|
3164
|
+
does. To keep the old behaviour in light mode, add `:root:not(.dark) { color-scheme: normal; }`
|
|
3165
|
+
after the theme import.
|
|
3166
|
+
|
|
3167
|
+
### motion
|
|
3168
|
+
|
|
3169
|
+
New `projects/ui/mobile/motion.ts`, exported from `@bitforgehq/ui/mobile`: `IOS_EASING`,
|
|
3170
|
+
`MD_EASING`, `SETTLE_EASING`, `DURATION_FAST` (150), `DURATION_OVERLAY` (260), `DURATION_PUSH`
|
|
3171
|
+
(500) and `DURATION_MD` (220). The page transitions, the overlay animations and the refresher
|
|
3172
|
+
import them (the refresher's own `SETTLE_EASING` copy is gone); `IOS_DURATION`, `MD_DURATION`,
|
|
3173
|
+
`REDUCED_DURATION`, `OVERLAY_DURATION` and `OVERLAY_EASING` stay as aliases. `bf-stack-outlet`'s
|
|
3174
|
+
modal-depth transition uses `var(--bf-duration-overlay) var(--ease-ios)` and `var(--radius-tile)`,
|
|
3175
|
+
and its dim layer is `bg-scrim/15` on `z-(--bf-z-dim)` (was `bg-text/15`, which dimmed with a
|
|
3176
|
+
light colour in dark mode).
|
|
3177
|
+
|
|
3178
|
+
### spinner
|
|
3179
|
+
|
|
3180
|
+
Sizes are now the `bf-icon` sizes: `sm` 16, `md` 20 (default), `lg` 24 px (were 16/24/32).
|
|
3181
|
+
|
|
3182
|
+
Added: `decorative` input (`booleanAttribute`). A decorative spinner has no `role="status"`, no
|
|
3183
|
+
`aria-live` and no `sr-only` label, and its wrapper is `aria-hidden="true"`: for a spinner inside a
|
|
3184
|
+
control that already reports its busy state, such as a loading `bf-button`.
|
|
3185
|
+
|
|
3186
|
+
### button
|
|
3187
|
+
|
|
3188
|
+
The loading spinner is `md` (20 px), the size of a default `bf-icon`, so swapping the start icon
|
|
3189
|
+
for the spinner no longer changes the button's width. The `outline` border is `border-strong`
|
|
3190
|
+
(hover `text-muted`). The label renders in a `data-label` span that truncates with an ellipsis
|
|
3191
|
+
(`max-w-full min-w-0 truncate`) when the container is narrower than the label; the icons keep
|
|
3192
|
+
their size, and the host and button are capped at `max-w-full`.
|
|
3193
|
+
|
|
3194
|
+
Fixed: the enabled button shows a pointer cursor (`cursor-pointer`; Tailwind's preflight gave
|
|
3195
|
+
`<button>` the default arrow), `not-allowed` when disabled.
|
|
3196
|
+
|
|
3197
|
+
Behaviour change: `loading` no longer sets native `disabled`, so keyboard focus stays on the button
|
|
3198
|
+
when an action starts (it fell to `<body>` in Chromium and WebKit). While loading the button carries
|
|
3199
|
+
`aria-disabled="true"` and `aria-busy="true"`, stays in the tab order, cancels a click or Enter/Space
|
|
3200
|
+
with `preventDefault()` (a `type="submit"` button no longer submits its form) and emits nothing. It
|
|
3201
|
+
keeps its variant fill instead of the disabled grey, with a `cursor-progress` cursor (a new `STATE`
|
|
3202
|
+
class map: `idle` `cursor-pointer`, `loading` `cursor-progress`). The spinner inside is
|
|
3203
|
+
`decorative`, so it adds no second status announcement. Consumers styling `:disabled` on a loading
|
|
3204
|
+
button see the change.
|
|
3205
|
+
|
|
3206
|
+
### tab
|
|
3207
|
+
|
|
3208
|
+
The label is `text-nano` (11/14; the line was 16.5 px). Dropped the `[&>svg]:h-5 [&>svg]:w-5`
|
|
3209
|
+
selector, which never matched (icons render inside the `bf-icon` host). A disabled tab uses tokens
|
|
3210
|
+
instead of `opacity-40`: `surface-sunken` capsule, `text-disabled` icon and label. The tab can
|
|
3211
|
+
shrink (`min-w-0`) and its label is capped at `max-w-full px-1` (it wraps, see "Changed" below), so
|
|
3212
|
+
a long label no longer overflows its quarter of the bar.
|
|
3213
|
+
|
|
3214
|
+
Fixed: the tab shows a pointer cursor and its capsule a hover (`primary-soft-hover` when active,
|
|
3215
|
+
`surface-muted` fill and `text` icon when inactive); a disabled tab keeps its sunken capsule.
|
|
3216
|
+
|
|
3217
|
+
Changed: a long label wraps to at most two lines (`line-clamp-2 wrap-anywhere text-center`) instead
|
|
3218
|
+
of truncating to one, so "Benachrichtigungen" stays readable in a quarter-width tab.
|
|
3219
|
+
|
|
3220
|
+
### list
|
|
3221
|
+
|
|
3222
|
+
`bf-list-item`'s title is `text-label` (15/20): its line is 20 px instead of an inherited 22.5, so
|
|
3223
|
+
a row without a 40 px leading slot is 2.5 px shorter (a title-only row is 48 px, above the 44 px
|
|
3224
|
+
hit area); rows with an avatar keep their 68 px.
|
|
3225
|
+
|
|
3226
|
+
Fixed: `bf-list-item` sets `role="listitem"` only inside a `bf-list` (which provides the new internal
|
|
3227
|
+
`BF_LIST` token); a standalone row has no role, so axe no longer reports `aria-required-parent` for it.
|
|
3228
|
+
|
|
3229
|
+
### alert
|
|
3230
|
+
|
|
3231
|
+
The body text is `text-label` (15 px, was 14) like other body copy; the dismiss glyph is
|
|
3232
|
+
`text-heading font-normal` (18 px, was 20; the glyph keeps its regular weight).
|
|
3233
|
+
|
|
3234
|
+
Fixed: the dismiss button shows a pointer cursor.
|
|
3235
|
+
|
|
3236
|
+
Fixed: the title and body break long words anywhere (`wrap-anywhere`), and a dismissible alert's text
|
|
3237
|
+
column reserves the dismiss button's width (`pe-10`, was `pr-12` without word breaking), so a long
|
|
3238
|
+
word no longer runs under the ×. The dismiss button is placed with `end-3` (was `right-3`).
|
|
3239
|
+
|
|
3240
|
+
### field
|
|
3241
|
+
|
|
3242
|
+
Fixed: the label, help and error text break long words anywhere (`wrap-anywhere`), so a German or
|
|
3243
|
+
Finnish compound no longer runs past the field at 320 px.
|
|
3244
|
+
|
|
3245
|
+
### progress
|
|
3246
|
+
|
|
3247
|
+
The value's width transition is off under `prefers-reduced-motion`.
|
|
3248
|
+
|
|
3249
|
+
### input
|
|
3250
|
+
|
|
3251
|
+
The rest border is `border-strong` (was `border`, 1.32:1) and the placeholder `text-muted` (was
|
|
3252
|
+
`text-subtle`, 2.73:1); the icon slots stay `text-subtle`.
|
|
3253
|
+
|
|
3254
|
+
Changed: the border narrows to 1 px (was 1.5 px); contrast stays ≥ 3:1.
|
|
3255
|
+
|
|
3256
|
+
Changed: the focus ring narrows to 2 px (was 4 px); it still shows on mouse focus, not just
|
|
3257
|
+
keyboard focus.
|
|
3258
|
+
|
|
3259
|
+
Added: `ariaLabel` input (mirrors `bf-toggle`) sets `aria-label` on the native input for a field
|
|
3260
|
+
with no visible `label` (e.g. a search box); ignored while `label` is set, so the visible label
|
|
3261
|
+
stays the sole accessible name.
|
|
3262
|
+
|
|
3263
|
+
Added: `maxLength`, `minLength`, `min`, `max`, `step`, `pattern` (exactly one pattern with no flags,
|
|
3264
|
+
`u` or `v` renders the native attribute; other flags such as `i` cannot be expressed in HTML, so the
|
|
3265
|
+
form alone validates them), `inputMode`, `enterKeyHint`, `spellcheck` and `autocapitalize` inputs,
|
|
3266
|
+
each rendered on the native `<input>` only when set; Signal Forms binds the constraint rules to them
|
|
3267
|
+
(`maxLength(p.code, 20)` renders `maxlength="20"`). New exported types `BfInputMode`,
|
|
3268
|
+
`BfEnterKeyHint` and `BfAutocapitalize`. `BfInputType` adds `date`, `time`, `datetime-local` and
|
|
3269
|
+
`month`, which open the native picker. `ariaLabelledby` sets `aria-labelledby` while there is no
|
|
3270
|
+
visible `label`. `focus(options?)` focuses the native control, so Signal Forms'
|
|
3271
|
+
`focusBoundControl()` and "focus the first invalid field" on submit now work (they were no-ops).
|
|
3272
|
+
|
|
3273
|
+
### textarea
|
|
3274
|
+
|
|
3275
|
+
The rest border is `border-strong` and the placeholder `text-muted`, as on `bf-input`.
|
|
3276
|
+
|
|
3277
|
+
Changed: the border narrows to 1 px (was 1.5 px), as on `bf-input`.
|
|
3278
|
+
|
|
3279
|
+
Changed: the focus ring narrows to 2 px (was 4 px), as on `bf-input`.
|
|
3280
|
+
|
|
3281
|
+
Added: `minLength` (native `minlength`), and `ariaLabel` / `ariaLabelledby` for a textarea without a
|
|
3282
|
+
visible `label` (ignored while `label` is set). `focus(options?)` focuses the native control, so
|
|
3283
|
+
Signal Forms' `focusBoundControl()` and "focus the first invalid field" on submit now work (they
|
|
3284
|
+
were no-ops).
|
|
3285
|
+
|
|
3286
|
+
### select
|
|
3287
|
+
|
|
3288
|
+
The rest border is `border-strong`; the empty value (placeholder) is `text-muted` (was
|
|
3289
|
+
`text-subtle`). The chevron stays `text-subtle`.
|
|
3290
|
+
|
|
3291
|
+
Changed: the border narrows to 1 px (was 1.5 px); the chevron stroke stays 1.5 px.
|
|
3292
|
+
|
|
3293
|
+
Changed: the focus ring narrows to 2 px (was 4 px), as on `bf-input`.
|
|
3294
|
+
|
|
3295
|
+
Fixed: the native select shows a pointer cursor and a hover border (`text-muted`, `danger` in the
|
|
3296
|
+
error state); focus and disabled borders still win.
|
|
3297
|
+
|
|
3298
|
+
Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
|
|
3299
|
+
could be edited; it now declares `readonly` and blocks changes. While read-only the select keeps its
|
|
3300
|
+
value on the `surface-muted` background with `aria-readonly="true"` and stays in the tab order; a
|
|
3301
|
+
mouse press focuses it without opening the picker, the picker keys (arrows, Alt+Arrow, Enter, Space,
|
|
3302
|
+
F4, type-ahead) are blocked, and a change that still gets through (the platform picker on touch
|
|
3303
|
+
screens, which offers no press to cancel) is reverted without writing the model. `ariaLabel` / `ariaLabelledby` name a
|
|
3304
|
+
select without a visible `label` (ignored while `label` is set). `focus(options?)` focuses the
|
|
3305
|
+
native control, so Signal Forms' `focusBoundControl()` and "focus the first invalid field" on submit
|
|
3306
|
+
now work (they were no-ops).
|
|
3307
|
+
|
|
3308
|
+
### checkbox
|
|
3309
|
+
|
|
3310
|
+
The error text sits 6 px under the row (`gap-1.5`, was 4 px), the label-to-message spacing of the
|
|
3311
|
+
other form widgets.
|
|
3312
|
+
|
|
3313
|
+
Fixed: hovering an enabled row tints the box (unchecked border `text-muted`, `danger` on error;
|
|
3314
|
+
checked `primary-hover`); the label was already `cursor-pointer`.
|
|
3315
|
+
|
|
3316
|
+
Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
|
|
3317
|
+
could be edited; it now declares `readonly` and blocks changes. While read-only the click (which
|
|
3318
|
+
Space also triggers) is cancelled, `aria-readonly="true"` is set, the box stays focusable, and the
|
|
3319
|
+
box rests on `surface-muted` without hover (default cursor), like a read-only radio; a checked box
|
|
3320
|
+
keeps a `primary` border and mark instead of the fill. `focus(options?)` focuses the
|
|
3321
|
+
native control, so Signal Forms' `focusBoundControl()` and "focus the first invalid field" on submit
|
|
3322
|
+
now work (they were no-ops).
|
|
3323
|
+
|
|
3324
|
+
Behaviour change (breaking for a bare attribute): `indeterminate` is now a `model(false)` (it was an
|
|
3325
|
+
`input` with `booleanAttribute`). A user change clears it and writes `false` back, so after a click
|
|
3326
|
+
the box shows the check mark that assistive tech reports instead of a stale dash; the "select all"
|
|
3327
|
+
pattern needs no `(changed)` handler to reset it. Migration: `[indeterminate]="expr"` still works
|
|
3328
|
+
one-way and `[(indeterminate)]` binds both ways; a bare `indeterminate` attribute no longer compiles,
|
|
3329
|
+
so write `[indeterminate]="true"`.
|
|
3330
|
+
|
|
3331
|
+
Fixed: the column is `items-start`, so the label row is only as wide as its content and a click on
|
|
3332
|
+
empty space beside it no longer toggles the box.
|
|
3333
|
+
|
|
3334
|
+
### radio-group
|
|
3335
|
+
|
|
3336
|
+
The legend, the options and the error text sit 6 px apart (legend `mb-1.5`, fieldset `gap-1.5`;
|
|
3337
|
+
the legend was 4 px above the options and the error 8 px below them).
|
|
3338
|
+
|
|
3339
|
+
Fixed: hovering an enabled, unselected option darkens its ring (`text-muted`, `danger` on
|
|
3340
|
+
error).
|
|
3341
|
+
|
|
3342
|
+
Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
|
|
3343
|
+
could be edited; it now declares `readonly` and blocks changes. While read-only clicks (and Space)
|
|
3344
|
+
and arrow keys are cancelled, the fieldset carries `aria-readonly="true"`, the radios stay
|
|
3345
|
+
focusable, and the rings rest on `surface-muted` without hover. `ariaLabel` / `ariaLabelledby` name
|
|
3346
|
+
a group without a visible `label`. `focus(options?)` focuses the checked radio, else the first
|
|
3347
|
+
enabled one, so Signal Forms' `focusBoundControl()` works.
|
|
3348
|
+
|
|
3349
|
+
Fixed: a disabled option's label dims to `text-disabled` (`has-[input:disabled]:text-text-disabled`
|
|
3350
|
+
on the row; `peer-disabled:` could not reach the text through its wrapper span). Vertical option
|
|
3351
|
+
rows are `items-start`, so a click on empty space beside an option no longer selects it.
|
|
3352
|
+
|
|
3353
|
+
### toggle
|
|
3354
|
+
|
|
3355
|
+
The track sits 10 px from the label (`gap-2.5`, was 12 px), like checkbox and radio. Label and
|
|
3356
|
+
error text are wrapped in a `flex flex-col items-start gap-1.5` column, so the error sits 6 px
|
|
3357
|
+
under the 44 px row (it followed with no gap). Disabled uses tokens instead of `opacity-60`: the
|
|
3358
|
+
track is `primary-soft` when checked and `surface-sunken` when unchecked, the label `text-disabled`.
|
|
3359
|
+
|
|
3360
|
+
Fixed: hovering an enabled row darkens the track (`primary-hover` on, `text-muted` off).
|
|
3361
|
+
|
|
3362
|
+
Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
|
|
3363
|
+
could be edited; it now declares `readonly` and blocks changes. While read-only the click (which
|
|
3364
|
+
Space also triggers) is cancelled and `aria-readonly="true"` is set; the switch stays focusable, the
|
|
3365
|
+
track rests on `surface-muted` without hover, bordered `primary` with a `primary` knob when on and
|
|
3366
|
+
`control-off` (keeping the ≥ 3:1 boundary) when off. `focus(options?)` focuses the native control, so Signal
|
|
3367
|
+
Forms' `focusBoundControl()` and "focus the first invalid field" on submit now work (they were
|
|
3368
|
+
no-ops).
|
|
3369
|
+
|
|
3370
|
+
### icon-button
|
|
3371
|
+
|
|
3372
|
+
The `outline` border is `border-strong` (hover `text-muted`).
|
|
3373
|
+
|
|
3374
|
+
Fixed: pointer cursor while enabled, `not-allowed` when disabled.
|
|
3375
|
+
|
|
3376
|
+
### chip
|
|
3377
|
+
|
|
3378
|
+
The unselected border is `border-strong` (hover `text-muted`). Disabled uses tokens instead of
|
|
3379
|
+
`opacity-60`: `surface-sunken` / `text-disabled`, transparent border. The label renders in a
|
|
3380
|
+
`data-label` span that truncates with an ellipsis; the chip and its host are capped at
|
|
3381
|
+
`max-w-full`. Icon to label stays 6 px (the pill's proportion at 36 px; `bf-button` uses 8).
|
|
3382
|
+
|
|
3383
|
+
Fixed: pointer cursor while enabled; a selected chip now has a hover too (`tonal`
|
|
3384
|
+
`primary-soft-hover`, `ink` `text-secondary`, fill and border).
|
|
3385
|
+
|
|
3386
|
+
### segmented
|
|
3387
|
+
|
|
3388
|
+
Each segment's hit area reaches 44 px through an `::after` extension (`after:-inset-y-1` on md
|
|
3389
|
+
36 px, `after:-inset-y-1.5` on sm 32 px); the visual size is unchanged. Labels render in a
|
|
3390
|
+
`data-label` span (it wraps, see "Changed" below), and segments are `min-w-0`, so a long label in
|
|
3391
|
+
`fullWidth` no longer overflows its share. Disabled: unselected segments take `surface-sunken`, the selected one
|
|
3392
|
+
keeps its `surface` pill without the shadow; text `text-disabled` as before.
|
|
3393
|
+
|
|
3394
|
+
Fixed: every segment shows a pointer cursor while enabled (`not-allowed` when disabled). The
|
|
3395
|
+
active segment now also tints on hover (`enabled:hover:bg-surface-muted`), matching the active
|
|
3396
|
+
tab's hover in `bf-tab-bar`.
|
|
3397
|
+
|
|
3398
|
+
Added: validation state, mirroring `bf-radio-group`. New `required`, `invalid`, `touched`, `errors`
|
|
3399
|
+
and `errorMessage` inputs; once touched and invalid (or with an explicit `errorMessage`) the first
|
|
3400
|
+
message renders under the track (`id="{id}-error"`), the radiogroup gets `aria-invalid="true"` and
|
|
3401
|
+
`aria-describedby`, and the track a 1.5 px `danger-border` ring; `aria-required` follows
|
|
3402
|
+
`required`. A required segmented choice no longer fails silently.
|
|
3403
|
+
|
|
3404
|
+
Layout change: the host is now a column (`inline-flex flex-col items-start gap-1.5`) so the message
|
|
3405
|
+
sits under the track. Without an error it renders as before; `justify-*` or `items-*` utilities a
|
|
3406
|
+
consumer put on the host now act on the column.
|
|
3407
|
+
|
|
3408
|
+
Added: `readonly` input. Behaviour change: the control ignored Signal Forms `readonly()` rules and
|
|
3409
|
+
could be edited; it now declares `readonly` and blocks changes. While read-only `select()` and the
|
|
3410
|
+
arrow, Home and End keys do nothing, the group carries `aria-readonly="true"`, the tab stop stays
|
|
3411
|
+
focusable, and the track rests on `surface-muted` without segment hover (default cursor).
|
|
3412
|
+
`ariaLabel` / `ariaLabelledby` name the group when `label` is not set. `focus(options?)` focuses the
|
|
3413
|
+
tab-stop segment, so Signal Forms' `focusBoundControl()` works. `name` is documented as bound but
|
|
3414
|
+
not rendered (the control has no native form element).
|
|
3415
|
+
|
|
3416
|
+
Fixed: under forced colours the selected label is legible. The selected segment adds
|
|
3417
|
+
`forced-colors:forced-color-adjust-none` next to `Highlight`/`HighlightText` (Chromium painted a
|
|
3418
|
+
Canvas text backplate under `HighlightText`, white on white in light, black on black in dark) and
|
|
3419
|
+
a `CanvasText` focus outline; unselected segments repaint in `ButtonText` on a transparent fill.
|
|
3420
|
+
|
|
3421
|
+
Changed: a long label wraps instead of truncating. Segments are `min-h-8`/`min-h-9` with `py-1`
|
|
3422
|
+
(were fixed `h-8`/`h-9`, `whitespace-nowrap`), the label span is `wrap-anywhere text-center`, so a
|
|
3423
|
+
segment grows taller rather than showing "Tages…".
|
|
3424
|
+
|
|
3425
|
+
### card
|
|
3426
|
+
|
|
3427
|
+
The `flat` border is `border-strong` (was `border`, 1.32:1).
|
|
3428
|
+
|
|
3429
|
+
### action-bar
|
|
3430
|
+
|
|
3431
|
+
The bar and its host are capped at `max-w-full`, so a long label no longer pushes the actions out
|
|
3432
|
+
of a narrow container (the label keeps its width and the actions wrap, see "Changed" below).
|
|
3433
|
+
|
|
3434
|
+
Changed: the label (the selection count) never truncates: it keeps its width (`shrink-0`, breaking a
|
|
3435
|
+
long word only past the bar's width), and the bar is `flex-wrap`, so actions that do not fit beside
|
|
3436
|
+
it move to a second row.
|
|
3437
|
+
|
|
3438
|
+
### platform service
|
|
3439
|
+
|
|
3440
|
+
Fixed: `BfPlatformService` now applies the `dark` class to `<html>` and persists the theme choice
|
|
3441
|
+
to `localStorage` only when `provideBfShell()` ran (`BF_SHELL_CONFIG` is present, checked via
|
|
3442
|
+
`inject(BF_SHELL_CONFIG, { optional: true })`). The `theme`/`dark` signals still compute correctly
|
|
3443
|
+
without it. Previously, injecting `BfPlatformService` on its own — e.g. a docs demo that only
|
|
3444
|
+
wants `mode`/`platform` for a widget preview — ran the DOM/storage effect anyway, which reverted
|
|
3445
|
+
the host page's own theme choice to the OS `prefers-color-scheme` on every change.
|
|
3446
|
+
|
|
3447
|
+
The shell now enforces a no-zoom viewport: with `provideBfShell()` on a native platform,
|
|
3448
|
+
`BfPlatformService` makes `meta[name=viewport]` contain `maximum-scale=1` and `user-scalable=no`
|
|
3449
|
+
(other directives kept; the meta is created with `width=device-width, initial-scale=1,
|
|
3450
|
+
maximum-scale=1, user-scalable=no, viewport-fit=cover` if missing). Fixed: iOS zoomed to 16/15 when
|
|
3451
|
+
a 15 px field was focused and stayed zoomed after blur; WKWebView honours `user-scalable=no` and
|
|
3452
|
+
drops the focus zoom along with pinch and double-tap zoom. See the behaviour change below for the
|
|
3453
|
+
recommended `index.html` viewport, which does not carry these directives statically.
|
|
3454
|
+
|
|
3455
|
+
**Behaviour change:** the no-zoom lock is native-only by design (the owner's ruling, audit §12),
|
|
3456
|
+
and the viewport guidance now says so. The recommended `index.html` viewport is `width=device-width,
|
|
3457
|
+
initial-scale=1, viewport-fit=cover` without `maximum-scale`/`user-scalable`, so web builds keep
|
|
3458
|
+
pinch zoom and pass axe `meta-viewport`; `provideBfShell()` still adds the lock at boot on iOS and
|
|
3459
|
+
Android, where the system text size takes its place (`BfTextScaleService` on iOS, the WebView's
|
|
3460
|
+
font scale on Android). The mobile demo's `index.html`, which also serves the web build and the
|
|
3461
|
+
docs' embedded phone, no longer locks zoom. Migration: drop `maximum-scale=1, user-scalable=no`
|
|
3462
|
+
from your `index.html` if the app also ships to the web; native builds behave as before.
|
|
3463
|
+
|
|
3464
|
+
### text scale service
|
|
3465
|
+
|
|
3466
|
+
New `BfTextScaleService` (`providedIn: 'root'`, started at bootstrap by `provideBfShell()`): iOS
|
|
3467
|
+
Dynamic Type in the native shell. A hidden, persistent probe with `font: -apple-system-body`
|
|
3468
|
+
gives the user's body text size (17 px at the default "Large", 21 px at xxL, 40 px at AX3), and
|
|
3469
|
+
`<html>` gets `font-size: probe / 17 × 100 %` (rounded to 0.1 %), so every `rem`-based type token
|
|
3470
|
+
and spacing step grows with it; a `ResizeObserver` on the probe and `visibilitychange` re-apply it
|
|
3471
|
+
when the setting changes while the app runs. The scale (`scale()` signal, 1 by default) never goes
|
|
3472
|
+
below 1 and is capped by the new optional `BfShellConfig.dynamicType` member (`{ maxScale: 2.35 }`
|
|
3473
|
+
when left out, the AX3 size, at least 200 %; `false` opts out), so a full `BfShellConfig` literal
|
|
3474
|
+
written without it still compiles. It acts only with `provideBfShell()` on
|
|
3475
|
+
native iOS; Android's WebView applies the system font scale itself, and the web keeps the
|
|
3476
|
+
browser's text size and zoom. Destroying the service removes the probe and the root font size.
|
|
3477
|
+
Fixed: iOS text could not be enlarged at all in the shell (F-048).
|
|
3478
|
+
|
|
3479
|
+
### agent docs
|
|
3480
|
+
|
|
3481
|
+
The package ships `ai/MANIFEST.index.json` beside `ai/MANIFEST.json`: one small record per
|
|
3482
|
+
component (`name`, `selector`, `className`, `category`, `entryPoint`, `summary`, `since`), generated
|
|
3483
|
+
with the rest of `ai/**` (`bun run ai:docs`). An agent can pick a component from it without reading
|
|
3484
|
+
the full manifest; the docs site bundles it and fetches the full manifest on demand. Every
|
|
3485
|
+
`MANIFEST.json` entry also gains a `summary` field, and `llms.txt` and the `UI_GUIDE.md` component
|
|
3486
|
+
table now show it in place of the description's first sentence; each `ai/components/*.md` carries
|
|
3487
|
+
it as a line under the title.
|
|
3488
|
+
|
|
3489
|
+
### docs site
|
|
3490
|
+
|
|
3491
|
+
Every `@bitforgehq/ui/mobile` component page runs a live demo in a drawn phone: the docs'
|
|
3492
|
+
`app-phone-frame` embeds the mobile demo's web build at the component's showcase route
|
|
3493
|
+
(`mobile-demo/?mode=ios|md&embed=1&start=showcase/<name>`), with an iPhone / Pixel picker
|
|
3494
|
+
remembered across pages and an "Open full screen" link. The Mobile page uses the same frame on
|
|
3495
|
+
the whole app (`start=tabs`). The phone follows the docs theme: the configurator's tokens and
|
|
3496
|
+
light/dark reach the demo through a same-origin `postMessage` (`bf-docs-theme`). The mobile demo
|
|
3497
|
+
gains the showcase routes, the `?start=` entry (only `tabs` or a known showcase; a device build
|
|
3498
|
+
has no query string and still starts on `tabs`) and, with `?embed=1` only, the theme bridge. The
|
|
3499
|
+
frame also takes an Appearance override (follow the docs theme, or pin light/dark) independent of
|
|
3500
|
+
the site theme, and sends its drawn status-bar/tab-bar heights to the embedded demo as
|
|
3501
|
+
`?insets=<top>,<bottom>` so its header and tab bar clear them.
|
|
3502
|
+
|
|
3503
|
+
New this phase: every component page renders from the same fixed skeleton (breadcrumb, title,
|
|
3504
|
+
summary, meta chips, install/import snippet, Preview | Code example card, Overview, usage
|
|
3505
|
+
sections, Theming, Accessibility, API, notes, migration notes, then previous/next links) sourced
|
|
3506
|
+
from the shipped manifest; a command-palette search (`Cmd/Ctrl+K`) finds any component or guide
|
|
3507
|
+
page; `SeoService` sets a per-route title, meta description, canonical link and Open Graph/Twitter
|
|
3508
|
+
tags, always under the Firebase origin regardless of which host serves the page, and marks the 404
|
|
3509
|
+
view `noindex`; every page passes an axe accessibility check (zero `serious`/`critical`
|
|
3510
|
+
violations, in light and dark) and the WAI-ARIA patterns for its tabs, search and table of
|
|
3511
|
+
contents; and CI's last step runs Lighthouse against five pages, blocking under performance 90,
|
|
3512
|
+
accessibility 100, best practices 95 or SEO 100.
|
|
3513
|
+
|
|
3514
|
+
Docs correctness: a site-wide pre-release note (an `aside` labelled "Release status") says the
|
|
3515
|
+
package is not on npm yet and links to the Install page's "Before the npm release", inside the
|
|
3516
|
+
"Add the packages" step: it states that the source repository is private and needs read access,
|
|
3517
|
+
clones it beside the app, packs the built library into `../bitforge-ui-packages/` (outside
|
|
3518
|
+
`dist/`, which every build wipes), adds that tarball and shows how to update it later; the
|
|
3519
|
+
generated guide and README install blocks carry the same note and instructions. The home page's
|
|
3520
|
+
install command reads `bun add @bitforgehq/ui@next`, labelled "Pre-release — not on npm yet". The
|
|
3521
|
+
Install page is a verified quick start rendered from one source, `INSTALL_STEPS`
|
|
3522
|
+
(`projects/docs/src/app/consts/install.const.ts`): prerequisites (Node in the Angular CLI 22
|
|
3523
|
+
range `^22.22.3 || ^24.15.0 || >=26.0.0`), `ng new`, `ng add tailwindcss`,
|
|
3524
|
+
one `bun add`, the `styles.css` block (no `@tailwindcss/forms`), `bg-bg text-text` on `<body>`,
|
|
3525
|
+
dark mode, a first component (removing the welcome page and updating the generated `app.spec.ts`)
|
|
3526
|
+
and what you should now see. `ai/UI_GUIDE.md` §Install and the
|
|
3527
|
+
install block of `projects/ui/README.md` are generated from the same steps, and the consumer-smoke job builds a
|
|
3528
|
+
clean app from them with 0 errors and 0 warnings. A new "Forms and validation" guide (`/forms`)
|
|
3529
|
+
covers Signal Forms setup, validators with messages, `[formRoot]` submission with focus on the
|
|
3530
|
+
first invalid field and server errors, with a live example whose Code view is the compiled file;
|
|
3531
|
+
Reactive Forms (supported through Angular 22's interop, no `ControlValueAccessor`) with an example
|
|
3532
|
+
the consumer-smoke job compiles and a precise account of what the interop syncs; template-driven
|
|
3533
|
+
forms; and read-only versus disabled. The footer links the guide. Deep links
|
|
3534
|
+
to a component-page section (`/components/button#api`) now scroll to it once the deferred body
|
|
3535
|
+
renders. The `bf-input` "With Signal Forms" snippet now compiles as shown (the imports,
|
|
3536
|
+
`[formRoot]` instead of a bare `(submit)`, and validators with messages). `llms.txt` links to the canonical origin (`https://bitforge-ui.web.app`, was a GitHub
|
|
3537
|
+
Pages mirror that returned 404) and lists the guide pages.
|
|
3538
|
+
|
|
3539
|
+
### packaging
|
|
3540
|
+
|
|
3541
|
+
Behaviour change for installs: the peer metadata now matches what each entry imports.
|
|
3542
|
+
`@angular/cdk`, `@angular/router` and `@capacitor/core` are optional peers, so a desktop-only
|
|
3543
|
+
install no longer pulls them in; an app that imports `@bitforgehq/ui/mobile` installs them itself.
|
|
3544
|
+
`rxjs` (`^7.8.0`, imported by `/mobile`) is declared as an optional peer. `lucide` is now a required
|
|
3545
|
+
peer (`bf-back-button` imports its icons statically, and `@bitforgehq/ui/icons` renders Lucide
|
|
3546
|
+
data). `@capacitor/app`, `@capacitor/keyboard` and `@capacitor/haptics` stay optional.
|
|
3547
|
+
`publishConfig` sets `access: public` and `provenance: false`: the source repository is private,
|
|
3548
|
+
and npm generates no provenance for private repositories.
|
|
3549
|
+
|
|
3550
|
+
### haptics
|
|
3551
|
+
|
|
3552
|
+
Fixed: a missing `@capacitor/haptics` no longer breaks the build. Its dynamic import sits inside a
|
|
3553
|
+
`try` with a `.catch()`, so esbuild tolerates the unresolved optional peer and `BfHapticsService`
|
|
3554
|
+
resolves every method as a no-op when the plugin is not installed. Before, every
|
|
3555
|
+
`@bitforgehq/ui/mobile` import failed to build with `Could not resolve "@capacitor/haptics"`.
|
|
3556
|
+
|
|
3557
|
+
### all components
|
|
3558
|
+
|
|
3559
|
+
Every boolean `input()` across `projects/ui/src` and `projects/ui/mobile` now carries `{ transform:
|
|
3560
|
+
booleanAttribute }`, so a bare HTML-style attribute (`<bf-button disabled fullWidth loading>`,
|
|
3561
|
+
`<bf-list-item interactive chevron>`, `<bf-input required readonly>`) compiles and works the same
|
|
3562
|
+
as `[disabled]="true"` under `strictTemplates`. A new `tools/boolean-inputs.test.ts` (`bun run
|
|
3563
|
+
test:tools`) scans every non-spec source file for a boolean `input()` missing the transform, so a
|
|
3564
|
+
regression fails CI. Boolean `model()` inputs are unaffected (a `transform` is not supported on
|
|
3565
|
+
`model()`).
|
|
3566
|
+
|
|
3567
|
+
### forms
|
|
3568
|
+
|
|
3569
|
+
The Signal Forms integration spec now covers all seven value controls (value both ways,
|
|
3570
|
+
`disabled()`, `readonly()`, `required` with the message after touch, `reset()`,
|
|
3571
|
+
`focusBoundControl()`, `submit()` focusing the first invalid field, and the `maxLength`,
|
|
3572
|
+
`minLength` and `pattern` rules on the native attributes). A new Reactive Forms interop spec pins
|
|
3573
|
+
`[formControl]` on every control with Angular 22's custom-control support (no
|
|
3574
|
+
`ControlValueAccessor`): value both ways, touched through `touch`, `disable()`/`enable()`,
|
|
3575
|
+
`Validators.required` to `required`, and errors without a `message`, whose text comes from
|
|
3576
|
+
`errorMessage`. `tools/form-spec-members.test.ts` fails when a form control's input, model or output
|
|
3577
|
+
is not exercised in its spec: only `setInput('<member>'`, `componentInstance.<member>`, a
|
|
3578
|
+
`.<member>(` call or a `[<member>]` / `(<member>)` binding count, never an error `kind` string or a
|
|
3579
|
+
DOM property read.
|
|
3580
|
+
|
|
3581
|
+
## 0.1.0 (not published)
|
|
3582
|
+
|
|
3583
|
+
### theme
|
|
3584
|
+
|
|
3585
|
+
Dark surfaces now sit further apart (`surface` #1e2024, `surface-muted` #262930, `surface-sunken` #0c0d0f) so sunken tracks and raised segments stay distinguishable on the neutral dark palette. Default primary changed from the Meadow Sage green (`#2f8f5b`) to indigo `#2b26ba`, with the derived brand tokens (`primary-hover`, `primary-soft`, `primary-soft-hover`, `primary-strong`, `selected`, `ring`) re-tuned in both modes. The dark palette is now neutral gray (surfaces, lines, text) instead of green-tinted, so any primary colour fits without touching the rest of the theme.
|
|
3586
|
+
|
|
3587
|
+
Initial widgets: `bf-button`, `bf-badge`, `bf-spinner`, `bf-icon`, the Meadow Sage theme, and the form set `bf-field`, `bf-input`, `bf-textarea`, `bf-select`, `bf-checkbox`, `bf-toggle`, `bf-radio-group` (all Signal Forms ready), and the display set `bf-icon-button`, `bf-chip`, `bf-segmented`, `bf-list`/`bf-list-item`, `bf-card`, `bf-progress`, `bf-avatar`, `bf-alert`, `bf-empty-state`, `bf-action-bar`, `bf-tab-bar`/`bf-tab`.
|
|
3588
|
+
|
|
3589
|
+
### input
|
|
3590
|
+
|
|
3591
|
+
Field sizes renamed: `sm`/`md` (40/48 px) is now `md`/`lg` (40/48 px), default stays 48 px but the default token changed to `lg`. The host no longer reflects a static `id` attribute onto `<bf-input>`; the id lands only on the native `<input>`.
|
|
3592
|
+
|
|
3593
|
+
### textarea
|
|
3594
|
+
|
|
3595
|
+
Same `md`/`lg` size rename as `bf-input`. The host no longer reflects a static `id` attribute onto `<bf-textarea>`.
|
|
3596
|
+
|
|
3597
|
+
### select
|
|
3598
|
+
|
|
3599
|
+
Same `md`/`lg` size rename as `bf-input`. Without a `placeholder`, an unmatched `value` (e.g. the empty string) now renders a hidden, disabled, selected placeholder option so the native control shows no visible selection instead of falling back to the first real option. The inert `[value]` binding on `<select>` was removed; selection is driven entirely by `[selected]` on each `<option>`. The host no longer reflects a static `id` attribute onto `<bf-select>`. Added `bg-none` so consumers loading `@tailwindcss/forms` don't get a second, duplicate chevron drawn by the plugin's default `background-image` alongside the library's own CSS chevron.
|
|
3600
|
+
|
|
3601
|
+
### checkbox
|
|
3602
|
+
|
|
3603
|
+
The label's cursor styling is now a computed `labelClasses`. The redundant `[attr.aria-checked]` binding was removed from the native checkbox; the native `checked`/`indeterminate` state already conveys it to assistive tech. The focus ring now clears in forced-colors mode. The host no longer reflects a static `id` attribute onto `<bf-checkbox>`. Fixed the checked box never showing its fill: `bg-surface`/`after:opacity-0` and the "on" branch's `bg-primary`/`after:opacity-100` were both present at once, and Tailwind's stylesheet order let `bg-surface` win, leaving a checked box white with an invisible check mark. The base class list and the on/off branches now each emit exactly one background and one after-opacity utility. The label's clickable hit area is now `min-h-11 items-center` instead of a `::after` pseudo-element; a `::after` extension over projected content (the label text) is the wrong tool — `min-h-11` gets the same ≥ 44 px target without an overlay. Dropped the box's `mt-0.5` top-margin nudge; with the row already `min-h-11 items-center`, the box centres on the label line on its own. Added `ariaLabel` (mirrors `bf-toggle`) for an accessible name when there's no visible `label`, e.g. a row-selection box; the label `<span>` no longer renders when `label` is empty, so an `ariaLabel`-only checkbox has no empty gap next to the box. Projected label content and the `label` input share one styled text span, which hides itself when both are empty (`empty:hidden`).
|
|
3604
|
+
|
|
3605
|
+
### toggle
|
|
3606
|
+
|
|
3607
|
+
Added `errorMessage` and Signal Forms error rendering (matching `bf-checkbox`): a `<p>` under the label, `aria-invalid` and `aria-describedby` on the input. `labelPosition="start"` now stretches the label to `w-full` with `justify-between` for settings-list layouts. The track and knob keep a transparent 1.5px border and the focus ring clears in forced-colors mode. The host no longer reflects a static `id` attribute onto `<bf-toggle>`. Fixed the knob sitting off-centre: the track's padding was `p-[3px]` alongside its `border-[1.5px]`, so the 22px knob had unequal 4.5px/1.5px margins at the two ends of its 20px `translate-x-5` travel; padding is now `p-[1.5px]` for equal 3px margins on both sides. Also removed the redundant `box-border` from the knob (preflight already sets `box-sizing: border-box` globally). The label's clickable hit area is now `min-h-11 items-center` instead of a `::after` pseudo-element, in both `labelPosition` branches, for the same reason as `bf-checkbox`.
|
|
3608
|
+
|
|
3609
|
+
### radio-group
|
|
3610
|
+
|
|
3611
|
+
The fieldset now carries `role="radiogroup"`. The ring's focus ring clears in forced-colors mode and the selected dot renders in `CanvasText` under forced colors. The host no longer reflects a static `id` attribute onto `<bf-radio-group>`; the id lands on the `<fieldset>`. Each option's label now carries `min-h-11 items-center`, so a stacked row is itself at least 44 px tall and is its own hit area, rather than a `::after` pseudo-element that overlapped into the neighbouring row's 10px gap and stole its clicks near the boundary; the vertical option list's gap tightened from `gap-2.5` to `gap-1` to match the now taller rows.
|
|
3612
|
+
|
|
3613
|
+
### field
|
|
3614
|
+
|
|
3615
|
+
`disabled` no longer dims the whole field; it dims only the label. Controls already carry their own disabled styling.
|
|
3616
|
+
|
|
3617
|
+
### icon-button
|
|
3618
|
+
|
|
3619
|
+
New `bf-icon-button`: a round button that holds a single icon, with `tonal` (default), `primary`, `outline` and `text` variants and `sm`/`md`/`lg` (32/40/48 px) square sizes. `label` is required and renders as `aria-label` since the icon alone gives assistive tech no name. `sm` and `md` extend the hit area vertically to 44 px with an invisible `::after`, matching `bf-button`, so adjacent icon buttons in a toolbar don't overlap.
|
|
3620
|
+
|
|
3621
|
+
### chip
|
|
3622
|
+
|
|
3623
|
+
New `bf-chip`: a toggleable pill for filters and tags, 36 px tall, with `tonal` (default) and `ink` treatments. Renders a `<button aria-pressed>` and shows a `::before` check mark while selected. Two-way `[(selected)]` model plus a `toggled` output fired after a user click; the hit area extends vertically with `::after`, matching `bf-button`. Fixed the check mark rendering as a thin vertical line instead of a tick: the `::before` pseudo-element had no `display` utility, and an inline, non-replaced box ignores explicit `width`/`height`; added `before:block`. Added `deselectable` (default `true`): set it `false` in a single-choice filter row so clicking the already-selected chip does nothing instead of clearing the selection. The unselected state's hover border is now `enabled:hover:border-border-strong` so a disabled chip doesn't show a hover state it can't act on.
|
|
3624
|
+
|
|
3625
|
+
### segmented
|
|
3626
|
+
|
|
3627
|
+
New `bf-segmented`: a `role="radiogroup"` control for switching between two to four views, implementing `FormValueControl<string>`. `sm`/`md` (32/36 px) item heights, optional `fullWidth`. Keyboard arrow keys move the roving-tabindex selection and wrap around, skipping disabled options; `Home`/`End` jump to the first/last enabled option. The roving tab stop (`tabStop`) now falls back to the first enabled option when the selected value is missing or disabled, instead of leaving every segment at `tabIndex -1`. A keyboard selection also moves DOM focus to the newly selected button. The active segment's fill is now visible under forced-colors (`forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]`), and the inactive segment explicitly clears its background there.
|
|
3628
|
+
|
|
3629
|
+
### list
|
|
3630
|
+
|
|
3631
|
+
New `bf-list` and `bf-list-item`: a `role="list"` container (inset by default, with a warm background and rounded corners; `inset=false` drops it for rows on a page) and a row with `leading`/`trailing` content slots, `title`/`subtitle` text, an optional trailing chevron, and an `interactive` mode that renders the row as a `<button>` emitting `clicked`. The interactive row's hover background is now `enabled:hover:bg-surface-muted` so a disabled row doesn't show a hover state; removed the unused `relative` from the row's base classes.
|
|
3632
|
+
|
|
3633
|
+
### card
|
|
3634
|
+
|
|
3635
|
+
The card surface now fills its host (`h-full`), so cards in a stretched grid row end at the same height instead of ragged bottoms; block hosts of auto height are unaffected.
|
|
3636
|
+
|
|
3637
|
+
New `bf-card`: a content surface with `header`/default/`footer` slots and four variants — `raised` (default, `shadow-raised`), `tint`, `ink` and `flat`. `padding` is `md` (18 px, default) or `lg` (24 px). `interactive` renders the card as a `<button>` that emits `clicked` and adds a hover shadow and focus ring.
|
|
3638
|
+
|
|
3639
|
+
### progress
|
|
3640
|
+
|
|
3641
|
+
New `bf-progress`: a determinate `role="progressbar"` for completion and quotas. `value` is clamped to 0–100 and rounded; `size` is `sm` (8 px, default) or `md` (10 px); `tone` (`primary` default, `success`, `warning`, `danger`) colors the fill. `label` is required and becomes the bar's `aria-label`. The native `<progress>` element now carries an explicit `role="progressbar"` attribute (it already had the role implicitly) so consumers and specs can select on it directly. `percent` now guards against a non-finite `value` (e.g. `NaN`), rendering `0` instead of propagating `NaN` into the fill width and `aria-valuenow`.
|
|
3642
|
+
|
|
3643
|
+
### avatar
|
|
3644
|
+
|
|
3645
|
+
New `bf-avatar`: a round avatar showing a photo (`src`/`alt`) or initials on a tinted circle. `size` is `sm` (30 px), `md` (40 px, default) or `lg` (84 px); `variant` is `tint` (default), `ink`, `warning` or `neutral`. The host now carries `role="img"`/`aria-label` only when `alt` is set; without `alt` the host is `aria-hidden="true"` and decorative, instead of always announcing itself with the initials as a fallback name. The inner `<img>` now always has `alt=""` so the image itself is hidden from the accessibility tree and the host's `aria-label` is the single source of the accessible name.
|
|
3646
|
+
|
|
3647
|
+
### alert
|
|
3648
|
+
|
|
3649
|
+
New `bf-alert`: an inline `role="status"` (`role="alert"` for `danger`) message with `neutral` (default), `success`, `warning` and `danger` variants, an `icon` slot, a `title`, and an `actions` slot. `dismissible` shows a close button that emits `dismissed`; the component itself never hides. The dismiss button now renders as a sibling of the `role="status"`/`role="alert"` element (both wrapped in an outer `div`) instead of inside it, so assistive tech reading the status/alert region doesn't also announce the dismiss control as part of the message; the visual position (absolute, top-right) is unchanged. Added `dismissLabel` (default `"Dismiss"`) to customize the button's accessible name. The 32 px dismiss button now extends its hit area to 44 px with `after:-inset-1.5`, a self-contained control so the `::after` extension is the right tool here (unlike a control over projected content).
|
|
3650
|
+
|
|
3651
|
+
### empty-state
|
|
3652
|
+
|
|
3653
|
+
New `bf-empty-state`: a centered placeholder with a 64 px `icon` tile, a required `title`, an optional `description`, and an `actions` slot for the action that fills it. Added `headingLevel` (`2` default or `3`) so the title renders as `h2` or `h3` to fit the page outline.
|
|
3654
|
+
|
|
3655
|
+
### action-bar
|
|
3656
|
+
|
|
3657
|
+
New `bf-action-bar`: a floating pill for actions on a selection, with an optional leading `label` and a default slot for buttons. It only styles itself; the consumer positions it (fixed bottom-centre on web, `pb-safe-bottom` on mobile). Uses `role="group"` (was toolbar): the bar's contents aren't a fixed, single-tab-stop toolbar of controls, so `group` is the correct ARIA role; `aria-labelledby` now points at the `label` span instead of relying on visible text alone.
|
|
3658
|
+
|
|
3659
|
+
### tab-bar
|
|
3660
|
+
|
|
3661
|
+
New `bf-tab-bar`: the Meadow Sage floating pill tab bar for mobile navigation, 64 px tall, holding a `role="tablist"` of `bf-tab` children. `active` (model) tracks the selected tab id; `label` (default `Main`) names the tablist; `floating` (default `true`) fixes the bar above the safe area with `shadow-floating`, or renders it inline with `shadow-raised` when false. ArrowLeft/ArrowRight/Home/End navigation moves both selection and focus together across the enabled tabs, wrapping at the ends and skipping disabled ones; selecting a tab emits `tabChange`. The bar and `bf-tab` communicate through the `BF_TAB_BAR` injection token so neither file imports the other. Added a `tabStop` computed, exposed through `BF_TAB_BAR`: the roving tab stop now falls back to the first enabled tab when `active` matches no enabled tab (empty, unresolved, or the active tab is disabled), instead of leaving every tab at `tabIndex -1` and removing the bar from the tab order.
|
|
3662
|
+
|
|
3663
|
+
### tab
|
|
3664
|
+
|
|
3665
|
+
New `bf-tab`: one destination inside `bf-tab-bar` — an icon in the default slot, `label` under it, and an optional `badge` count. `id` and `label` are required; `disabled` removes it from keyboard navigation and blocks clicks. Renders a native `<button role="tab">` with `aria-selected` and a roving `tabindex` driven by the bar's `tabStop`; the active tab's icon sits on a `primary-soft` capsule (`forced-colors:bg-[Highlight]`) with a `primary-strong` label, inactive tabs are `text-muted`. The host no longer reflects `id` as a DOM attribute (`bf-tab id="today"` previously duplicated `id="today"` onto both the host and the inner `<button>`); the id now lands once, on the button. The badge is `aria-hidden`; a visually hidden `sr-only` span after the label announces it instead (`"{n} new"` for a numeric badge, the string as-is otherwise), so the tab's accessible name carries meaning instead of a bare number.
|