@bitforgehq/ui 0.3.1 → 0.3.2
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 +116 -0
- package/README.md +9 -8
- package/ai/MANIFEST.index.json +18 -0
- package/ai/MANIFEST.json +368 -31
- package/ai/REFERENCE.index.json +63 -0
- package/ai/REFERENCE.json +247 -5
- package/ai/UI_GUIDE.md +59 -33
- package/ai/components/app-frame.md +328 -0
- package/ai/components/card.md +56 -2
- package/ai/components/empty-state.md +99 -6
- package/ai/components/header.md +5 -1
- package/ai/components/sidebar-toggle.md +287 -0
- package/ai/components/sidebar.md +188 -10
- package/ai/components/stat.md +74 -10
- package/ai/components/table.md +134 -5
- package/ai/components/tabs.md +5 -1
- package/ai/components/toolbar.md +69 -4
- package/ai/reference/bf-app-frame-breakpoint.md +27 -0
- package/ai/reference/bf-app-frame-content-width.md +29 -0
- package/ai/reference/bf-empty-state-heading-level.md +1 -1
- package/ai/reference/bf-empty-state-heading.md +33 -0
- package/ai/reference/bf-labels.md +6 -0
- package/ai/reference/bf-sidebar-state-service.md +21 -0
- package/ai/reference/bf-sidebar-toggle-harness.md +32 -0
- package/ai/reference/bf-size.md +1 -0
- package/ai/reference/bf-stat-appearance.md +33 -0
- package/ai/reference/bf-stat-variant.md +1 -1
- package/ai/reference/bf-table-harness.md +1 -1
- package/ai/reference/bf-table-stack.md +29 -0
- package/ai/reference/bf_sidebar_state_storage.md +20 -0
- package/ai/reference/provide-bf-labels.md +4 -3
- package/ai/reference/provide-bf-sidebar-state.md +20 -0
- package/bin/bf-ui.js +433 -3
- package/fesm2022/bitforgehq-ui-overlay.mjs +571 -446
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +18 -2
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +759 -127
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +1560 -43
- package/llms.txt +11 -0
- package/package.json +1 -1
- package/types/bitforgehq-ui-overlay.d.ts +51 -0
- package/types/bitforgehq-ui-testing.d.ts +35 -1
- package/types/bitforgehq-ui.d.ts +276 -20
package/llms-full.txt
CHANGED
|
@@ -117,7 +117,12 @@ the widgets style their native controls themselves.
|
|
|
117
117
|
Optional per-app overrides go after the import. A brand colour is a set: override
|
|
118
118
|
`--color-primary` together with `--color-primary-text` (primary as text, ≥ 4.5:1 on `bg` and
|
|
119
119
|
`surface`) and the rest of the `--color-primary-*` tokens, in `@theme` and in `.dark`; the docs
|
|
120
|
-
site's theme configurator derives the whole contrast-checked set from one colour.
|
|
120
|
+
site's theme configurator derives the whole contrast-checked set from one colour. From a
|
|
121
|
+
terminal, `bunx bf-ui theme '<hex>' [--dark '<hex>']` prints the set (`primary`, its hover,
|
|
122
|
+
pressed, soft and strong steps, `on-primary`, `primary-text`, `selected`, `ring`) as a `@theme`
|
|
123
|
+
and a `.dark` block with each contrast pair's ratio, and exits 1 naming the pairs below their
|
|
124
|
+
minimum and a shade that passes; paste its output, never hand-pick the steps. Example (the
|
|
125
|
+
Meadow Sage green): `bunx bf-ui theme '#238652' --dark '#5cc48a'`.
|
|
121
126
|
`--radius-control` (`9999px`, pills) reshapes buttons, chips, badges, segmented, the tab bar, the
|
|
122
127
|
action bar and toggles at once:
|
|
123
128
|
|
|
@@ -138,16 +143,17 @@ Render library components only in the browser, for example in a `@defer` block o
|
|
|
138
143
|
route. Support will be decided before 1.0.
|
|
139
144
|
|
|
140
145
|
The one server-side check is a render smoke, not a support test: the library repository's
|
|
141
|
-
`tools/ssr-smoke.ts` renders
|
|
146
|
+
`tools/ssr-smoke.ts` renders 43 fixtures with `renderApplication()` (every root widget, `bf-icon`,
|
|
142
147
|
`bf-number-input`, a `/charts` line chart, a `/mobile` page with `provideBfShell()`, `bf-page`,
|
|
143
148
|
`bf-header` and `bf-content`, `bf-tabs` with `bf-stack-outlet` under the router, and an `/overlay`
|
|
144
|
-
dialog), and all of them render without an error. 27 of them render identically twice. The other
|
|
149
|
+
dialog), and all of them render without an error. 27 of them render identically twice. The other 16
|
|
145
150
|
number their ids differently on every render, from a module-level counter, so they could not
|
|
146
|
-
hydrate: `bf-calendar`, `bf-checkbox`, `bf-input`, `bf-radio-group`, `bf-segmented`,
|
|
147
|
-
`bf-sidebar`, `bf-tab-group`, `bf-table`, `bf-textarea`, `bf-theme-switch`,
|
|
148
|
-
`bf-line-chart`, `bf-number-input` and the `/overlay` dialog. Hydration itself is still
|
|
149
|
-
supported or tested, and more stands in its way: `/mobile` writes to `<html>` and adds a
|
|
150
|
-
probe to `<body>`, and an `/overlay` dialog renders outside the app root and marks it
|
|
151
|
+
hydrate: `bf-app-frame`, `bf-calendar`, `bf-checkbox`, `bf-input`, `bf-radio-group`, `bf-segmented`,
|
|
152
|
+
`bf-select`, `bf-sidebar`, `bf-tab-group`, `bf-table`, `bf-textarea`, `bf-theme-switch`,
|
|
153
|
+
`bf-toggle`, `bf-line-chart`, `bf-number-input` and the `/overlay` dialog. Hydration itself is still
|
|
154
|
+
not supported or tested, and more stands in its way: `/mobile` writes to `<html>` and adds a
|
|
155
|
+
keyboard probe to `<body>`, and an `/overlay` dialog renders outside the app root and marks it
|
|
156
|
+
inert.
|
|
151
157
|
|
|
152
158
|
## Keeping this guide in sync
|
|
153
159
|
|
|
@@ -335,8 +341,19 @@ protected readonly colorScheme = inject(BfColorSchemeService);
|
|
|
335
341
|
`bf-tabs`, `bf-back-button` and `bf-header` are Capacitor shell parts. A desktop web app (no
|
|
336
342
|
`provideBfShell()`) uses their desktop counterparts from `@bitforgehq/ui`:
|
|
337
343
|
|
|
338
|
-
- **
|
|
339
|
-
|
|
344
|
+
- **App frame:** wrap the whole app in `<bf-app-frame>` (never build the frame by hand): the
|
|
345
|
+
`bf-sidebar` in `bfSlot="sidebar"` (sticky, full height, from `md` up), and below the
|
|
346
|
+
breakpoint a header bar with `bfSlot="brand"`, `bfSlot="menu"` (a `bf-menu-button` of
|
|
347
|
+
`a[bfMenuItem]` links from the same destinations array) and `bfSlot="actions"` (the theme
|
|
348
|
+
menu); the router outlet is the content. It brings the skip link and the focusable main
|
|
349
|
+
landmark; `breakpoint` and `contentWidth` adjust it. Desktop only: never inside
|
|
350
|
+
`provideBfShell()`.
|
|
351
|
+
- **Sidebar collapse:** `<bf-sidebar-toggle>` (`/overlay`, icons in `bfSlot="collapse-icon"` and
|
|
352
|
+
`bfSlot="expand-icon"`) in the sidebar footer beside the icon-only theme menu; with
|
|
353
|
+
`provideBfSidebarState()` bind `[(collapsed)]="sidebarState.collapsed"`
|
|
354
|
+
(`BfSidebarStateService`) so the choice is remembered.
|
|
355
|
+
- **Navigation:** `bf-sidebar` (groups in `bf-nav-group`, `[(collapsed)]` for an icon rail) or
|
|
356
|
+
`bf-top-nav` for a few sections, holding
|
|
340
357
|
`<a bfNavItem routerLink="…" routerLinkActive ariaCurrentWhenActive="page">` links with an icon
|
|
341
358
|
in `bfSlot="icon"` and an optional `[badge]` count. The router sets `aria-current="page"` and
|
|
342
359
|
the item shows it. Drive the sidebar, a below-`md` `bf-menu-button` of `a[bfMenuItem]` links and,
|
|
@@ -354,8 +371,9 @@ protected readonly colorScheme = inject(BfColorSchemeService);
|
|
|
354
371
|
controls default to `sm`, and `status` is a polite live region for the result count.
|
|
355
372
|
- **Table:** `<bf-table [rows] [columns] [(sort)] selectable [(selection)]>` with
|
|
356
373
|
`ng-template[bfTableCell]` for rich cells and `bfSlot="empty"` for the empty state; the app
|
|
357
|
-
sorts the rows in a `computed()`, and a selection shows a `bf-action-bar`.
|
|
358
|
-
|
|
374
|
+
sorts the rows in a `computed()`, and a selection shows a `bf-action-bar`. `stack="narrow"`
|
|
375
|
+
(below 36rem of its own width) or `stack="coarse"` turns each row into a label/value tile with
|
|
376
|
+
a sort and select bar; in the Capacitor shell use `bf-list-item` rows instead.
|
|
359
377
|
- **Details:** `bf-description-list` of `div[bfDescriptionItem]` label/value pairs, and `bf-stat`
|
|
360
378
|
figure tiles in a grid; both work in both shells.
|
|
361
379
|
- **Cards in a grid:** the grid stretches each `bf-card` and its `bfSlot="footer"` sits on the
|
|
@@ -384,6 +402,12 @@ protected readonly colorScheme = inject(BfColorSchemeService);
|
|
|
384
402
|
`bf-sparkline` inside a `bf-chart-card` (title, period slot, loading, empty and error states),
|
|
385
403
|
themed by the `chart-1`…`chart-8` tokens. Every chart takes a `label` and offers its data as a
|
|
386
404
|
table.
|
|
405
|
+
- **Surfaces:** one surface style per level. A raised `bf-card` holds sunken tiles
|
|
406
|
+
(`bf-stat variant="tile"`), never an outlined surface (`variant="card"` or a `flat` card) inside
|
|
407
|
+
a raised one; next to raised cards on a dashboard, use raised cards.
|
|
408
|
+
- **Empty pages:** a 404, "no access" or "nothing here yet" page is a `bf-empty-state` with
|
|
409
|
+
`headingLevel` 1, centred in the content area, with the main action and an optional link.
|
|
410
|
+
- **Toolbar on a phone:** `bf-toolbar` keeps its status and actions together on the last row.
|
|
387
411
|
|
|
388
412
|
## Overlays (`@bitforgehq/ui/overlay`)
|
|
389
413
|
|
|
@@ -1230,6 +1254,7 @@ notes.
|
|
|
1230
1254
|
|
|
1231
1255
|
| Component | Entry point | Purpose |
|
|
1232
1256
|
| --- | --- | --- |
|
|
1257
|
+
| `bf-app-frame` | `@bitforgehq/ui` | Desktop app frame with skip link, sticky sidebar, narrow header and main content |
|
|
1233
1258
|
| `bf-back-button` | `@bitforgehq/ui/mobile` | Header back button that pops the page stack or falls back to a default route |
|
|
1234
1259
|
| `bf-breadcrumb` | `@bitforgehq/ui` | Desktop breadcrumb trail of links to the current page, with optional collapsing |
|
|
1235
1260
|
| `li[bfBreadcrumbItem]` | `@bitforgehq/ui` | One level of a breadcrumb trail: a link, or the current page as text |
|
|
@@ -1242,6 +1267,7 @@ notes.
|
|
|
1242
1267
|
| `bf-pagination` | `@bitforgehq/ui` | Previous, next and numbered page buttons for a long list or table |
|
|
1243
1268
|
| `bf-refresher` | `@bitforgehq/ui/mobile` | Pull-to-refresh indicator with haptic feedback, native iOS bounce and an Android-style pull |
|
|
1244
1269
|
| `bf-sidebar` | `@bitforgehq/ui` | Vertical app navigation with brand, grouped links, footer and an icon rail mode |
|
|
1270
|
+
| `bf-sidebar-toggle` | `@bitforgehq/ui/overlay` | Icon button inside a sidebar that collapses it to an icon rail and back |
|
|
1245
1271
|
| `bf-stack-outlet` | `@bitforgehq/ui/mobile` | Router outlet keeping a live page stack with slide transitions and iOS swipe back |
|
|
1246
1272
|
| `bf-tab` | `@bitforgehq/ui` | One tab bar destination with an icon, a label and an optional count badge |
|
|
1247
1273
|
| `bf-tab-bar` | `@bitforgehq/ui` | Floating pill tab bar for mobile apps with arrow-key selection and count badges |
|
|
@@ -2920,6 +2946,337 @@ focusable content. `provideBfShell()` now also provides `BfAlertDialogService`.
|
|
|
2920
2946
|
|
|
2921
2947
|
---
|
|
2922
2948
|
|
|
2949
|
+
# bf-app-frame
|
|
2950
|
+
|
|
2951
|
+
Desktop app frame with skip link, sticky sidebar, narrow header and main content
|
|
2952
|
+
|
|
2953
|
+
`BfAppFrameComponent` from `@bitforgehq/ui` · Navigation · since 0.3.2
|
|
2954
|
+
|
|
2955
|
+
## Example
|
|
2956
|
+
|
|
2957
|
+
```ts
|
|
2958
|
+
import { Component, signal } from '@angular/core';
|
|
2959
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
2960
|
+
import {
|
|
2961
|
+
BfAppFrameComponent,
|
|
2962
|
+
BfIconButtonComponent,
|
|
2963
|
+
BfNavItemComponent,
|
|
2964
|
+
BfSidebarComponent,
|
|
2965
|
+
} from '@bitforgehq/ui';
|
|
2966
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
2967
|
+
import {
|
|
2968
|
+
BfMenuButtonComponent,
|
|
2969
|
+
BfMenuComponent,
|
|
2970
|
+
BfMenuItemDirective,
|
|
2971
|
+
} from '@bitforgehq/ui/overlay';
|
|
2972
|
+
import { Bell, type IconNode, Layers, Menu, PanelLeft, PanelTop } from 'lucide';
|
|
2973
|
+
|
|
2974
|
+
/** One top-level destination of the app, shared by the sidebar and the narrow-screen menu. */
|
|
2975
|
+
interface Destination {
|
|
2976
|
+
readonly path: string;
|
|
2977
|
+
readonly label: string;
|
|
2978
|
+
readonly icon: IconNode;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* The bf-app-frame overview.
|
|
2983
|
+
*
|
|
2984
|
+
* Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the
|
|
2985
|
+
* `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`; the header also holds the
|
|
2986
|
+
* brand and a notifications action. Tab into the frame to reveal its skip link; Enter moves focus
|
|
2987
|
+
* to the content. The result line reports it.
|
|
2988
|
+
*/
|
|
2989
|
+
@Component({
|
|
2990
|
+
selector: 'app-example-app-frame-overview',
|
|
2991
|
+
imports: [
|
|
2992
|
+
RouterLink,
|
|
2993
|
+
RouterLinkActive,
|
|
2994
|
+
BfAppFrameComponent,
|
|
2995
|
+
BfIconButtonComponent,
|
|
2996
|
+
BfIconComponent,
|
|
2997
|
+
BfMenuButtonComponent,
|
|
2998
|
+
BfMenuComponent,
|
|
2999
|
+
BfMenuItemDirective,
|
|
3000
|
+
BfNavItemComponent,
|
|
3001
|
+
BfSidebarComponent,
|
|
3002
|
+
],
|
|
3003
|
+
template: `
|
|
3004
|
+
<!-- In an app the frame is the root of the page; here a fixed height stands in for the
|
|
3005
|
+
viewport, so the sidebar and the header bar stick inside the box. -->
|
|
3006
|
+
<div (focusin)="onFocus($event)">
|
|
3007
|
+
<bf-app-frame
|
|
3008
|
+
mainId="app-frame-demo-content"
|
|
3009
|
+
class="border-border rounded-card h-96 overflow-auto border"
|
|
3010
|
+
>
|
|
3011
|
+
<bf-sidebar bfSlot="sidebar" label="App frame demo">
|
|
3012
|
+
<span bfSlot="header" class="text-heading ps-3">Atlas</span>
|
|
3013
|
+
@for (destination of destinations; track destination.path) {
|
|
3014
|
+
<a
|
|
3015
|
+
bfNavItem
|
|
3016
|
+
[routerLink]="destination.path"
|
|
3017
|
+
routerLinkActive
|
|
3018
|
+
ariaCurrentWhenActive="page"
|
|
3019
|
+
>
|
|
3020
|
+
<bf-icon bfSlot="icon" [icon]="destination.icon" />
|
|
3021
|
+
{{ destination.label }}
|
|
3022
|
+
</a>
|
|
3023
|
+
}
|
|
3024
|
+
</bf-sidebar>
|
|
3025
|
+
|
|
3026
|
+
<span bfSlot="brand" class="text-heading">Atlas</span>
|
|
3027
|
+
<bf-menu-button bfSlot="menu" variant="text" [menu]="navMenu">
|
|
3028
|
+
<bf-icon bfSlot="icon-start" [icon]="menu" />
|
|
3029
|
+
Sections
|
|
3030
|
+
</bf-menu-button>
|
|
3031
|
+
<bf-icon-button
|
|
3032
|
+
bfSlot="actions"
|
|
3033
|
+
variant="text"
|
|
3034
|
+
label="Notifications"
|
|
3035
|
+
(clicked)="status.set('Notifications opened.')"
|
|
3036
|
+
>
|
|
3037
|
+
<bf-icon [icon]="bell" />
|
|
3038
|
+
</bf-icon-button>
|
|
3039
|
+
|
|
3040
|
+
<div class="flex flex-col gap-3">
|
|
3041
|
+
<h2 class="text-title">Inbox</h2>
|
|
3042
|
+
<p class="text-body text-text-muted max-w-prose">
|
|
3043
|
+
The page sits in the content column, beside the sidebar from the md breakpoint up and
|
|
3044
|
+
under the header bar below it.
|
|
3045
|
+
</p>
|
|
3046
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
3047
|
+
@for (row of rows; track row) {
|
|
3048
|
+
<div class="bg-surface rounded-tile text-body p-4">{{ row }}</div>
|
|
3049
|
+
}
|
|
3050
|
+
</div>
|
|
3051
|
+
</bf-app-frame>
|
|
3052
|
+
</div>
|
|
3053
|
+
|
|
3054
|
+
<ng-template #navMenu>
|
|
3055
|
+
<bf-menu>
|
|
3056
|
+
@for (destination of destinations; track destination.path) {
|
|
3057
|
+
<a
|
|
3058
|
+
bfMenuItem
|
|
3059
|
+
[routerLink]="destination.path"
|
|
3060
|
+
routerLinkActive
|
|
3061
|
+
ariaCurrentWhenActive="page"
|
|
3062
|
+
>{{ destination.label }}</a
|
|
3063
|
+
>
|
|
3064
|
+
}
|
|
3065
|
+
</bf-menu>
|
|
3066
|
+
</ng-template>
|
|
3067
|
+
`,
|
|
3068
|
+
})
|
|
3069
|
+
export class AppFrameOverviewExampleComponent {
|
|
3070
|
+
/** In an app, derive this from the route config, so a new route appears on every surface. */
|
|
3071
|
+
protected readonly destinations: readonly Destination[] = [
|
|
3072
|
+
{ path: '/components/app-frame', label: 'App frame', icon: PanelLeft },
|
|
3073
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: PanelTop },
|
|
3074
|
+
{ path: '/components/menu-button', label: 'Menu button', icon: Layers },
|
|
3075
|
+
];
|
|
3076
|
+
protected readonly rows = [
|
|
3077
|
+
'Review the brand brief',
|
|
3078
|
+
'Plan the launch',
|
|
3079
|
+
'Write the release notes',
|
|
3080
|
+
];
|
|
3081
|
+
protected readonly bell = Bell;
|
|
3082
|
+
protected readonly menu = Menu;
|
|
3083
|
+
readonly status = signal('Tab into the frame to reveal the skip link.');
|
|
3084
|
+
|
|
3085
|
+
/** Reports when focus lands on the content, as the skip link moves it there. */
|
|
3086
|
+
protected onFocus(event: FocusEvent): void {
|
|
3087
|
+
if (event.target instanceof HTMLElement && event.target.id === 'app-frame-demo-content') {
|
|
3088
|
+
this.status.set('Skipped to the content.');
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
```
|
|
3093
|
+
|
|
3094
|
+
## Overview
|
|
3095
|
+
|
|
3096
|
+
The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the
|
|
3097
|
+
breakpoint up, a header bar with the brand, the narrow-screen menu and actions below it, and
|
|
3098
|
+
the `main` landmark with a content column of a set maximum width.
|
|
3099
|
+
|
|
3100
|
+
The frame around a desktop web app's pages: a skip link, a [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar) column that sticks to the viewport at its full height from a breakpoint up, a header bar below the breakpoint with the brand, the narrow-screen [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) and end actions, and the `main` landmark with a content column of a set maximum width. It replaces the `hidden md:flex` sidebar, the `md:hidden` menu button and the padded `main` an app would otherwise build by hand. The frame imports no overlay and never reads the router: the menu is your own `bf-menu-button`, projected into a slot.
|
|
3101
|
+
|
|
3102
|
+
## When to use
|
|
3103
|
+
|
|
3104
|
+
Use `bf-app-frame` once, at the root of a desktop web app whose top-level destinations live in a `bf-sidebar`, with your `router-outlet` as its content. Use [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) in your own header instead for an app with a handful of sections and no sidebar. In the Capacitor shell use [bf-tabs](https://bitforge-ui.web.app/components/tabs) with a [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar), and a [bf-page](https://bitforge-ui.web.app/components/page) with a [bf-header](https://bitforge-ui.web.app/components/header) per page, instead; never put the frame inside [`provideBfShell()`](https://bitforge-ui.web.app/api/provide-bf-shell).
|
|
3105
|
+
|
|
3106
|
+
## Slots and layout
|
|
3107
|
+
|
|
3108
|
+
Put the `bf-sidebar` in `bfSlot="sidebar"`, the app's name or logo in `bfSlot="brand"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot="menu"` and end actions (the theme menu, the account) in `bfSlot="actions"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the brand at the start, then the menu, and the actions at the end. Keep the brand in the sidebar's own `header` slot too, for the wide layout.
|
|
3109
|
+
|
|
3110
|
+
## Breakpoint and content width
|
|
3111
|
+
|
|
3112
|
+
`breakpoint` (`BfAppFrameBreakpoint`) is the viewport width from which the sidebar shows: `md` (768 px, the default) or `lg` (1024 px) for a wide sidebar that would crowd a tablet. `contentWidth` (`BfAppFrameContentWidth`) caps the content column, centred in the space beside the sidebar: `md` 48rem, `lg` 64rem, `xl` 80rem (the default) and `full` for no limit. The content is padded 16 px below the breakpoint and 32 px from it up.
|
|
3113
|
+
|
|
3114
|
+
```ts
|
|
3115
|
+
import { Component } from '@angular/core';
|
|
3116
|
+
import { RouterOutlet } from '@angular/router';
|
|
3117
|
+
import { BfAppFrameComponent, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* A narrow reading column, with the sidebar from lg up.
|
|
3121
|
+
*
|
|
3122
|
+
* A settings or reading app keeps its lines short: `contentWidth="md"` caps the content column at
|
|
3123
|
+
* 48rem (`lg` 64rem, the default `xl` 80rem, `full` no limit), and `breakpoint="lg"` keeps the
|
|
3124
|
+
* header bar up to 1024 px, for a sidebar that would crowd a tablet. `mainId` names the main
|
|
3125
|
+
* landmark, the skip link's target.
|
|
3126
|
+
*
|
|
3127
|
+
* @preview none
|
|
3128
|
+
*/
|
|
3129
|
+
@Component({
|
|
3130
|
+
selector: 'app-example-app-frame-content-width',
|
|
3131
|
+
imports: [RouterOutlet, BfAppFrameComponent, BfNavItemComponent, BfSidebarComponent],
|
|
3132
|
+
template: `
|
|
3133
|
+
<bf-app-frame contentWidth="md" breakpoint="lg" mainId="content">
|
|
3134
|
+
<bf-sidebar bfSlot="sidebar" label="Settings">
|
|
3135
|
+
<a bfNavItem href="/account">Account</a>
|
|
3136
|
+
<a bfNavItem href="/billing">Billing</a>
|
|
3137
|
+
</bf-sidebar>
|
|
3138
|
+
<span bfSlot="brand" class="text-heading">Settings</span>
|
|
3139
|
+
<router-outlet />
|
|
3140
|
+
</bf-app-frame>
|
|
3141
|
+
`,
|
|
3142
|
+
})
|
|
3143
|
+
export class AppFrameContentWidthExampleComponent {}
|
|
3144
|
+
```
|
|
3145
|
+
|
|
3146
|
+
## Skip link and the main landmark
|
|
3147
|
+
|
|
3148
|
+
The first element in the tab order is a "Skip to main content" link (`provideBfLabels()`'s `skipToContent`), visually hidden until it has focus, then shown as a pill at the top inline-start corner. Activating it moves focus to the frame's main landmark (`tabindex="-1"`, its id from `mainId`, generated when left out) by script and cancels the link's navigation. That interception matters: under `<base href="/">`, which every Angular app has, a bare `#id` link resolves against the base, so following it would load the root URL instead of moving focus. The href stays for the link's semantics. Nested inside another `main` landmark (a docs preview, as on this page) the content drops its `main` role, so the document keeps exactly one.
|
|
3149
|
+
|
|
3150
|
+
## One array of destinations
|
|
3151
|
+
|
|
3152
|
+
Keep one array of destinations, ideally derived from your route config, and drive every surface from it: the sidebar's `a[bfNavItem]` links, the narrow menu's `a[bfMenuItem]` links and, in the Capacitor shell, the `bf-tab`s of a `bf-tab-bar` (where each destination's path is a child route of the tabs and the tab's id). The overview at the top of this page drives the sidebar and the menu from one array; the shell keeps `bf-tabs` and has no frame:
|
|
3153
|
+
|
|
3154
|
+
```ts
|
|
3155
|
+
import { Component } from '@angular/core';
|
|
3156
|
+
import { BfTabBarComponent, BfTabComponent } from '@bitforgehq/ui';
|
|
3157
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
3158
|
+
import { BfStackOutletComponent, BfTabsComponent } from '@bitforgehq/ui/mobile';
|
|
3159
|
+
import { Folder, type IconNode, Inbox, Settings } from 'lucide';
|
|
3160
|
+
|
|
3161
|
+
/** One top-level destination: `path` is the route under the tabs, and the tab's id. */
|
|
3162
|
+
interface Destination {
|
|
3163
|
+
readonly path: string;
|
|
3164
|
+
readonly label: string;
|
|
3165
|
+
readonly icon: IconNode;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* The same destinations in the Capacitor shell.
|
|
3170
|
+
*
|
|
3171
|
+
* Inside `provideBfShell()` there is no `bf-app-frame`: the array that feeds the frame's sidebar
|
|
3172
|
+
* and narrow menu drives the `bf-tab`s of a `bf-tab-bar` in `bf-tabs` instead, and each page
|
|
3173
|
+
* brings its own `bf-page` with a `bf-header`. Each destination's `path` is a child route of
|
|
3174
|
+
* `app` and the tab's id; on desktop the same array feeds `a[bfNavItem]` and `a[bfMenuItem]`
|
|
3175
|
+
* links (`routerLink="/app/{path}"`).
|
|
3176
|
+
*
|
|
3177
|
+
* @preview none
|
|
3178
|
+
*/
|
|
3179
|
+
@Component({
|
|
3180
|
+
selector: 'app-example-app-frame-shell',
|
|
3181
|
+
imports: [
|
|
3182
|
+
BfIconComponent,
|
|
3183
|
+
BfStackOutletComponent,
|
|
3184
|
+
BfTabBarComponent,
|
|
3185
|
+
BfTabComponent,
|
|
3186
|
+
BfTabsComponent,
|
|
3187
|
+
],
|
|
3188
|
+
template: `
|
|
3189
|
+
<bf-tabs tabsPath="app">
|
|
3190
|
+
<bf-stack-outlet />
|
|
3191
|
+
<bf-tab-bar>
|
|
3192
|
+
@for (destination of destinations; track destination.path) {
|
|
3193
|
+
<bf-tab [id]="destination.path" [label]="destination.label"
|
|
3194
|
+
><bf-icon [icon]="destination.icon"
|
|
3195
|
+
/></bf-tab>
|
|
3196
|
+
}
|
|
3197
|
+
</bf-tab-bar>
|
|
3198
|
+
</bf-tabs>
|
|
3199
|
+
`,
|
|
3200
|
+
})
|
|
3201
|
+
export class AppFrameShellExampleComponent {
|
|
3202
|
+
protected readonly destinations: readonly Destination[] = [
|
|
3203
|
+
{ path: 'inbox', label: 'Inbox', icon: Inbox },
|
|
3204
|
+
{ path: 'projects', label: 'Projects', icon: Folder },
|
|
3205
|
+
{ path: 'settings', label: 'Settings', icon: Settings },
|
|
3206
|
+
];
|
|
3207
|
+
}
|
|
3208
|
+
```
|
|
3209
|
+
|
|
3210
|
+
## Desktop
|
|
3211
|
+
|
|
3212
|
+
The frame is a desktop pattern. The skip link shows its focus ring on keyboard focus only (`focus-visible`); it stays out of sight for pointer users, who never focus it. The sidebar column keeps its height while the page scrolls under the pointer, and the narrow header bar stays at the top. The breakpoint follows the viewport, not a container, so a frame inside a smaller box (as in the preview above) still switches at the viewport's width. Under a coarse pointer (a touchscreen laptop) the sidebar's items and the projected controls take their own touch heights; the frame adds none.
|
|
3213
|
+
|
|
3214
|
+
## Theming
|
|
3215
|
+
|
|
3216
|
+
- Frame: `bg` background, `text` colour.
|
|
3217
|
+
- Narrow header bar: `surface` with a `border` line at the bottom, at least 56 px tall.
|
|
3218
|
+
- Skip link: `surface` background, `text`, `shadow-floating`, the `control` (pill) radius and the `ring` focus ring.
|
|
3219
|
+
- Content: 16 px padding below the breakpoint, 32 px from it up; `max-w-3xl`, `max-w-5xl`, `max-w-7xl` or no cap by `contentWidth`.
|
|
3220
|
+
- The sidebar's own tokens are on the [sidebar](https://bitforge-ui.web.app/components/sidebar#theming) page.
|
|
3221
|
+
|
|
3222
|
+
## Accessibility
|
|
3223
|
+
|
|
3224
|
+
The skip link is a native `<a>` with an href to the main landmark and is the first focusable element, so a keyboard user reaches the content in one step; it moves focus by script (see above). The content is a `role="main"` landmark with `tabindex="-1"`, so it takes focus without joining the tab order, and drops the role inside another `main`. The narrow header bar is a native `header` (the banner landmark at the root of a page). Name the `bf-sidebar` by `label` and the menu button by its text; the frame names nothing else itself. Hidden regions use `display: none`, so the sidebar below the breakpoint and the header bar from it up are out of the accessibility tree too.
|
|
3225
|
+
|
|
3226
|
+
### Keyboard
|
|
3227
|
+
|
|
3228
|
+
| Key | Action |
|
|
3229
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
3230
|
+
| <kbd>Tab</kbd> | From the top of the page, focuses the skip link first and reveals it |
|
|
3231
|
+
| <kbd>Enter</kbd> | On the skip link, moves focus to the main content |
|
|
3232
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the sidebar or the header bar, then the page, in that order |
|
|
3233
|
+
|
|
3234
|
+
## Inputs
|
|
3235
|
+
|
|
3236
|
+
| Name | Type | Default | Description |
|
|
3237
|
+
| --- | --- | --- | --- |
|
|
3238
|
+
| `breakpoint` | `BfAppFrameBreakpoint` (`'md' \| 'lg'`) | `'md'` | The viewport width from which the sidebar column replaces the narrow header bar. |
|
|
3239
|
+
| `contentWidth` | `BfAppFrameContentWidth` (`'md' \| 'lg' \| 'xl' \| 'full'`) | `'xl'` | The maximum width of the content column (`md` 48rem, `lg` 64rem, `xl` 80rem, `full`). |
|
|
3240
|
+
| `mainId` | `string` | `generated` | The id of the `main` landmark, the skip link's target; generated when left out. |
|
|
3241
|
+
|
|
3242
|
+
## Slots
|
|
3243
|
+
|
|
3244
|
+
- `sidebar`: The `bf-sidebar`, shown from the breakpoint up; it fills the column's height.
|
|
3245
|
+
- `brand`: The app's name or logo in the narrow header bar.
|
|
3246
|
+
- `menu`: The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links.
|
|
3247
|
+
- `actions`: End actions of the narrow header bar, such as the theme menu.
|
|
3248
|
+
- `default`: The page: what the router shows, inside the `main` landmark.
|
|
3249
|
+
|
|
3250
|
+
## Notes
|
|
3251
|
+
|
|
3252
|
+
### Do
|
|
3253
|
+
|
|
3254
|
+
- Put the frame at the root of the app, with the `router-outlet` as its content, so the sidebar stays put between pages.
|
|
3255
|
+
- Drive the sidebar, the narrow menu and the shell's tab bar from one array of destinations.
|
|
3256
|
+
- Keep one `h1` per page in the content; the frame renders no heading.
|
|
3257
|
+
|
|
3258
|
+
### Don't
|
|
3259
|
+
|
|
3260
|
+
- Don't use `bf-app-frame` inside the Capacitor shell; use `bf-tabs` with a `bf-tab-bar` and `bf-page` there.
|
|
3261
|
+
- Don't add your own skip link or hide the `bf-sidebar` with `hidden md:flex` inside the frame; the frame does both.
|
|
3262
|
+
|
|
3263
|
+
## Migration notes
|
|
3264
|
+
|
|
3265
|
+
### 0.3.2
|
|
3266
|
+
|
|
3267
|
+
New `bf-app-frame` in `@bitforgehq/ui`: the frame of a desktop web app. Slots `sidebar` (a
|
|
3268
|
+
`bf-sidebar`, in a column that sticks to the viewport at full height from the breakpoint up),
|
|
3269
|
+
`brand`, `menu` (the narrow-screen `bf-menu-button`) and `actions` (the header bar shown below
|
|
3270
|
+
the breakpoint) and the default slot (the page, in the `main` landmark). `breakpoint`
|
|
3271
|
+
(`BfAppFrameBreakpoint`: `'md'` default, `'lg'`), `contentWidth` (`BfAppFrameContentWidth`:
|
|
3272
|
+
`'md'` 48rem, `'lg'` 64rem, `'xl'` 80rem default, `'full'`) and `mainId`. A skip link, first in
|
|
3273
|
+
the tab order and hidden until focused, moves focus to the main landmark by script, so it works
|
|
3274
|
+
under `<base href="/">`; new label key `skipToContent` ("Skip to main content"). Inside another
|
|
3275
|
+
`main` landmark the content drops its `main` role. Desktop only: the Capacitor shell keeps
|
|
3276
|
+
`bf-tabs`.
|
|
3277
|
+
|
|
3278
|
+
---
|
|
3279
|
+
|
|
2923
3280
|
# bf-avatar
|
|
2924
3281
|
|
|
2925
3282
|
Round avatar showing a photo or initials, in three sizes and four color variants
|
|
@@ -5078,7 +5435,7 @@ Surface for a piece of content: a project summary, a stat, a focus prompt. Four
|
|
|
5078
5435
|
the design: raised (default), tint, ink and flat. To make the whole card clickable, put a
|
|
5079
5436
|
`bfAction` link or button in its heading (the stretched-link pattern).
|
|
5080
5437
|
|
|
5081
|
-
Cards group a piece of content on the canvas. The raised card is the workhorse; tint highlights a stat or a callout; ink is the dark focus card; flat
|
|
5438
|
+
Cards group a piece of content on the canvas. The raised card is the workhorse; tint highlights a stat or a callout; ink is the dark focus card; flat is the outlined card for a page level that should stay quiet, such as a dense grid of cards.
|
|
5082
5439
|
|
|
5083
5440
|
## When to use
|
|
5084
5441
|
|
|
@@ -5131,6 +5488,53 @@ import { BfCardComponent } from '@bitforgehq/ui';
|
|
|
5131
5488
|
export class CardVariantsExampleComponent {}
|
|
5132
5489
|
```
|
|
5133
5490
|
|
|
5491
|
+
## Nesting surfaces
|
|
5492
|
+
|
|
5493
|
+
Each level of a page takes one surface style, and a level inside a card steps down, never up:
|
|
5494
|
+
|
|
5495
|
+
- **Raised card, then sunken tile.** Inside a raised card, group figures or blocks on sunken, borderless tiles: [bf-stat](https://bitforge-ui.web.app/components/stat) `variant="tile"`, or your own block on `bg-surface-sunken` with the `card` radius.
|
|
5496
|
+
- **Never an outlined surface inside a raised one.** A `flat` card or a `card` stat inside a raised card draws a border on top of a shadow, two edges for one level; use a tile instead. Don't nest raised cards either.
|
|
5497
|
+
- **One surface style per level of a dashboard.** All cards on the page are raised (or all flat), and all tiles in them are sunken; mixing raised and flat cards side by side, or tiles and outlined stats in one card, reads as different kinds of content. The [bf-stat dashboard example](https://bitforge-ui.web.app/components/stat) puts raised cards on the page and sunken tiles in each.
|
|
5498
|
+
|
|
5499
|
+
```ts
|
|
5500
|
+
import { Component } from '@angular/core';
|
|
5501
|
+
import { BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
|
|
5502
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
5503
|
+
import { CircleCheck, Clock, ListTodo } from 'lucide';
|
|
5504
|
+
|
|
5505
|
+
/**
|
|
5506
|
+
* A raised card holding a row of sunken tiles.
|
|
5507
|
+
*
|
|
5508
|
+
* One surface per level: the card is raised on the page, and the stats inside it are sunken,
|
|
5509
|
+
* borderless tiles (`bf-stat variant="tile"`), never outlined surfaces.
|
|
5510
|
+
*/
|
|
5511
|
+
@Component({
|
|
5512
|
+
selector: 'app-example-card-nesting',
|
|
5513
|
+
imports: [BfCardComponent, BfIconComponent, BfStatComponent],
|
|
5514
|
+
template: `
|
|
5515
|
+
<bf-card padding="lg">
|
|
5516
|
+
<h3 class="text-heading">This week</h3>
|
|
5517
|
+
<div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
5518
|
+
<bf-stat label="Open tasks" value="14" variant="tile">
|
|
5519
|
+
<bf-icon bfSlot="icon" [icon]="listTodo" />
|
|
5520
|
+
</bf-stat>
|
|
5521
|
+
<bf-stat label="Done" value="23" variant="tile">
|
|
5522
|
+
<bf-icon bfSlot="icon" [icon]="circleCheck" />
|
|
5523
|
+
</bf-stat>
|
|
5524
|
+
<bf-stat label="Time logged" value="31 h" variant="tile">
|
|
5525
|
+
<bf-icon bfSlot="icon" [icon]="clock" />
|
|
5526
|
+
</bf-stat>
|
|
5527
|
+
</div>
|
|
5528
|
+
</bf-card>
|
|
5529
|
+
`,
|
|
5530
|
+
})
|
|
5531
|
+
export class CardNestingExampleComponent {
|
|
5532
|
+
protected readonly listTodo = ListTodo;
|
|
5533
|
+
protected readonly circleCheck = CircleCheck;
|
|
5534
|
+
protected readonly clock = Clock;
|
|
5535
|
+
}
|
|
5536
|
+
```
|
|
5537
|
+
|
|
5134
5538
|
## Footer
|
|
5135
5539
|
|
|
5136
5540
|
Content in `bfSlot="footer"` sits on the card's bottom edge: it takes the free space above it, so in a grid of cards (a `bf-card` fills its grid cell, `h-full`) every footer lines up, however differently the descriptions wrap, and the body keeps its 12 px gap above the footer. Put the footer's parts in one element (`<div bfSlot="footer" class="flex …">`), so they move together.
|
|
@@ -5302,13 +5706,20 @@ The card renders a plain `<div>` with no role. A clickable card follows the stre
|
|
|
5302
5706
|
|
|
5303
5707
|
### Don't
|
|
5304
5708
|
|
|
5305
|
-
- Don't nest raised cards; use `
|
|
5709
|
+
- Don't nest raised cards, and don't put an outlined surface (`flat`, a `card` stat) inside a raised one; use sunken tiles (`bf-stat variant="tile"`) inside.
|
|
5710
|
+
- Don't mix surface styles on one level of a dashboard; keep one per level.
|
|
5306
5711
|
- Don't use `interactive` (deprecated) or wrap a card in an `<a>`; both hide the heading and nest interactive content.
|
|
5307
5712
|
- Don't put more than one `bfAction` in a card.
|
|
5308
5713
|
- Don't put a `text` button on an ink card — its primary colour fails contrast on ink; use `tonal` or `outline`.
|
|
5309
5714
|
|
|
5310
5715
|
## Migration notes
|
|
5311
5716
|
|
|
5717
|
+
### 0.3.2
|
|
5718
|
+
|
|
5719
|
+
New "Nesting surfaces" docs section: a raised card holds sunken tiles, never an outlined
|
|
5720
|
+
surface (`flat`, a `card` stat), and each level of a dashboard keeps one surface style. New
|
|
5721
|
+
`card/nesting` example. No code change.
|
|
5722
|
+
|
|
5312
5723
|
### 0.3.1
|
|
5313
5724
|
|
|
5314
5725
|
The footer (`bfSlot="footer"`) sits on the card's bottom edge (`mt-auto`), so in a grid of cards
|
|
@@ -10011,6 +10422,89 @@ export class EmptyStateActionExampleComponent {
|
|
|
10011
10422
|
|
|
10012
10423
|
Set `headingLevel` to `3` when the empty state sits inside a section that already opened with an `h2`, so the page outline stays in order.
|
|
10013
10424
|
|
|
10425
|
+
## As the page
|
|
10426
|
+
|
|
10427
|
+
When the empty state is the whole page (a not-found page, a no-access page, an app's first run), its title is the page's heading: set `headingLevel` to `1` so it renders the `h1`, and give the page no other `h1`. Centre it in the content area: a column with a minimum height (`min-h-[60vh]`) and `justify-center`. Put the primary action first in the `actions` slot (a `bfButton` link home) and an optional secondary link after it (a `text` `bfButton`, such as "Contact support").
|
|
10428
|
+
|
|
10429
|
+
The preview below sits inside this docs page, which already has its `h1`, so it keeps `headingLevel` 2 in a shorter area; a real page uses `1`, as in the code-only example after it.
|
|
10430
|
+
|
|
10431
|
+
```ts
|
|
10432
|
+
import { Component } from '@angular/core';
|
|
10433
|
+
import { RouterLink } from '@angular/router';
|
|
10434
|
+
import { BfButtonComponent, BfEmptyStateComponent } from '@bitforgehq/ui';
|
|
10435
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
10436
|
+
import { FileQuestionMark } from 'lucide';
|
|
10437
|
+
|
|
10438
|
+
/**
|
|
10439
|
+
* An empty state as a whole page.
|
|
10440
|
+
*
|
|
10441
|
+
* Centred in a tall content area, with a primary action and a secondary link. This preview sits
|
|
10442
|
+
* inside a docs page that has its own `h1`, so it uses `headingLevel` 2; on a real not-found page
|
|
10443
|
+
* the empty state is the page, so set it to 1 (see the next example).
|
|
10444
|
+
*/
|
|
10445
|
+
@Component({
|
|
10446
|
+
selector: 'app-example-empty-state-page',
|
|
10447
|
+
imports: [RouterLink, BfButtonComponent, BfEmptyStateComponent, BfIconComponent],
|
|
10448
|
+
template: `
|
|
10449
|
+
<div class="flex min-h-96 flex-col justify-center">
|
|
10450
|
+
<bf-empty-state
|
|
10451
|
+
title="Page not found"
|
|
10452
|
+
description="The page may have moved, or the link has a typo."
|
|
10453
|
+
[headingLevel]="2"
|
|
10454
|
+
>
|
|
10455
|
+
<bf-icon bfSlot="icon" [icon]="fileQuestion" size="lg" />
|
|
10456
|
+
<a bfSlot="actions" bfButton routerLink="/">Go to the home page</a>
|
|
10457
|
+
<a bfSlot="actions" bfButton variant="text" href="mailto:support@example.com">
|
|
10458
|
+
Contact support
|
|
10459
|
+
</a>
|
|
10460
|
+
</bf-empty-state>
|
|
10461
|
+
</div>
|
|
10462
|
+
`,
|
|
10463
|
+
})
|
|
10464
|
+
export class EmptyStatePageExampleComponent {
|
|
10465
|
+
protected readonly fileQuestion = FileQuestionMark;
|
|
10466
|
+
}
|
|
10467
|
+
```
|
|
10468
|
+
|
|
10469
|
+
```ts
|
|
10470
|
+
import { Component } from '@angular/core';
|
|
10471
|
+
import { RouterLink } from '@angular/router';
|
|
10472
|
+
import { BfButtonComponent, BfEmptyStateComponent } from '@bitforgehq/ui';
|
|
10473
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
10474
|
+
import { FileQuestionMark } from 'lucide';
|
|
10475
|
+
|
|
10476
|
+
/**
|
|
10477
|
+
* A not-found page whose empty state is the page heading.
|
|
10478
|
+
*
|
|
10479
|
+
* The route's whole content: `headingLevel` 1 renders the title as the page's `h1`, and the
|
|
10480
|
+
* `min-h-[60vh]` area centres the empty state vertically in the content column.
|
|
10481
|
+
*
|
|
10482
|
+
* @preview none
|
|
10483
|
+
*/
|
|
10484
|
+
@Component({
|
|
10485
|
+
selector: 'app-example-empty-state-not-found-page',
|
|
10486
|
+
imports: [RouterLink, BfButtonComponent, BfEmptyStateComponent, BfIconComponent],
|
|
10487
|
+
template: `
|
|
10488
|
+
<div class="flex min-h-[60vh] flex-col justify-center">
|
|
10489
|
+
<bf-empty-state
|
|
10490
|
+
title="Page not found"
|
|
10491
|
+
description="The page may have moved, or the link has a typo."
|
|
10492
|
+
[headingLevel]="1"
|
|
10493
|
+
>
|
|
10494
|
+
<bf-icon bfSlot="icon" [icon]="fileQuestion" size="lg" />
|
|
10495
|
+
<a bfSlot="actions" bfButton routerLink="/">Go to the home page</a>
|
|
10496
|
+
<a bfSlot="actions" bfButton variant="text" href="mailto:support@example.com">
|
|
10497
|
+
Contact support
|
|
10498
|
+
</a>
|
|
10499
|
+
</bf-empty-state>
|
|
10500
|
+
</div>
|
|
10501
|
+
`,
|
|
10502
|
+
})
|
|
10503
|
+
export class EmptyStateNotFoundPageExampleComponent {
|
|
10504
|
+
protected readonly fileQuestion = FileQuestionMark;
|
|
10505
|
+
}
|
|
10506
|
+
```
|
|
10507
|
+
|
|
10014
10508
|
## Theming
|
|
10015
10509
|
|
|
10016
10510
|
- Icon tile: `surface-sunken` background, `text-muted` icon color, `tile` radius.
|
|
@@ -10018,7 +10512,7 @@ Set `headingLevel` to `3` when the empty state sits inside a section that alread
|
|
|
10018
10512
|
|
|
10019
10513
|
## Accessibility
|
|
10020
10514
|
|
|
10021
|
-
The title renders as an `<h2>` by default
|
|
10515
|
+
The title renders as an `<h2>` by default, an `<h3>` when `headingLevel` is `3` and an `<h1>` when it is `1` (only when the empty state is the page and the page has no other `h1`), so it slots into the surrounding page outline instead of always claiming the top level. The icon and action slots carry their own accessibility — an icon should stay decorative, and the action needs its own accessible name.
|
|
10022
10516
|
|
|
10023
10517
|
## Inputs
|
|
10024
10518
|
|
|
@@ -10026,7 +10520,7 @@ The title renders as an `<h2>` by default and an `<h3>` when `headingLevel` is `
|
|
|
10026
10520
|
| --- | --- | --- | --- |
|
|
10027
10521
|
| `title` | `string` | required | What is missing, as a short title. |
|
|
10028
10522
|
| `description` | `string` | `''` | One sentence on how to fill it. |
|
|
10029
|
-
| `headingLevel` | `
|
|
10523
|
+
| `headingLevel` | `BfEmptyStateHeading` (`2 \| 3 \| 1`) | `2` | Heading level of the title so the empty state fits the page outline; 1 when it is the page. |
|
|
10030
10524
|
|
|
10031
10525
|
## Slots
|
|
10032
10526
|
|
|
@@ -10042,10 +10536,20 @@ The title renders as an `<h2>` by default and an `<h3>` when `headingLevel` is `
|
|
|
10042
10536
|
|
|
10043
10537
|
### Don't
|
|
10044
10538
|
|
|
10539
|
+
- Don't set `headingLevel` to `1` on an empty state inside a page that already has its own `h1`.
|
|
10045
10540
|
- Don't use `bf-empty-state` for a loading screen; that is `bf-spinner`.
|
|
10046
10541
|
|
|
10047
10542
|
## Migration notes
|
|
10048
10543
|
|
|
10544
|
+
### 0.3.2
|
|
10545
|
+
|
|
10546
|
+
`headingLevel` now takes the new named type `BfEmptyStateHeading` (`BfEmptyStateHeadingLevel |
|
|
10547
|
+
1`): `1` renders the title as the page's `h1`, for an empty state that is the page (a not-found
|
|
10548
|
+
or no-access page). The released `BfEmptyStateHeadingLevel` keeps its two members, so `Record`s
|
|
10549
|
+
over it keep compiling. New "As the page" docs section with the `empty-state/page` example
|
|
10550
|
+
(centred in a tall area, a primary action and a secondary link) and the code-only
|
|
10551
|
+
`empty-state/not-found-page`.
|
|
10552
|
+
|
|
10049
10553
|
### 0.3.0
|
|
10050
10554
|
|
|
10051
10555
|
New exported `BfEmptyStateHeadingLevel` (`2 | 3`), the type of `headingLevel` (was an inline union
|
|
@@ -10992,7 +11496,7 @@ inside one.
|
|
|
10992
11496
|
|
|
10993
11497
|
## Desktop
|
|
10994
11498
|
|
|
10995
|
-
`bf-header` is part of the Capacitor shell (`provideBfShell()`), shaped like an iOS or Material app bar. On a desktop web page use [bf-page-header](https://bitforge-ui.web.app/components/page-header): the page's one `h1` (`title`) with an optional `subtitle`, badges in `meta`, and the actions in `actions` (the main action as a primary [bf-button](https://bitforge-ui.web.app/components/button), secondary ones as `ghost` or `outline` buttons, the rest in an icon-only [bf-menu-button](https://bitforge-ui.web.app/components/menu-button) labelled "More actions"); the actions wrap under the title in a narrow window. On a nested page, put a [bf-breadcrumb](https://bitforge-ui.web.app/components/breadcrumb) in its `breadcrumb` slot, and a [bf-toolbar](https://bitforge-ui.web.app/components/toolbar) under it for search and filters. The two are siblings rather than one shell-aware widget: the large title and the scroll-under surface are phone patterns that depend on `bf-content`.
|
|
11499
|
+
`bf-header` is part of the Capacitor shell (`provideBfShell()`), shaped like an iOS or Material app bar. On a desktop web page use [bf-page-header](https://bitforge-ui.web.app/components/page-header): the page's one `h1` (`title`) with an optional `subtitle`, badges in `meta`, and the actions in `actions` (the main action as a primary [bf-button](https://bitforge-ui.web.app/components/button), secondary ones as `ghost` or `outline` buttons, the rest in an icon-only [bf-menu-button](https://bitforge-ui.web.app/components/menu-button) labelled "More actions"); the actions wrap under the title in a narrow window. On a nested page, put a [bf-breadcrumb](https://bitforge-ui.web.app/components/breadcrumb) in its `breadcrumb` slot, and a [bf-toolbar](https://bitforge-ui.web.app/components/toolbar) under it for search and filters. Below the breakpoint, [bf-app-frame](https://bitforge-ui.web.app/components/app-frame) supplies the app-level header bar (brand, navigation menu, actions) above the page header. The two are siblings rather than one shell-aware widget: the large title and the scroll-under surface are phone patterns that depend on `bf-content`.
|
|
10996
11500
|
|
|
10997
11501
|
```ts
|
|
10998
11502
|
import { Component, signal } from '@angular/core';
|
|
@@ -11132,6 +11636,10 @@ The title is not in the tab order; after a push, `bf-stack-outlet` focuses it wi
|
|
|
11132
11636
|
|
|
11133
11637
|
## Migration notes
|
|
11134
11638
|
|
|
11639
|
+
### 0.3.2
|
|
11640
|
+
|
|
11641
|
+
The `## Desktop` docs say `bf-app-frame` supplies the app-level header bar below its breakpoint.
|
|
11642
|
+
|
|
11135
11643
|
### 0.3.1
|
|
11136
11644
|
|
|
11137
11645
|
The `## Desktop` docs and When to use name the desktop replacement, `bf-page-header` (with
|
|
@@ -20212,9 +20720,9 @@ export class SidebarOverviewExampleComponent {
|
|
|
20212
20720
|
|
|
20213
20721
|
The vertical navigation of a desktop app: a `nav` landmark with a `header` slot (the brand),
|
|
20214
20722
|
the destinations (`a[bfNavItem]` links, optionally in `bf-nav-group`s) and a `footer` slot.
|
|
20215
|
-
`collapsed` turns it into an icon rail
|
|
20216
|
-
`[(collapsed)]
|
|
20217
|
-
`bf-menu-button` from the same routes.
|
|
20723
|
+
`collapsed` turns it into an icon rail: put a `bf-sidebar-toggle` (`@bitforgehq/ui/overlay`) in
|
|
20724
|
+
it, or bind `[(collapsed)]` to your own button. Below a breakpoint hide it
|
|
20725
|
+
(`class="hidden md:flex"`) and drive a `bf-menu-button` from the same routes.
|
|
20218
20726
|
|
|
20219
20727
|
The vertical navigation of a desktop web app: a `nav` landmark down the inline-start edge with a `header` slot for the brand, the destinations as [`a[bfNavItem]`](https://bitforge-ui.web.app/components/nav-item) links (optionally in titled [`bf-nav-group`](https://bitforge-ui.web.app/components/nav-group)s) and a `footer` slot for settings or the account. `collapsed` turns it into an icon rail. Its horizontal sibling for apps with few sections is [`bf-top-nav`](https://bitforge-ui.web.app/components/top-nav); both share the same item, so the current page, the badge and the focus ring look the same in either.
|
|
20220
20728
|
|
|
@@ -20228,11 +20736,178 @@ Put `bfNavItem` on your own `<a routerLink routerLinkActive ariaCurrentWhenActiv
|
|
|
20228
20736
|
|
|
20229
20737
|
## Collapsing to an icon rail
|
|
20230
20738
|
|
|
20231
|
-
`collapsed` is a model
|
|
20739
|
+
`collapsed` is a model. Put a [`bf-sidebar-toggle`](https://bitforge-ui.web.app/components/sidebar-toggle) (`@bitforgehq/ui/overlay`) in the header or footer: it flips the model, names itself by what the next press does ("Collapse sidebar", "Expand sidebar") and takes its two icons as slots, since a widget never imports one. Bind `[(collapsed)]` to keep the state in your own signal, or bind it to your own button outside the sidebar. Collapsed, every item shows only its icon, centred in a square; its label stays in the DOM as screen-reader text, so its accessible name does not change, and becomes the link's `title`, the tooltip a pointer user sees on hover. A count badge becomes a small dot that is still announced, and the group headings are hidden visually but still name their groups. The width follows the rail (`w-auto`); expanded the sidebar is `w-60` (240 px).
|
|
20740
|
+
|
|
20741
|
+
## Remember the collapsed sidebar
|
|
20742
|
+
|
|
20743
|
+
`BfSidebarStateService` (from `@bitforgehq/ui`) holds a `collapsed` signal: bind the sidebar's `[(collapsed)]` to it (`sidebarState.collapsed`) and add `provideBfSidebarState()` to the app's providers, and every change is stored under `bf-sidebar-collapsed` (`localStorage` in the browser, `BF_SIDEBAR_STATE_STORAGE` to keep it elsewhere) and read back at the next start, so the app opens as the user left it. Without the provider the signal still holds the state but nothing is stored; on the server it starts expanded and reads nothing. The widget itself injects no service.
|
|
20744
|
+
|
|
20745
|
+
```ts
|
|
20746
|
+
import { Component, inject } from '@angular/core';
|
|
20747
|
+
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
20748
|
+
import { BfNavItemComponent, BfSidebarComponent, BfSidebarStateService } from '@bitforgehq/ui';
|
|
20749
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
20750
|
+
import { BfSidebarToggleComponent } from '@bitforgehq/ui/overlay';
|
|
20751
|
+
import { Inbox, PanelLeftClose, PanelLeftOpen } from 'lucide';
|
|
20752
|
+
|
|
20753
|
+
/**
|
|
20754
|
+
* A sidebar that opens collapsed or expanded, as the user left it.
|
|
20755
|
+
*
|
|
20756
|
+
* `[(collapsed)]` binds `BfSidebarStateService.collapsed`; with `provideBfSidebarState()` in the
|
|
20757
|
+
* app's providers every change is stored under `bf-sidebar-collapsed` (`localStorage` in the
|
|
20758
|
+
* browser) and read back at the next start.
|
|
20759
|
+
*
|
|
20760
|
+
* @preview none
|
|
20761
|
+
*/
|
|
20762
|
+
@Component({
|
|
20763
|
+
selector: 'app-example-sidebar-remembered',
|
|
20764
|
+
imports: [
|
|
20765
|
+
RouterLink,
|
|
20766
|
+
RouterLinkActive,
|
|
20767
|
+
RouterOutlet,
|
|
20768
|
+
BfIconComponent,
|
|
20769
|
+
BfNavItemComponent,
|
|
20770
|
+
BfSidebarComponent,
|
|
20771
|
+
BfSidebarToggleComponent,
|
|
20772
|
+
],
|
|
20773
|
+
template: `
|
|
20774
|
+
<div class="flex min-h-dvh">
|
|
20775
|
+
<bf-sidebar [(collapsed)]="sidebarState.collapsed" class="sticky top-0 h-dvh">
|
|
20776
|
+
<a bfNavItem routerLink="/inbox" routerLinkActive ariaCurrentWhenActive="page">
|
|
20777
|
+
<bf-icon bfSlot="icon" [icon]="inbox" />
|
|
20778
|
+
Inbox
|
|
20779
|
+
</a>
|
|
20780
|
+
<bf-sidebar-toggle bfSlot="footer">
|
|
20781
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
20782
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
20783
|
+
</bf-sidebar-toggle>
|
|
20784
|
+
</bf-sidebar>
|
|
20785
|
+
<main class="min-w-0 flex-1 p-6"><router-outlet /></main>
|
|
20786
|
+
</div>
|
|
20787
|
+
`,
|
|
20788
|
+
})
|
|
20789
|
+
export class SidebarRememberedExampleComponent {
|
|
20790
|
+
// app.config.ts: providers: [provideBfSidebarState()]
|
|
20791
|
+
protected readonly sidebarState = inject(BfSidebarStateService);
|
|
20792
|
+
protected readonly inbox = Inbox;
|
|
20793
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
20794
|
+
protected readonly panelClose = PanelLeftClose;
|
|
20795
|
+
}
|
|
20796
|
+
```
|
|
20797
|
+
|
|
20798
|
+
## Footer
|
|
20799
|
+
|
|
20800
|
+
The footer slot takes several elements, stacked with a 4 px gap: a Settings link as an `a[bfNavItem]`, a row holding an icon-only text [`bf-theme-menu-button`](https://bitforge-ui.web.app/components/theme-menu-button) and the `bf-sidebar-toggle`, and an optional version line in `text-caption text-text-muted`. Expanded, the row puts the two buttons at either end (`flex items-center justify-between`). As an icon rail every footer element is centred: switch the row to a column (`flex flex-col items-center gap-1`) so both buttons fit the rail, and hide the version line (`[class.hidden]="collapsed()"`), which has no icon to shrink to. Open the theme menu upwards (`placement="above-start"`), since the footer sits at the bottom of the screen.
|
|
20801
|
+
|
|
20802
|
+
```ts
|
|
20803
|
+
import { Component, computed, signal } from '@angular/core';
|
|
20804
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
20805
|
+
import { type BfColorScheme, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
20806
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
20807
|
+
import { BfSidebarToggleComponent, BfThemeMenuButtonComponent } from '@bitforgehq/ui/overlay';
|
|
20808
|
+
import {
|
|
20809
|
+
Inbox,
|
|
20810
|
+
type IconNode,
|
|
20811
|
+
Moon,
|
|
20812
|
+
PanelLeftClose,
|
|
20813
|
+
PanelLeftOpen,
|
|
20814
|
+
Settings,
|
|
20815
|
+
Sun,
|
|
20816
|
+
SunMoon,
|
|
20817
|
+
Users,
|
|
20818
|
+
} from 'lucide';
|
|
20819
|
+
|
|
20820
|
+
interface NavLink {
|
|
20821
|
+
readonly path: string;
|
|
20822
|
+
readonly label: string;
|
|
20823
|
+
readonly icon: IconNode;
|
|
20824
|
+
}
|
|
20825
|
+
|
|
20826
|
+
/**
|
|
20827
|
+
* A sidebar footer: settings, theme, toggle and version.
|
|
20828
|
+
*
|
|
20829
|
+
* The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
|
|
20830
|
+
* `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
|
|
20831
|
+
* buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
|
|
20832
|
+
* line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
|
|
20833
|
+
* `BfColorSchemeService.scheme`.
|
|
20834
|
+
*/
|
|
20835
|
+
@Component({
|
|
20836
|
+
selector: 'app-example-sidebar-footer',
|
|
20837
|
+
imports: [
|
|
20838
|
+
RouterLink,
|
|
20839
|
+
RouterLinkActive,
|
|
20840
|
+
BfIconComponent,
|
|
20841
|
+
BfNavItemComponent,
|
|
20842
|
+
BfSidebarComponent,
|
|
20843
|
+
BfSidebarToggleComponent,
|
|
20844
|
+
BfThemeMenuButtonComponent,
|
|
20845
|
+
],
|
|
20846
|
+
template: `
|
|
20847
|
+
<div class="border-border bg-bg rounded-card flex h-[26rem] overflow-hidden border">
|
|
20848
|
+
<bf-sidebar label="Footer demo" [(collapsed)]="collapsed">
|
|
20849
|
+
@for (link of links; track link.path) {
|
|
20850
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
20851
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
20852
|
+
{{ link.label }}
|
|
20853
|
+
</a>
|
|
20854
|
+
}
|
|
20855
|
+
<a
|
|
20856
|
+
bfSlot="footer"
|
|
20857
|
+
bfNavItem
|
|
20858
|
+
routerLink="/theming"
|
|
20859
|
+
routerLinkActive
|
|
20860
|
+
ariaCurrentWhenActive="page"
|
|
20861
|
+
>
|
|
20862
|
+
<bf-icon bfSlot="icon" [icon]="settings" />
|
|
20863
|
+
Settings
|
|
20864
|
+
</a>
|
|
20865
|
+
<div bfSlot="footer" [class]="rowClasses()">
|
|
20866
|
+
<bf-theme-menu-button iconOnly variant="text" placement="above-start" [(value)]="scheme">
|
|
20867
|
+
<bf-icon bfSlot="icon" [icon]="themeIcon()" />
|
|
20868
|
+
</bf-theme-menu-button>
|
|
20869
|
+
<bf-sidebar-toggle>
|
|
20870
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
20871
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
20872
|
+
</bf-sidebar-toggle>
|
|
20873
|
+
</div>
|
|
20874
|
+
<p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
|
|
20875
|
+
Version 0.3.2
|
|
20876
|
+
</p>
|
|
20877
|
+
</bf-sidebar>
|
|
20878
|
+
<div class="min-w-0 flex-1 p-4">
|
|
20879
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
20880
|
+
{{ collapsed() ? 'Icon rail' : 'Expanded' }}, theme {{ scheme() }}.
|
|
20881
|
+
</p>
|
|
20882
|
+
</div>
|
|
20883
|
+
</div>
|
|
20884
|
+
`,
|
|
20885
|
+
})
|
|
20886
|
+
export class SidebarFooterExampleComponent {
|
|
20887
|
+
readonly collapsed = signal(false);
|
|
20888
|
+
/** Stands in for `BfColorSchemeService.scheme`. */
|
|
20889
|
+
readonly scheme = signal<BfColorScheme>('system');
|
|
20890
|
+
|
|
20891
|
+
/** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
|
|
20892
|
+
protected readonly rowClasses = computed(() =>
|
|
20893
|
+
this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
|
|
20894
|
+
);
|
|
20895
|
+
protected readonly themeIcon = computed(
|
|
20896
|
+
() => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
|
|
20897
|
+
);
|
|
20898
|
+
protected readonly links: readonly NavLink[] = [
|
|
20899
|
+
{ path: '/components/nav-item', label: 'Inbox', icon: Inbox },
|
|
20900
|
+
{ path: '/components/nav-group', label: 'Team', icon: Users },
|
|
20901
|
+
];
|
|
20902
|
+
protected readonly settings = Settings;
|
|
20903
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
20904
|
+
protected readonly panelClose = PanelLeftClose;
|
|
20905
|
+
}
|
|
20906
|
+
```
|
|
20232
20907
|
|
|
20233
20908
|
## Responsive layouts
|
|
20234
20909
|
|
|
20235
|
-
Below a breakpoint the sidebar does not turn itself into a menu: a root widget cannot render `bf-menu`. Keep one array of destinations (ideally derived from your route config) and drive every surface from it: the sidebar from `md` up (`class="hidden md:flex"` on `bf-sidebar`; its own `flex` loses to your `hidden`), and below it a [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` router links (`class="md:hidden"`, from `@bitforgehq/ui/overlay`). Each surface keeps the router's `routerLinkActive`.
|
|
20910
|
+
Below a breakpoint the sidebar does not turn itself into a menu: a root widget cannot render `bf-menu`. Keep one array of destinations (ideally derived from your route config) and drive every surface from it: the sidebar from `md` up (`class="hidden md:flex"` on `bf-sidebar`; its own `flex` loses to your `hidden`), and below it a [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` router links (`class="md:hidden"`, from `@bitforgehq/ui/overlay`). Each surface keeps the router's `routerLinkActive`. [`bf-app-frame`](https://bitforge-ui.web.app/components/app-frame) does this layout for you: it shows the sidebar in a sticky column from the breakpoint up and the menu button in a header bar below it.
|
|
20236
20911
|
|
|
20237
20912
|
```ts
|
|
20238
20913
|
import { Component } from '@angular/core';
|
|
@@ -20390,7 +21065,7 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
20390
21065
|
|
|
20391
21066
|
## Accessibility
|
|
20392
21067
|
|
|
20393
|
-
`bf-sidebar` renders a native `<nav>` landmark named by `label`, else `provideBfLabels()`'s `navigation` ("Main"); give each landmark on a page a different name. The header, the destinations and the footer all sit inside it, in that DOM order. The current page is the link the router marks with `aria-current="page"`; the tonal fill is never the only cue under forced colours, where an outline marks it. Collapsing changes only what is visible: every item keeps its label as screen-reader text and each group its name, so the accessible names stay the same.
|
|
21068
|
+
`bf-sidebar` renders a native `<nav>` landmark named by `label`, else `provideBfLabels()`'s `navigation` ("Main"); give each landmark on a page a different name. The header, the destinations and the footer all sit inside it, in that DOM order. The current page is the link the router marks with `aria-current="page"`; the tonal fill is never the only cue under forced colours, where an outline marks it. Collapsing changes only what is visible: every item keeps its label as screen-reader text and each group its name, so the accessible names stay the same. `bf-sidebar-toggle` is a native button named by what it does ("Collapse sidebar"), with `aria-expanded` and `aria-controls` on the `nav`; your own toggle should do the same, and sit in the sidebar or right before it.
|
|
20394
21069
|
|
|
20395
21070
|
### Keyboard
|
|
20396
21071
|
|
|
@@ -20404,7 +21079,7 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
20404
21079
|
| Name | Type | Default | Description |
|
|
20405
21080
|
| --- | --- | --- | --- |
|
|
20406
21081
|
| `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `navigation` ("Main"). Name each landmark differently when a page has more than one. |
|
|
20407
|
-
| `collapsed` | `boolean` | `false` | Collapses the sidebar to an icon rail: items show their icon only, their label stays for screen readers and becomes a tooltip.
|
|
21082
|
+
| `collapsed` | `boolean` | `false` | Collapses the sidebar to an icon rail: items show their icon only, their label stays for screen readers and becomes a tooltip. A `bf-sidebar-toggle` inside flips it; bind `[(collapsed)]` to keep it in your state. |
|
|
20408
21083
|
|
|
20409
21084
|
## Slots
|
|
20410
21085
|
|
|
@@ -20418,6 +21093,7 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
20418
21093
|
|
|
20419
21094
|
- Mark the current page with `routerLinkActive` and `ariaCurrentWhenActive="page"` on every link.
|
|
20420
21095
|
- Drive the sidebar, the small-screen menu and the shell's tab bar from one array of destinations.
|
|
21096
|
+
- Remember the collapsed state with `BfSidebarStateService` and `provideBfSidebarState()`.
|
|
20421
21097
|
|
|
20422
21098
|
### Don't
|
|
20423
21099
|
|
|
@@ -20426,6 +21102,16 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
20426
21102
|
|
|
20427
21103
|
## Migration notes
|
|
20428
21104
|
|
|
21105
|
+
### 0.3.2
|
|
21106
|
+
|
|
21107
|
+
New `BfSidebarStateService` (a `collapsed` signal) and `provideBfSidebarState()` in
|
|
21108
|
+
`@bitforgehq/ui`: bind the sidebar's `[(collapsed)]` to `sidebarState.collapsed` and the provider
|
|
21109
|
+
stores every change under `bf-sidebar-collapsed` in `BF_SIDEBAR_STATE_STORAGE` (`localStorage` in
|
|
21110
|
+
the browser, memory elsewhere), read back at the next start; without the provider nothing is
|
|
21111
|
+
written, and on the server nothing is read. The sidebar's `nav` now has an id, for a toggle's
|
|
21112
|
+
`aria-controls`. The docs gain "Remember the collapsed sidebar" and a "Footer" composition
|
|
21113
|
+
(Settings link, theme menu, toggle, version line) that works expanded and as an icon rail.
|
|
21114
|
+
|
|
20429
21115
|
### 0.3.1
|
|
20430
21116
|
|
|
20431
21117
|
New `bf-sidebar`: the vertical navigation of a desktop app, a `nav` landmark with `header`,
|
|
@@ -20435,6 +21121,296 @@ breakpoint, hide it with `hidden md:flex` and drive a `bf-menu-button` from the
|
|
|
20435
21121
|
|
|
20436
21122
|
---
|
|
20437
21123
|
|
|
21124
|
+
# bf-sidebar-toggle
|
|
21125
|
+
|
|
21126
|
+
Icon button inside a sidebar that collapses it to an icon rail and back
|
|
21127
|
+
|
|
21128
|
+
`BfSidebarToggleComponent` from `@bitforgehq/ui/overlay` · Navigation · since 0.3.2
|
|
21129
|
+
|
|
21130
|
+
## Example
|
|
21131
|
+
|
|
21132
|
+
```ts
|
|
21133
|
+
import { Component, computed, signal } from '@angular/core';
|
|
21134
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
21135
|
+
import { BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
21136
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
21137
|
+
import { BfSidebarToggleComponent } from '@bitforgehq/ui/overlay';
|
|
21138
|
+
import { type IconNode, Layers, PanelLeftClose, PanelLeftOpen, PanelTop } from 'lucide';
|
|
21139
|
+
|
|
21140
|
+
interface NavLink {
|
|
21141
|
+
readonly path: string;
|
|
21142
|
+
readonly label: string;
|
|
21143
|
+
readonly icon: IconNode;
|
|
21144
|
+
}
|
|
21145
|
+
|
|
21146
|
+
/**
|
|
21147
|
+
* The bf-sidebar-toggle overview.
|
|
21148
|
+
*
|
|
21149
|
+
* Live demo of `bf-sidebar-toggle` in the header of a `bf-sidebar`: a press collapses the
|
|
21150
|
+
* sidebar to an icon rail and the next one expands it again. Its name and tooltip say what the
|
|
21151
|
+
* next press does, and only the matching icon slot shows. The sidebar's `[(collapsed)]` keeps
|
|
21152
|
+
* the state in a signal, and the result line reports it.
|
|
21153
|
+
*/
|
|
21154
|
+
@Component({
|
|
21155
|
+
selector: 'app-example-sidebar-toggle-overview',
|
|
21156
|
+
imports: [
|
|
21157
|
+
RouterLink,
|
|
21158
|
+
RouterLinkActive,
|
|
21159
|
+
BfIconComponent,
|
|
21160
|
+
BfNavItemComponent,
|
|
21161
|
+
BfSidebarComponent,
|
|
21162
|
+
BfSidebarToggleComponent,
|
|
21163
|
+
],
|
|
21164
|
+
template: `
|
|
21165
|
+
<div class="border-border bg-bg rounded-card flex h-72 overflow-hidden border">
|
|
21166
|
+
<bf-sidebar label="Toggle demo" [(collapsed)]="collapsed">
|
|
21167
|
+
<div bfSlot="header" class="flex items-center justify-between gap-2">
|
|
21168
|
+
@if (!collapsed()) {
|
|
21169
|
+
<span class="text-heading text-text ps-3">Atlas</span>
|
|
21170
|
+
}
|
|
21171
|
+
<bf-sidebar-toggle>
|
|
21172
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
21173
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
21174
|
+
</bf-sidebar-toggle>
|
|
21175
|
+
</div>
|
|
21176
|
+
@for (link of links; track link.path) {
|
|
21177
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
21178
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
21179
|
+
{{ link.label }}
|
|
21180
|
+
</a>
|
|
21181
|
+
}
|
|
21182
|
+
</bf-sidebar>
|
|
21183
|
+
<div class="min-w-0 flex-1 p-4">
|
|
21184
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
21185
|
+
{{ status() }}
|
|
21186
|
+
</p>
|
|
21187
|
+
</div>
|
|
21188
|
+
</div>
|
|
21189
|
+
`,
|
|
21190
|
+
})
|
|
21191
|
+
export class SidebarToggleOverviewExampleComponent {
|
|
21192
|
+
readonly collapsed = signal(false);
|
|
21193
|
+
readonly status = computed(() =>
|
|
21194
|
+
this.collapsed() ? 'Sidebar collapsed to an icon rail.' : 'Sidebar expanded.',
|
|
21195
|
+
);
|
|
21196
|
+
|
|
21197
|
+
protected readonly links: readonly NavLink[] = [
|
|
21198
|
+
{ path: '/components/sidebar-toggle', label: 'Sidebar toggle', icon: PanelTop },
|
|
21199
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: Layers },
|
|
21200
|
+
];
|
|
21201
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
21202
|
+
protected readonly panelClose = PanelLeftClose;
|
|
21203
|
+
}
|
|
21204
|
+
```
|
|
21205
|
+
|
|
21206
|
+
## Overview
|
|
21207
|
+
|
|
21208
|
+
The collapse button of a `bf-sidebar`: a text icon button placed inside the sidebar (usually in
|
|
21209
|
+
its header or footer) that flips the sidebar between full width and an icon rail. Its name and
|
|
21210
|
+
tooltip say what the next press does ("Collapse sidebar", "Expand sidebar"), `aria-expanded`
|
|
21211
|
+
follows the sidebar and `aria-controls` points at its `nav`. It shows the `collapse-icon` slot
|
|
21212
|
+
while expanded and the `expand-icon` slot while collapsed. It reads and writes the sidebar's
|
|
21213
|
+
`collapsed` model, so `[(collapsed)]` on the sidebar sees every press. Outside a `bf-sidebar`
|
|
21214
|
+
(in a `bf-top-nav`, or on its own) it renders nothing.
|
|
21215
|
+
|
|
21216
|
+
The sidebar toggle collapses a [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar) to an icon rail and expands it again, from `@bitforgehq/ui/overlay` (it shows a `bfTooltip`, so it needs `@angular/cdk`; no Capacitor). Put it inside the sidebar, in its header or footer: it reads and writes the sidebar's `collapsed` model, so the sidebar's `[(collapsed)]` sees every press and the toggle needs no binding of its own. It is a text icon button whose name and tooltip say what the next press does. Import `BfSidebarToggleComponent` from `@bitforgehq/ui/overlay`.
|
|
21217
|
+
|
|
21218
|
+
## When to use
|
|
21219
|
+
|
|
21220
|
+
Use `bf-sidebar-toggle` for the collapse button of a desktop app's `bf-sidebar`. Bind your own [bf-icon-button](https://bitforge-ui.web.app/components/icon-button) to the sidebar's `[(collapsed)]` instead when the button must sit outside the sidebar, in a top bar. In the Capacitor shell there is no sidebar: use [bf-tabs](https://bitforge-ui.web.app/components/tabs) with a `bf-tab-bar` instead.
|
|
21221
|
+
|
|
21222
|
+
## Icons
|
|
21223
|
+
|
|
21224
|
+
The toggle never picks an icon. Project two, `bfSlot="collapse-icon"` (shown while the sidebar is expanded, such as Lucide's `PanelLeftClose`) and `bfSlot="expand-icon"` (shown while it is an icon rail, `PanelLeftOpen`); only the one that matches the state shows. Under a right-to-left layout pick the mirrored icons (`PanelRightClose`, `PanelRightOpen`), since the sidebar then sits at the right.
|
|
21225
|
+
|
|
21226
|
+
## Placement
|
|
21227
|
+
|
|
21228
|
+
The toggle renders only inside a `bf-sidebar`. In a `bf-top-nav`, which never collapses, or on its own it renders nothing and takes no space, so a misplaced toggle never shows a button that does nothing. In the header, put it after the brand and show the brand only while expanded; in the footer, put it beside the theme menu ([footer composition](https://bitforge-ui.web.app/components/sidebar#footer)). It keeps its place in the tab order either way, so a keyboard user finds it where they left it.
|
|
21229
|
+
|
|
21230
|
+
```ts
|
|
21231
|
+
import { Component, computed, signal } from '@angular/core';
|
|
21232
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
21233
|
+
import { type BfColorScheme, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
21234
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
21235
|
+
import { BfSidebarToggleComponent, BfThemeMenuButtonComponent } from '@bitforgehq/ui/overlay';
|
|
21236
|
+
import {
|
|
21237
|
+
Inbox,
|
|
21238
|
+
type IconNode,
|
|
21239
|
+
Moon,
|
|
21240
|
+
PanelLeftClose,
|
|
21241
|
+
PanelLeftOpen,
|
|
21242
|
+
Settings,
|
|
21243
|
+
Sun,
|
|
21244
|
+
SunMoon,
|
|
21245
|
+
Users,
|
|
21246
|
+
} from 'lucide';
|
|
21247
|
+
|
|
21248
|
+
interface NavLink {
|
|
21249
|
+
readonly path: string;
|
|
21250
|
+
readonly label: string;
|
|
21251
|
+
readonly icon: IconNode;
|
|
21252
|
+
}
|
|
21253
|
+
|
|
21254
|
+
/**
|
|
21255
|
+
* A sidebar footer: settings, theme, toggle and version.
|
|
21256
|
+
*
|
|
21257
|
+
* The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
|
|
21258
|
+
* `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
|
|
21259
|
+
* buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
|
|
21260
|
+
* line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
|
|
21261
|
+
* `BfColorSchemeService.scheme`.
|
|
21262
|
+
*/
|
|
21263
|
+
@Component({
|
|
21264
|
+
selector: 'app-example-sidebar-footer',
|
|
21265
|
+
imports: [
|
|
21266
|
+
RouterLink,
|
|
21267
|
+
RouterLinkActive,
|
|
21268
|
+
BfIconComponent,
|
|
21269
|
+
BfNavItemComponent,
|
|
21270
|
+
BfSidebarComponent,
|
|
21271
|
+
BfSidebarToggleComponent,
|
|
21272
|
+
BfThemeMenuButtonComponent,
|
|
21273
|
+
],
|
|
21274
|
+
template: `
|
|
21275
|
+
<div class="border-border bg-bg rounded-card flex h-[26rem] overflow-hidden border">
|
|
21276
|
+
<bf-sidebar label="Footer demo" [(collapsed)]="collapsed">
|
|
21277
|
+
@for (link of links; track link.path) {
|
|
21278
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
21279
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
21280
|
+
{{ link.label }}
|
|
21281
|
+
</a>
|
|
21282
|
+
}
|
|
21283
|
+
<a
|
|
21284
|
+
bfSlot="footer"
|
|
21285
|
+
bfNavItem
|
|
21286
|
+
routerLink="/theming"
|
|
21287
|
+
routerLinkActive
|
|
21288
|
+
ariaCurrentWhenActive="page"
|
|
21289
|
+
>
|
|
21290
|
+
<bf-icon bfSlot="icon" [icon]="settings" />
|
|
21291
|
+
Settings
|
|
21292
|
+
</a>
|
|
21293
|
+
<div bfSlot="footer" [class]="rowClasses()">
|
|
21294
|
+
<bf-theme-menu-button iconOnly variant="text" placement="above-start" [(value)]="scheme">
|
|
21295
|
+
<bf-icon bfSlot="icon" [icon]="themeIcon()" />
|
|
21296
|
+
</bf-theme-menu-button>
|
|
21297
|
+
<bf-sidebar-toggle>
|
|
21298
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
21299
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
21300
|
+
</bf-sidebar-toggle>
|
|
21301
|
+
</div>
|
|
21302
|
+
<p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
|
|
21303
|
+
Version 0.3.2
|
|
21304
|
+
</p>
|
|
21305
|
+
</bf-sidebar>
|
|
21306
|
+
<div class="min-w-0 flex-1 p-4">
|
|
21307
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
21308
|
+
{{ collapsed() ? 'Icon rail' : 'Expanded' }}, theme {{ scheme() }}.
|
|
21309
|
+
</p>
|
|
21310
|
+
</div>
|
|
21311
|
+
</div>
|
|
21312
|
+
`,
|
|
21313
|
+
})
|
|
21314
|
+
export class SidebarFooterExampleComponent {
|
|
21315
|
+
readonly collapsed = signal(false);
|
|
21316
|
+
/** Stands in for `BfColorSchemeService.scheme`. */
|
|
21317
|
+
readonly scheme = signal<BfColorScheme>('system');
|
|
21318
|
+
|
|
21319
|
+
/** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
|
|
21320
|
+
protected readonly rowClasses = computed(() =>
|
|
21321
|
+
this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
|
|
21322
|
+
);
|
|
21323
|
+
protected readonly themeIcon = computed(
|
|
21324
|
+
() => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
|
|
21325
|
+
);
|
|
21326
|
+
protected readonly links: readonly NavLink[] = [
|
|
21327
|
+
{ path: '/components/nav-item', label: 'Inbox', icon: Inbox },
|
|
21328
|
+
{ path: '/components/nav-group', label: 'Team', icon: Users },
|
|
21329
|
+
];
|
|
21330
|
+
protected readonly settings = Settings;
|
|
21331
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
21332
|
+
protected readonly panelClose = PanelLeftClose;
|
|
21333
|
+
}
|
|
21334
|
+
```
|
|
21335
|
+
|
|
21336
|
+
## Remembering the state
|
|
21337
|
+
|
|
21338
|
+
The toggle holds no state. To open the app with the sidebar as the user left it, bind the sidebar to `BfSidebarStateService` from `@bitforgehq/ui` and add `provideBfSidebarState()` to the app's providers ([remember the collapsed sidebar](https://bitforge-ui.web.app/components/sidebar#remember-the-collapsed-sidebar)).
|
|
21339
|
+
|
|
21340
|
+
## Sizes
|
|
21341
|
+
|
|
21342
|
+
`size` is `sm` 32, `md` 40 or `lg` 48 px, square; unset it follows `provideBfDefaults()`'s `controlSize`, as `bf-icon-button` does.
|
|
21343
|
+
|
|
21344
|
+
## Labels
|
|
21345
|
+
|
|
21346
|
+
The name and tooltip read `sidebarCollapse` ("Collapse sidebar") while the sidebar is expanded and `sidebarExpand` ("Expand sidebar") while it is an icon rail. Translate them with `provideBfLabels()`.
|
|
21347
|
+
|
|
21348
|
+
## Testing
|
|
21349
|
+
|
|
21350
|
+
`BfSidebarToggleHarness` from `@bitforgehq/ui/testing` reads the name with `getLabel()`, the state with `isExpanded()` and presses it with `toggle()`; filter it with `label: 'Collapse sidebar'`.
|
|
21351
|
+
|
|
21352
|
+
## Desktop
|
|
21353
|
+
|
|
21354
|
+
The toggle looks and behaves like a text `bf-icon-button`: the muted text colour, a `surface-muted` fill and the pointer cursor on hover, a pressed tint while pressed, and the focus ring on keyboard focus only (`focus-visible`). Its tooltip shows after a 500 ms rest of a fine pointer and at once on keyboard focus: above the button while expanded, at the inline end of the rail while collapsed. Under a fine pointer the visible box is the hit area; under a coarse pointer (a touchscreen laptop) a `sm` or `md` toggle extends vertically to 44 px, and no tooltip shows.
|
|
21355
|
+
|
|
21356
|
+
## Theming
|
|
21357
|
+
|
|
21358
|
+
The toggle consumes `bf-icon-button`'s text variant: `text-muted` at rest, `text` and `surface-muted` on hover, `primary-soft-pressed` while pressed, and the `ring` token for focus. The tooltip consumes `bf-tooltip`'s tokens.
|
|
21359
|
+
|
|
21360
|
+
## Accessibility
|
|
21361
|
+
|
|
21362
|
+
- A native `<button type="button">` named by `aria-label`: "Collapse sidebar" while expanded, "Expand sidebar" while collapsed, so the name says what a press does. The tooltip shows the same text and describes the button while it shows.
|
|
21363
|
+
- `aria-expanded` is `true` while the sidebar is at full width and `false` as an icon rail; `aria-controls` points at the sidebar's `nav`.
|
|
21364
|
+
- The projected icons add nothing to the name (`bf-icon` is `aria-hidden`).
|
|
21365
|
+
- Collapsing keeps focus on the toggle, and every nav item keeps its accessible name, so nothing a screen reader user relies on moves.
|
|
21366
|
+
|
|
21367
|
+
### Keyboard
|
|
21368
|
+
|
|
21369
|
+
| Key | Action |
|
|
21370
|
+
| ----------------------------------- | ------------------------------------------------ |
|
|
21371
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Collapses or expands the sidebar |
|
|
21372
|
+
| <kbd>Escape</kbd> | Hides the tooltip while it shows |
|
|
21373
|
+
| <kbd>Tab</kbd> | Moves on to the next control, hiding the tooltip |
|
|
21374
|
+
|
|
21375
|
+
## Inputs
|
|
21376
|
+
|
|
21377
|
+
| Name | Type | Default | Description |
|
|
21378
|
+
| --- | --- | --- | --- |
|
|
21379
|
+
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step: sm 32, md 40, lg 48 px (square); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
|
|
21380
|
+
|
|
21381
|
+
## Slots
|
|
21382
|
+
|
|
21383
|
+
- `collapse-icon`: The icon shown while the sidebar is expanded (a panel closing).
|
|
21384
|
+
- `expand-icon`: The icon shown while the sidebar is an icon rail (a panel opening).
|
|
21385
|
+
|
|
21386
|
+
## Notes
|
|
21387
|
+
|
|
21388
|
+
### Do
|
|
21389
|
+
|
|
21390
|
+
- Put the toggle inside the `bf-sidebar` it collapses, in its header or footer.
|
|
21391
|
+
- Project both icon slots, a panel closing and a panel opening.
|
|
21392
|
+
- Bind the sidebar to `BfSidebarStateService.collapsed` with `provideBfSidebarState()`, so the choice survives a reload.
|
|
21393
|
+
|
|
21394
|
+
### Don't
|
|
21395
|
+
|
|
21396
|
+
- Don't put it outside a `bf-sidebar`; it renders nothing there. Use a `bf-icon-button` bound to `[(collapsed)]` instead.
|
|
21397
|
+
- Don't add your own `aria-label` or tooltip; the toggle names itself by what the next press does.
|
|
21398
|
+
|
|
21399
|
+
## Migration notes
|
|
21400
|
+
|
|
21401
|
+
### 0.3.2
|
|
21402
|
+
|
|
21403
|
+
New `bf-sidebar-toggle` in `@bitforgehq/ui/overlay`: a text icon button placed inside a
|
|
21404
|
+
`bf-sidebar` (header or footer) that flips the sidebar between full width and an icon rail by
|
|
21405
|
+
writing its `collapsed` model, so `[(collapsed)]` sees every press. Its name and `bfTooltip` say
|
|
21406
|
+
what the next press does (new label keys `sidebarCollapse` "Collapse sidebar" and `sidebarExpand`
|
|
21407
|
+
"Expand sidebar"); `aria-expanded` follows the sidebar and `aria-controls` points at its `nav`.
|
|
21408
|
+
Icons are the `collapse-icon` and `expand-icon` slots, only the matching one shown; `size` as
|
|
21409
|
+
`bf-icon-button`. Outside a sidebar (in a `bf-top-nav`, or alone) it renders nothing. A
|
|
21410
|
+
`BfSidebarToggleHarness` in `@bitforgehq/ui/testing` (`getLabel()`, `isExpanded()`, `toggle()`).
|
|
21411
|
+
|
|
21412
|
+
---
|
|
21413
|
+
|
|
20438
21414
|
# bf-skeleton
|
|
20439
21415
|
|
|
20440
21416
|
Loading placeholder shaped as text lines, a rectangle or a circle, with a shimmer
|
|
@@ -21745,7 +22721,7 @@ Use `bf-stat` for a few headline figures the reader should take in at a glance,
|
|
|
21745
22721
|
|
|
21746
22722
|
## Cards
|
|
21747
22723
|
|
|
21748
|
-
`variant="card"` gives each stat its own outlined tile, the `flat` card surface. In a grid the cards stretch to the row's height, so a row of stats lines up even when one has a supporting line and another has none. Put an icon in the `icon` slot (`bfSlot="icon"`, a `bf-icon`) to show what the figure is about.
|
|
22724
|
+
`variant="card"` gives each stat its own outlined tile, the `flat` card surface, for stats that sit on the page itself; inside a raised card use `tile` instead. In a grid the cards stretch to the row's height, so a row of stats lines up even when one has a supporting line and another has none. Put an icon in the `icon` slot (`bfSlot="icon"`, a `bf-icon`) to show what the figure is about.
|
|
21749
22725
|
|
|
21750
22726
|
```ts
|
|
21751
22727
|
import { Component } from '@angular/core';
|
|
@@ -21787,6 +22763,60 @@ export class StatOverviewExampleComponent {
|
|
|
21787
22763
|
}
|
|
21788
22764
|
```
|
|
21789
22765
|
|
|
22766
|
+
## Tiles
|
|
22767
|
+
|
|
22768
|
+
`variant="tile"` is a sunken, borderless tile (`surface-sunken`, the `card` radius and padding) for a row of stats inside a raised [bf-card](https://bitforge-ui.web.app/components/card): a raised card holds sunken tiles, never outlined ones. On a tile the icon tile turns `surface`, so it still shows. Keep one surface style per level of a dashboard: raised cards on the page, tiles in each card.
|
|
22769
|
+
|
|
22770
|
+
```ts
|
|
22771
|
+
import { Component } from '@angular/core';
|
|
22772
|
+
import { BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
|
|
22773
|
+
|
|
22774
|
+
/**
|
|
22775
|
+
* A dashboard with one surface per level.
|
|
22776
|
+
*
|
|
22777
|
+
* The page holds raised cards, and every card holds sunken stat tiles: each level has one surface
|
|
22778
|
+
* style, so the eye reads the cards as the groups and the tiles as their figures.
|
|
22779
|
+
*/
|
|
22780
|
+
@Component({
|
|
22781
|
+
selector: 'app-example-stat-dashboard',
|
|
22782
|
+
imports: [BfCardComponent, BfStatComponent],
|
|
22783
|
+
template: `
|
|
22784
|
+
<div class="grid gap-4 @lg:grid-cols-2">
|
|
22785
|
+
<bf-card>
|
|
22786
|
+
<h3 class="text-heading">Sales</h3>
|
|
22787
|
+
<div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
22788
|
+
<bf-stat label="Revenue" value="€48,200" variant="tile" delta="+12%" trend="up">
|
|
22789
|
+
vs. last month
|
|
22790
|
+
</bf-stat>
|
|
22791
|
+
<bf-stat label="Orders" value="1,204" variant="tile" delta="+3%" trend="up">
|
|
22792
|
+
vs. last month
|
|
22793
|
+
</bf-stat>
|
|
22794
|
+
</div>
|
|
22795
|
+
</bf-card>
|
|
22796
|
+
<bf-card>
|
|
22797
|
+
<h3 class="text-heading">Support</h3>
|
|
22798
|
+
<div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
22799
|
+
<bf-stat
|
|
22800
|
+
label="Open tickets"
|
|
22801
|
+
value="18"
|
|
22802
|
+
variant="tile"
|
|
22803
|
+
delta="-4"
|
|
22804
|
+
trend="down"
|
|
22805
|
+
trendIntent="negative"
|
|
22806
|
+
>
|
|
22807
|
+
vs. last week
|
|
22808
|
+
</bf-stat>
|
|
22809
|
+
<bf-stat label="First reply" value="2 h" variant="tile" delta="0" trend="flat">
|
|
22810
|
+
vs. last week
|
|
22811
|
+
</bf-stat>
|
|
22812
|
+
</div>
|
|
22813
|
+
</bf-card>
|
|
22814
|
+
</div>
|
|
22815
|
+
`,
|
|
22816
|
+
})
|
|
22817
|
+
export class StatDashboardExampleComponent {}
|
|
22818
|
+
```
|
|
22819
|
+
|
|
21790
22820
|
## Plain
|
|
21791
22821
|
|
|
21792
22822
|
`variant="plain"` (the default) draws no surface, for stats that sit on a card or a page section of their own. The default slot is the supporting line under the value: a short trend ("+12% this week") or a `bf-badge`.
|
|
@@ -21866,8 +22896,9 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
|
|
|
21866
22896
|
- Label (`dt`): `text-caption` in `text-muted`. Value (`dd`): `text-title` in `text`, with tabular figures (`tabular-nums`) so numbers line up across stats.
|
|
21867
22897
|
- Supporting line: `text-caption` in `text-muted`.
|
|
21868
22898
|
- Delta pill: the badge's label shape (`control` radius, `text-micro` bold, tabular figures); good changes `success-soft` with `success` text, bad ones `danger-soft` with `danger` text, flat or no trend `neutral-soft` with `neutral` text. The arrow mark is drawn with borders in `currentColor`, so it takes the pill's text colour.
|
|
21869
|
-
- Icon tile: `surface-sunken` background, `text-muted` icon, `tile` radius, 40 px.
|
|
22899
|
+
- Icon tile: `surface-sunken` background (`surface` on a `tile` stat), `text-muted` icon, `tile` radius, 40 px.
|
|
21870
22900
|
- `card`: `surface`, the `card` radius, `--spacing-card` padding (`p-card`) and a 1.5 px `border-strong` edge, as the `flat` `bf-card`.
|
|
22901
|
+
- `tile`: `surface-sunken`, the `card` radius and `--spacing-card` padding, no border.
|
|
21871
22902
|
|
|
21872
22903
|
## Accessibility
|
|
21873
22904
|
|
|
@@ -21879,7 +22910,7 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
|
|
|
21879
22910
|
| --- | --- | --- | --- |
|
|
21880
22911
|
| `label` | `string` | required | What the figure measures, shown above it. |
|
|
21881
22912
|
| `value` | `string` | required | The figure, already formatted (`45 min`, `1,204`, `98%`). |
|
|
21882
|
-
| `variant` | `
|
|
22913
|
+
| `variant` | `BfStatAppearance` (`'plain' \| 'card' \| 'tile'`) | `'plain'` | Surface: plain (default) on its container, card (an outlined tile) or tile (sunken, no border). |
|
|
21883
22914
|
| `delta` | `string` | `''` | A change shown in a pill beside the value, already formatted (`+12%`); empty shows none. |
|
|
21884
22915
|
| `trend` | `BfStatTrend \| undefined` (`'up' \| 'down' \| 'flat' \| undefined`) | `undefined` | The delta's direction: an arrow mark and a hidden word; left unset, the pill is neutral. |
|
|
21885
22916
|
| `trendIntent` | `BfStatTrendIntent` (`'positive' \| 'negative'`) | `'positive'` | Whether up is good (positive, default) or down is good (negative, such as costs). |
|
|
@@ -21899,12 +22930,21 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
|
|
|
21899
22930
|
|
|
21900
22931
|
### Don't
|
|
21901
22932
|
|
|
22933
|
+
- Don't put `card` stats (outlined) inside a raised `bf-card`; use `tile`.
|
|
21902
22934
|
- Don't use a stat for a value the user edits or acts on; give it its own control.
|
|
21903
22935
|
- Don't put long text in `value`; it is set at the `title` size for a short figure.
|
|
21904
22936
|
- Don't put the comparison period in `delta`; keep it to the change (`+12%`) and say "vs. last month" in the supporting line.
|
|
21905
22937
|
|
|
21906
22938
|
## Migration notes
|
|
21907
22939
|
|
|
22940
|
+
### 0.3.2
|
|
22941
|
+
|
|
22942
|
+
New `variant="tile"`: a sunken, borderless tile (`surface-sunken`, the `card` radius and
|
|
22943
|
+
padding) for a row of stats inside a raised `bf-card`; on it the icon tile turns `surface`.
|
|
22944
|
+
`variant` now takes the new named type `BfStatAppearance` (`BfStatVariant | 'tile'`); the
|
|
22945
|
+
released `BfStatVariant` keeps its two members, so `Record`s over it keep compiling. New
|
|
22946
|
+
`stat/dashboard` example with one surface per level.
|
|
22947
|
+
|
|
21908
22948
|
### 0.3.1
|
|
21909
22949
|
|
|
21910
22950
|
New: `bf-stat`, one labelled figure in its own `dl`, with the value at the `title` size in
|
|
@@ -23146,13 +24186,14 @@ export class TableOverviewExampleComponent {
|
|
|
23146
24186
|
|
|
23147
24187
|
Data table for desktop: column headers that set a sort, optional row selection with
|
|
23148
24188
|
checkboxes, custom cells from `bfTableCell` templates and an empty slot. It never sorts or
|
|
23149
|
-
filters itself; the consumer sorts the rows in a `computed()` from `sort`.
|
|
24189
|
+
filters itself; the consumer sorts the rows in a `computed()` from `sort`. With `stack` it
|
|
24190
|
+
renders each row as a tile of label/value pairs in a narrow space or under touch.
|
|
23150
24191
|
|
|
23151
24192
|
A data table for desktop pages: rows of records the user compares across columns, with headers that sort, an optional checkbox column that selects rows, custom cells and an empty state. `bf-table` is data-driven: you pass `rows` and `columns`, and it never sorts or filters the rows itself. Its headers set the `sort` model and you sort the rows in a `computed()`, on the client or on the server.
|
|
23152
24193
|
|
|
23153
24194
|
## When to use
|
|
23154
24195
|
|
|
23155
|
-
Use `bf-table` on a desktop page for records the user scans and compares column by column: tasks, invoices, members. On a phone or in the Capacitor shell use [bf-list](https://bitforge-ui.web.app/components/list) rows instead. Use a [bf-list](https://bitforge-ui.web.app/components/list) on desktop too when each record is one line of text with a status, and a description list for the fields of a single record.
|
|
24196
|
+
Use `bf-table` on a desktop page for records the user scans and compares column by column: tasks, invoices, members. On a phone or in the Capacitor shell use [bf-list](https://bitforge-ui.web.app/components/list) rows instead, or let the table stack its rows into tiles with `stack` (see Stacked rows). Use a [bf-list](https://bitforge-ui.web.app/components/list) on desktop too when each record is one line of text with a status, and a description list for the fields of a single record.
|
|
23156
24197
|
|
|
23157
24198
|
## Columns and rows
|
|
23158
24199
|
|
|
@@ -23443,9 +24484,115 @@ export class TablePagedExampleComponent {
|
|
|
23443
24484
|
|
|
23444
24485
|
`size` sets the row height: `sm` 40 px, `md` 48 px (default), `lg` 56 px, with a 32, 40 or 48 px header. Unset, it follows `provideBfDefaults()`'s `controlSize`, so `{ size: 'sm' }` in a page's `providers` makes a dense table. The checkboxes take the same step. Under a coarse pointer every row is at least 44 px tall, and the sortable header buttons are 44 px targets; a cell grows to fit its content and is never covered by a hit-area extension.
|
|
23445
24486
|
|
|
24487
|
+
## Stacked rows
|
|
24488
|
+
|
|
24489
|
+
`stack` lets the table itself fit a narrow space. With `stack="narrow"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack="coarse"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.
|
|
24490
|
+
|
|
24491
|
+
Stacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select of every sortable column, ascending and descending (`tableSortBy`, `tableSortAscending`, `tableSortDescending` labels), since the hidden header's buttons are not shown. Choosing an option sets `sort`, as a header click does.
|
|
24492
|
+
|
|
24493
|
+
```ts
|
|
24494
|
+
import { Component, computed, signal } from '@angular/core';
|
|
24495
|
+
import {
|
|
24496
|
+
BfBadgeComponent,
|
|
24497
|
+
BfSegmentedComponent,
|
|
24498
|
+
BfTableCellDirective,
|
|
24499
|
+
BfTableComponent,
|
|
24500
|
+
type BfSegmentOption,
|
|
24501
|
+
type BfTableColumn,
|
|
24502
|
+
type BfTableSort,
|
|
24503
|
+
} from '@bitforgehq/ui';
|
|
24504
|
+
|
|
24505
|
+
interface Task {
|
|
24506
|
+
id: string;
|
|
24507
|
+
title: string;
|
|
24508
|
+
project: string;
|
|
24509
|
+
status: 'Open' | 'Done';
|
|
24510
|
+
hours: number;
|
|
24511
|
+
}
|
|
24512
|
+
|
|
24513
|
+
const TASKS: readonly Task[] = [
|
|
24514
|
+
{ id: 'brief', title: 'Write the brief', project: 'Atlas', status: 'Done', hours: 3 },
|
|
24515
|
+
{ id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', status: 'Open', hours: 6 },
|
|
24516
|
+
{ id: 'deck', title: 'Pitch deck', project: 'Atlas', status: 'Open', hours: 9 },
|
|
24517
|
+
];
|
|
24518
|
+
|
|
24519
|
+
type Width = 'phone' | 'full';
|
|
24520
|
+
|
|
24521
|
+
/**
|
|
24522
|
+
* Stacked rows on a narrow table.
|
|
24523
|
+
*
|
|
24524
|
+
* `stack="narrow"` turns each row into a tile of label/value pairs while the table is narrower
|
|
24525
|
+
* than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
|
|
24526
|
+
* holds Select all and a Sort by select. Switch the box between phone width and full width.
|
|
24527
|
+
*/
|
|
24528
|
+
@Component({
|
|
24529
|
+
selector: 'app-example-table-stacked',
|
|
24530
|
+
imports: [BfTableComponent, BfTableCellDirective, BfBadgeComponent, BfSegmentedComponent],
|
|
24531
|
+
template: `
|
|
24532
|
+
<!-- A stacking table is a size container: its width comes from its parent, never its columns. -->
|
|
24533
|
+
<div class="flex w-152 max-w-full flex-col gap-3">
|
|
24534
|
+
<bf-segmented label="Box width" [options]="widths" [(value)]="width" />
|
|
24535
|
+
<div [class]="width() === 'phone' ? 'w-full max-w-sm' : 'w-full'">
|
|
24536
|
+
<bf-table
|
|
24537
|
+
label="Stacked tasks"
|
|
24538
|
+
stack="narrow"
|
|
24539
|
+
selectable
|
|
24540
|
+
[rows]="rows()"
|
|
24541
|
+
[columns]="columns"
|
|
24542
|
+
[(sort)]="sort"
|
|
24543
|
+
[(selection)]="selection"
|
|
24544
|
+
>
|
|
24545
|
+
<ng-template bfTableCell="status" [bfTableCellOf]="rows()" let-task>
|
|
24546
|
+
<bf-badge [variant]="task.status === 'Done' ? 'success' : 'neutral'">
|
|
24547
|
+
{{ task.status }}
|
|
24548
|
+
</bf-badge>
|
|
24549
|
+
</ng-template>
|
|
24550
|
+
</bf-table>
|
|
24551
|
+
</div>
|
|
24552
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
24553
|
+
</div>
|
|
24554
|
+
`,
|
|
24555
|
+
})
|
|
24556
|
+
export class TableStackedExampleComponent {
|
|
24557
|
+
readonly widths: BfSegmentOption<Width>[] = [
|
|
24558
|
+
{ value: 'phone', label: 'Phone' },
|
|
24559
|
+
{ value: 'full', label: 'Full width' },
|
|
24560
|
+
];
|
|
24561
|
+
readonly width = signal<Width | null>('phone');
|
|
24562
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
24563
|
+
{ key: 'title', label: 'Task', sortable: true },
|
|
24564
|
+
{ key: 'project', label: 'Project' },
|
|
24565
|
+
{ key: 'status', label: 'Status' },
|
|
24566
|
+
{ key: 'hours', label: 'Hours', sortable: true, align: 'end' },
|
|
24567
|
+
];
|
|
24568
|
+
readonly sort = signal<BfTableSort | null>(null);
|
|
24569
|
+
readonly selection = signal<readonly string[]>([]);
|
|
24570
|
+
|
|
24571
|
+
/** The table never sorts: the page sorts its rows from `sort`. */
|
|
24572
|
+
readonly rows = computed(() => {
|
|
24573
|
+
const sort = this.sort();
|
|
24574
|
+
if (!sort) {
|
|
24575
|
+
return TASKS;
|
|
24576
|
+
}
|
|
24577
|
+
const factor = sort.direction === 'ascending' ? 1 : -1;
|
|
24578
|
+
return [...TASKS].sort((a, b) =>
|
|
24579
|
+
sort.column === 'hours'
|
|
24580
|
+
? (a.hours - b.hours) * factor
|
|
24581
|
+
: a.title.localeCompare(b.title) * factor,
|
|
24582
|
+
);
|
|
24583
|
+
});
|
|
24584
|
+
|
|
24585
|
+
readonly status = computed(() => {
|
|
24586
|
+
const sort = this.sort();
|
|
24587
|
+
const sorted = sort ? `Sorted by ${sort.column}, ${sort.direction}` : 'Unsorted';
|
|
24588
|
+
return `${sorted} · ${this.selection().length} selected.`;
|
|
24589
|
+
});
|
|
24590
|
+
}
|
|
24591
|
+
```
|
|
24592
|
+
|
|
23446
24593
|
## Narrow screens and touch
|
|
23447
24594
|
|
|
23448
|
-
A table needs width.
|
|
24595
|
+
A table needs width. When stacked tiles are not what a phone should show (long records, a summary per row), render the same rows as a divided `bf-list` of `bf-list-item`s below the `md` breakpoint (a `md:hidden` wrapper) and the table only from `md` up (a `hidden md:block` wrapper), from the one `rows` array. Inside the Capacitor shell use the list alone, or a table with `stack="coarse"`. The table's wrapper scrolls sideways when its columns cannot fit, but that is a last resort, not a layout for phones.
|
|
23449
24596
|
|
|
23450
24597
|
```ts
|
|
23451
24598
|
import { Component } from '@angular/core';
|
|
@@ -23514,6 +24661,7 @@ A selectable table tints a row `surface-muted` while hovered (a selected row `pr
|
|
|
23514
24661
|
- Rows: `text` on the page's surface, divided by `border` lines; hover (selectable only) `surface-muted`; selected `primary-soft`, hover `primary-soft-hover`.
|
|
23515
24662
|
- Sort arrow: a 1.5 px CSS chevron in the header's text colour.
|
|
23516
24663
|
- Focus ring: `ring`. Header buttons: `rounded-field`.
|
|
24664
|
+
- Stacked rows: tiles on `surface-sunken` with `rounded-tile`, labels `text-label-xs` bold in `text-muted`.
|
|
23517
24665
|
- Text: `text-body` (`text-label-sm` at `sm`), `tabular-nums` in `end` columns.
|
|
23518
24666
|
|
|
23519
24667
|
Put the table on a `bf-card` (or another surface) yourself; it has no background or border of its own.
|
|
@@ -23522,6 +24670,8 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
|
|
|
23522
24670
|
|
|
23523
24671
|
A native `table` with a visually hidden `caption` from `label`, `th scope="col"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort="ascending"` or `"descending"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` ("Select all rows"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.
|
|
23524
24672
|
|
|
24673
|
+
A table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / ""`), so a screen reader reads the column header once, not the label as well.
|
|
24674
|
+
|
|
23525
24675
|
### Keyboard
|
|
23526
24676
|
|
|
23527
24677
|
| Key | Action |
|
|
@@ -23532,6 +24682,8 @@ A native `table` with a visually hidden `caption` from `label`, `th scope="col"`
|
|
|
23532
24682
|
| <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |
|
|
23533
24683
|
| <kbd>Enter</kbd> | Sorts by the focused header |
|
|
23534
24684
|
|
|
24685
|
+
A stacked table's Sort by select is a native select with its own keys.
|
|
24686
|
+
|
|
23535
24687
|
Text fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.
|
|
23536
24688
|
|
|
23537
24689
|
## Inputs
|
|
@@ -23545,6 +24697,7 @@ Text fields, selects and composite widgets (radios, comboboxes, sliders) in a ce
|
|
|
23545
24697
|
| `selectable` | `boolean` | `false` | Adds a first column of checkboxes that select rows. |
|
|
23546
24698
|
| `selectAllLabel` | `string` | `''` | Accessible name of the header checkbox; empty falls back to `provideBfLabels()`'s `tableSelectAll` ("Select all rows"). |
|
|
23547
24699
|
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Row height step: sm 40, md 48, lg 56 px (the header 32/40/48; at least 44 px under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
|
|
24700
|
+
| `stack` | `BfTableStack` (`'never' \| 'narrow' \| 'coarse'`) | `'never'` | When the rows render as cards of label/value pairs: `never`, `narrow` (the table is narrower than 36rem) or `coarse` (under a coarse pointer, at any width). |
|
|
23548
24701
|
| `sort` | `BfTableSort \| null` | `null` | The sorted column and direction, `null` while unsorted; two-way bindable. A sortable header sets it, and the consumer sorts `rows` from it. Two-way: `[(sort)]`. |
|
|
23549
24702
|
| `selection` | `readonly string[]` | `[]` | The keys (`rowKey`) of the selected rows; two-way bindable. Two-way: `[(selection)]`. |
|
|
23550
24703
|
|
|
@@ -23585,15 +24738,31 @@ Selects or clears one row.
|
|
|
23585
24738
|
|
|
23586
24739
|
- Give every table a `label`, and every sortable column a short header.
|
|
23587
24740
|
- Sort and filter the rows in a `computed()` from `sort` and your filters.
|
|
23588
|
-
-
|
|
24741
|
+
- Use `stack="narrow"` for a table that may sit in a narrow column, or `stack="coarse"` in the Capacitor shell; otherwise render the rows as `bf-list-item`s below `md`.
|
|
23589
24742
|
|
|
23590
24743
|
### Don't
|
|
23591
24744
|
|
|
23592
|
-
- Don't put a table on a phone screen that scrolls sideways; use the list.
|
|
24745
|
+
- Don't put a table on a phone screen that scrolls sideways; stack it or use the list.
|
|
24746
|
+
- Don't put a stacking table in a parent that sizes it from its content: the container query needs a width from outside.
|
|
23593
24747
|
- Don't make a whole row clickable; put a link or button in a custom cell.
|
|
23594
24748
|
|
|
23595
24749
|
## Migration notes
|
|
23596
24750
|
|
|
24751
|
+
### 0.3.2
|
|
24752
|
+
|
|
24753
|
+
New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not
|
|
24754
|
+
change, `'narrow'` or `'coarse'`): while the table is narrower than 36rem (a container query on
|
|
24755
|
+
the host, which then gets `@container`) or, for `'coarse'`, under a coarse pointer at any width,
|
|
24756
|
+
each body row renders as a sunken tile of label/value pairs. The markup stays a `table` with
|
|
24757
|
+
explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles (some browsers drop a
|
|
24758
|
+
table's semantics once its display changes), the header row is visually hidden but still read,
|
|
24759
|
+
the selection checkbox and the first cell head each tile, and every other cell shows its column
|
|
24760
|
+
label (`data-label`, a `::before` with empty alternative text, so it is not read twice). A bar
|
|
24761
|
+
above the tiles holds a labelled Select all checkbox and a Sort by `bf-select`, since the hidden
|
|
24762
|
+
header's controls are not shown. New label keys `tableSortBy` ("Sort by"), `tableSortAscending`
|
|
24763
|
+
and `tableSortDescending` (`"Hours, ascending"`). `BfTableHarness` reads and drives a stacked
|
|
24764
|
+
table the same way. New `table/stacked` example and a Stacked rows docs section.
|
|
24765
|
+
|
|
23597
24766
|
### 0.3.1
|
|
23598
24767
|
|
|
23599
24768
|
New `bf-table` and `ng-template[bfTableCell]` (`BfTableCellDirective`) in `@bitforgehq/ui`, a
|
|
@@ -23808,7 +24977,7 @@ fires no keyboard event).
|
|
|
23808
24977
|
|
|
23809
24978
|
## Desktop
|
|
23810
24979
|
|
|
23811
|
-
`bf-tabs` is part of the Capacitor shell (`provideBfShell()`). On a desktop web app, put the top-level destinations in a [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) (a few sections) or a [bf-sidebar](https://bitforge-ui.web.app/components/sidebar) (more, in groups, collapsible to an icon rail) of `a[bfNavItem]` links, each with `routerLink`, `routerLinkActive` and `ariaCurrentWhenActive="page"`, above a plain `router-outlet`. The router sets `aria-current="page"` on the link of the current route and the item shows it. Drive both from one array of destinations: inside the shell the same array feeds the `bf-tab`s of a [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar), and below a breakpoint a [bf-menu-button](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` links (see bf-sidebar, Responsive). Inside the shell under a mouse, the tab links take the `bf-tab` hover and focus styles.
|
|
24980
|
+
`bf-tabs` is part of the Capacitor shell (`provideBfShell()`). On a desktop web app, put the top-level destinations in a [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) (a few sections) or a [bf-sidebar](https://bitforge-ui.web.app/components/sidebar) (more, in groups, collapsible to an icon rail) of `a[bfNavItem]` links, each with `routerLink`, `routerLinkActive` and `ariaCurrentWhenActive="page"`, above a plain `router-outlet`. The router sets `aria-current="page"` on the link of the current route and the item shows it. Drive both from one array of destinations: inside the shell the same array feeds the `bf-tab`s of a [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar), and below a breakpoint a [bf-menu-button](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` links (see bf-sidebar, Responsive). Wrap the desktop app in a [bf-app-frame](https://bitforge-ui.web.app/components/app-frame): it holds the sidebar from `md` up and, below it, a header bar with that menu button. Inside the shell under a mouse, the tab links take the `bf-tab` hover and focus styles.
|
|
23812
24981
|
|
|
23813
24982
|
```ts
|
|
23814
24983
|
import { Component } from '@angular/core';
|
|
@@ -23979,6 +25148,10 @@ content to the top.
|
|
|
23979
25148
|
|
|
23980
25149
|
## Migration notes
|
|
23981
25150
|
|
|
25151
|
+
### 0.3.2
|
|
25152
|
+
|
|
25153
|
+
The `## Desktop` docs point at `bf-app-frame` for the frame around the desktop navigation.
|
|
25154
|
+
|
|
23982
25155
|
### 0.3.1
|
|
23983
25156
|
|
|
23984
25157
|
The `## Desktop` docs name the desktop replacement: a `bf-top-nav` or `bf-sidebar` of
|
|
@@ -26142,7 +27315,7 @@ export class ToolbarStatusExampleComponent {
|
|
|
26142
27315
|
|
|
26143
27316
|
## Wrapping
|
|
26144
27317
|
|
|
26145
|
-
The row is a wrapping flex row with an 8 px gap between items and between rows.
|
|
27318
|
+
The row is a wrapping flex row with an 8 px gap between items and between rows. The status and the actions are one group that takes the rest of the last row: on a wide row the status follows the view switch and the actions sit at the inline end; when the group no longer fits beside the filters, it wraps onto a row of its own as a whole, with the status at the start and the actions at the end. So the actions never end up alone on a row, and they stay on the last filter row whenever the group fits there. Give a search field a width (`class="w-full sm:w-56"`) so it fills a phone-wide row and stays compact on a wide one.
|
|
26146
27319
|
|
|
26147
27320
|
```ts
|
|
26148
27321
|
import { Component } from '@angular/core';
|
|
@@ -26157,8 +27330,8 @@ import {
|
|
|
26157
27330
|
/**
|
|
26158
27331
|
* A toolbar that wraps.
|
|
26159
27332
|
*
|
|
26160
|
-
* In a narrow column the filters wrap onto new rows 8 px apart, and the
|
|
26161
|
-
* together,
|
|
27333
|
+
* In a narrow column the filters wrap onto new rows 8 px apart, and the status and the actions
|
|
27334
|
+
* move down together: the count at the start of the last row, the actions at the inline end.
|
|
26162
27335
|
*/
|
|
26163
27336
|
@Component({
|
|
26164
27337
|
selector: 'app-example-toolbar-wrapping',
|
|
@@ -26187,6 +27360,63 @@ export class ToolbarWrappingExampleComponent {
|
|
|
26187
27360
|
}
|
|
26188
27361
|
```
|
|
26189
27362
|
|
|
27363
|
+
## Phone width
|
|
27364
|
+
|
|
27365
|
+
At phone width (in a desktop web app's narrow window or a narrow column) the search takes a row, the filters share the next, and the status and actions share the last row. A toolbar without actions keeps the status as a plain item after the filters.
|
|
27366
|
+
|
|
27367
|
+
```ts
|
|
27368
|
+
import { Component, signal } from '@angular/core';
|
|
27369
|
+
import {
|
|
27370
|
+
BfButtonComponent,
|
|
27371
|
+
BfInputComponent,
|
|
27372
|
+
BfSelectComponent,
|
|
27373
|
+
BfToolbarComponent,
|
|
27374
|
+
type BfSelectOption,
|
|
27375
|
+
} from '@bitforgehq/ui';
|
|
27376
|
+
|
|
27377
|
+
/**
|
|
27378
|
+
* A toolbar at phone width.
|
|
27379
|
+
*
|
|
27380
|
+
* In a phone-wide column the search fills its own row, the two filters share the next one, and
|
|
27381
|
+
* the status and the Refresh action share the last row: the count at the start, the action at the
|
|
27382
|
+
* end, never alone on a row of its own.
|
|
27383
|
+
*/
|
|
27384
|
+
@Component({
|
|
27385
|
+
selector: 'app-example-toolbar-narrow',
|
|
27386
|
+
imports: [BfButtonComponent, BfInputComponent, BfSelectComponent, BfToolbarComponent],
|
|
27387
|
+
template: `
|
|
27388
|
+
<div class="max-w-xs">
|
|
27389
|
+
<bf-toolbar label="Order filters" [status]="count()">
|
|
27390
|
+
<bf-input
|
|
27391
|
+
type="search"
|
|
27392
|
+
ariaLabel="Search orders"
|
|
27393
|
+
placeholder="Search orders"
|
|
27394
|
+
class="w-full"
|
|
27395
|
+
/>
|
|
27396
|
+
<bf-select ariaLabel="Status" [options]="statuses" value="open" />
|
|
27397
|
+
<bf-select ariaLabel="Period" [options]="periods" value="week" />
|
|
27398
|
+
<bf-button bfSlot="actions" variant="outline" (clicked)="refresh()">Refresh</bf-button>
|
|
27399
|
+
</bf-toolbar>
|
|
27400
|
+
</div>
|
|
27401
|
+
`,
|
|
27402
|
+
})
|
|
27403
|
+
export class ToolbarNarrowExampleComponent {
|
|
27404
|
+
protected readonly count = signal('12 orders');
|
|
27405
|
+
protected readonly statuses: BfSelectOption[] = [
|
|
27406
|
+
{ value: 'open', label: 'Open' },
|
|
27407
|
+
{ value: 'shipped', label: 'Shipped' },
|
|
27408
|
+
];
|
|
27409
|
+
protected readonly periods: BfSelectOption[] = [
|
|
27410
|
+
{ value: 'week', label: 'This week' },
|
|
27411
|
+
{ value: 'month', label: 'This month' },
|
|
27412
|
+
];
|
|
27413
|
+
|
|
27414
|
+
protected refresh(): void {
|
|
27415
|
+
this.count.set('12 orders, updated');
|
|
27416
|
+
}
|
|
27417
|
+
}
|
|
27418
|
+
```
|
|
27419
|
+
|
|
26190
27420
|
## Desktop
|
|
26191
27421
|
|
|
26192
27422
|
`bf-toolbar` is a desktop pattern. Every control and field projected into it resolves its unset `size` to `sm` (32 px tall), because its `providers` set `provideBfDefaults({ size: 'sm' })` for its content; a control that sets `size` itself keeps it, and nothing outside the toolbar changes ([density guide](https://bitforge-ui.web.app/density)). Hover, the pointer cursor and the `focus-visible` ring come from each control; the row adds none of its own. Under a fine pointer each control's visual box is its hit area; under a coarse pointer the `sm` touch extensions reach up to 7.5 px past the visual edge, which the 8 px gap keeps off the neighbouring row.
|
|
@@ -26217,7 +27447,7 @@ The `status` element is `role="status"` (an `aria-live="polite"` region), always
|
|
|
26217
27447
|
|
|
26218
27448
|
- `default`: The search field and the filters (bf-input, bf-select, bf-chip-group), in order.
|
|
26219
27449
|
- `view`: A view switch after the filters, such as a bf-segmented of list and grid.
|
|
26220
|
-
- `actions`: The trailing actions, kept together at the inline end of the row.
|
|
27450
|
+
- `actions`: The trailing actions, kept together at the inline end of the row, beside the status.
|
|
26221
27451
|
|
|
26222
27452
|
## Notes
|
|
26223
27453
|
|
|
@@ -26233,6 +27463,14 @@ The `status` element is `role="status"` (an `aria-live="polite"` region), always
|
|
|
26233
27463
|
|
|
26234
27464
|
## Migration notes
|
|
26235
27465
|
|
|
27466
|
+
### 0.3.2
|
|
27467
|
+
|
|
27468
|
+
On a narrow row the actions no longer end up alone on a row of their own: the status and the
|
|
27469
|
+
actions are now one group that takes the rest of the last row and wraps as a whole, with the
|
|
27470
|
+
status at the start and the actions at the inline end. The wide layout is unchanged, and a
|
|
27471
|
+
toolbar without actions keeps the status as a plain item of the row. New `toolbar/narrow`
|
|
27472
|
+
example at phone width.
|
|
27473
|
+
|
|
26236
27474
|
### 0.3.1
|
|
26237
27475
|
|
|
26238
27476
|
New `bf-toolbar` in `@bitforgehq/ui`: a wrapping `role="group"` row for the search field and
|
|
@@ -26759,6 +27997,29 @@ const BF_LABELS: InjectionToken<BfLabels>
|
|
|
26759
27997
|
|
|
26760
27998
|
---
|
|
26761
27999
|
|
|
28000
|
+
# BF_SIDEBAR_STATE_STORAGE
|
|
28001
|
+
|
|
28002
|
+
Injection token for the store that keeps the collapsed sidebar between visits
|
|
28003
|
+
|
|
28004
|
+
Injection token from `@bitforgehq/ui` · since 0.3.2
|
|
28005
|
+
|
|
28006
|
+
Where `BfSidebarStateService` keeps whether the sidebar is collapsed (the key
|
|
28007
|
+
`bf-sidebar-collapsed`, `'true'` or `'false'`): `localStorage` when it is available, otherwise
|
|
28008
|
+
an in-memory map. Provide another store (`sessionStorage`, or an in-memory one in tests) to keep
|
|
28009
|
+
it elsewhere.
|
|
28010
|
+
|
|
28011
|
+
```ts
|
|
28012
|
+
import { BF_SIDEBAR_STATE_STORAGE } from '@bitforgehq/ui';
|
|
28013
|
+
```
|
|
28014
|
+
|
|
28015
|
+
## Signature
|
|
28016
|
+
|
|
28017
|
+
```ts
|
|
28018
|
+
const BF_SIDEBAR_STATE_STORAGE: InjectionToken<Pick<Storage, "getItem" | "setItem">>
|
|
28019
|
+
```
|
|
28020
|
+
|
|
28021
|
+
---
|
|
28022
|
+
|
|
26762
28023
|
# BF_TAB_BAR
|
|
26763
28024
|
|
|
26764
28025
|
Injection token through which each tab reaches its enclosing tab bar
|
|
@@ -26886,6 +28147,68 @@ type BfAlertVariant = 'neutral' | 'success' | 'warning' | 'danger'
|
|
|
26886
28147
|
|
|
26887
28148
|
---
|
|
26888
28149
|
|
|
28150
|
+
# BfAppFrameBreakpoint
|
|
28151
|
+
|
|
28152
|
+
The breakpoint where an app frame switches from its header bar to the sidebar
|
|
28153
|
+
|
|
28154
|
+
Type from `@bitforgehq/ui` · since 0.3.2
|
|
28155
|
+
|
|
28156
|
+
The viewport width from which `bf-app-frame` shows its sidebar column instead of the narrow
|
|
28157
|
+
header bar: Tailwind's `md` (768 px) or `lg` (1024 px).
|
|
28158
|
+
|
|
28159
|
+
```ts
|
|
28160
|
+
import { type BfAppFrameBreakpoint } from '@bitforgehq/ui';
|
|
28161
|
+
```
|
|
28162
|
+
|
|
28163
|
+
## Signature
|
|
28164
|
+
|
|
28165
|
+
```ts
|
|
28166
|
+
type BfAppFrameBreakpoint = 'md' | 'lg'
|
|
28167
|
+
```
|
|
28168
|
+
|
|
28169
|
+
## Values
|
|
28170
|
+
|
|
28171
|
+
- `md`
|
|
28172
|
+
- `lg`
|
|
28173
|
+
|
|
28174
|
+
## Used by
|
|
28175
|
+
|
|
28176
|
+
- [bf-app-frame](../components/app-frame.md)
|
|
28177
|
+
|
|
28178
|
+
---
|
|
28179
|
+
|
|
28180
|
+
# BfAppFrameContentWidth
|
|
28181
|
+
|
|
28182
|
+
The maximum width of an app frame's content column, from medium to full
|
|
28183
|
+
|
|
28184
|
+
Type from `@bitforgehq/ui` · since 0.3.2
|
|
28185
|
+
|
|
28186
|
+
The maximum width of `bf-app-frame`'s content column: `md` 48rem, `lg` 64rem, `xl` 80rem, or
|
|
28187
|
+
`full` for no limit.
|
|
28188
|
+
|
|
28189
|
+
```ts
|
|
28190
|
+
import { type BfAppFrameContentWidth } from '@bitforgehq/ui';
|
|
28191
|
+
```
|
|
28192
|
+
|
|
28193
|
+
## Signature
|
|
28194
|
+
|
|
28195
|
+
```ts
|
|
28196
|
+
type BfAppFrameContentWidth = 'md' | 'lg' | 'xl' | 'full'
|
|
28197
|
+
```
|
|
28198
|
+
|
|
28199
|
+
## Values
|
|
28200
|
+
|
|
28201
|
+
- `md`
|
|
28202
|
+
- `lg`
|
|
28203
|
+
- `xl`
|
|
28204
|
+
- `full`
|
|
28205
|
+
|
|
28206
|
+
## Used by
|
|
28207
|
+
|
|
28208
|
+
- [bf-app-frame](../components/app-frame.md)
|
|
28209
|
+
|
|
28210
|
+
---
|
|
28211
|
+
|
|
26889
28212
|
# BfAutocapitalize
|
|
26890
28213
|
|
|
26891
28214
|
How a phone keyboard capitalises the text typed into a field
|
|
@@ -27555,6 +28878,42 @@ type BfDescriptionListLayout = 'rows' | 'grid'
|
|
|
27555
28878
|
|
|
27556
28879
|
---
|
|
27557
28880
|
|
|
28881
|
+
# BfEmptyStateHeading
|
|
28882
|
+
|
|
28883
|
+
The heading level of an empty state title, one for a whole page
|
|
28884
|
+
|
|
28885
|
+
Type from `@bitforgehq/ui` · since 0.3.2
|
|
28886
|
+
|
|
28887
|
+
The heading level a `bf-empty-state` title takes: a `BfEmptyStateHeadingLevel`, or `1` when the
|
|
28888
|
+
empty state is the page itself (a not-found or no-access page). A new type, so `Record`s over
|
|
28889
|
+
the released `BfEmptyStateHeadingLevel` keep compiling.
|
|
28890
|
+
|
|
28891
|
+
```ts
|
|
28892
|
+
import { type BfEmptyStateHeading } from '@bitforgehq/ui';
|
|
28893
|
+
```
|
|
28894
|
+
|
|
28895
|
+
## Signature
|
|
28896
|
+
|
|
28897
|
+
```ts
|
|
28898
|
+
type BfEmptyStateHeading = BfEmptyStateHeadingLevel | 1
|
|
28899
|
+
```
|
|
28900
|
+
|
|
28901
|
+
## Values
|
|
28902
|
+
|
|
28903
|
+
- `2`
|
|
28904
|
+
- `3`
|
|
28905
|
+
- `1`
|
|
28906
|
+
|
|
28907
|
+
## Uses
|
|
28908
|
+
|
|
28909
|
+
- [BfEmptyStateHeadingLevel](bf-empty-state-heading-level.md)
|
|
28910
|
+
|
|
28911
|
+
## Used by
|
|
28912
|
+
|
|
28913
|
+
- [bf-empty-state](../components/empty-state.md)
|
|
28914
|
+
|
|
28915
|
+
---
|
|
28916
|
+
|
|
27558
28917
|
# BfEmptyStateHeadingLevel
|
|
27559
28918
|
|
|
27560
28919
|
The heading level of an empty state title, two for a screen or three
|
|
@@ -27580,7 +28939,7 @@ type BfEmptyStateHeadingLevel = 2 | 3
|
|
|
27580
28939
|
|
|
27581
28940
|
## Used by
|
|
27582
28941
|
|
|
27583
|
-
- [bf-empty-state
|
|
28942
|
+
- [BfEmptyStateHeading](bf-empty-state-heading.md)
|
|
27584
28943
|
|
|
27585
28944
|
---
|
|
27586
28945
|
|
|
@@ -27975,6 +29334,9 @@ import { type BfLabels } from '@bitforgehq/ui';
|
|
|
27975
29334
|
| `dialog` | `string` | `'Dialog'` | A dialog's or side panel's name without an ariaLabel, a title or a heading. |
|
|
27976
29335
|
| `notifications` | `string` | `'Notifications'` | The desktop toast stack's region name. |
|
|
27977
29336
|
| `tableSelectAll` | `string` | `'Select all rows'` | bf-table's header checkbox, which selects or clears every row. |
|
|
29337
|
+
| `tableSortBy` | `string` | `'Sort by'` | The label of a stacked bf-table's sort select. |
|
|
29338
|
+
| `tableSortAscending` | `(column: string) => string` | `` (column) => `${column}, ascending` `` | A stacked bf-table's sort option for a column in ascending order. |
|
|
29339
|
+
| `tableSortDescending` | `(column: string) => string` | `` (column) => `${column}, descending` `` | A stacked bf-table's sort option for a column in descending order. |
|
|
27978
29340
|
| `theme` | `string` | `'Theme'` | bf-theme-switch's and bf-theme-menu-button's name without a label. |
|
|
27979
29341
|
| `themeSystem` | `string` | `'System'` | The theme choice that follows the system's colour scheme. |
|
|
27980
29342
|
| `themeLight` | `string` | `'Light'` | The theme choice that forces the light colour scheme. |
|
|
@@ -27983,6 +29345,9 @@ import { type BfLabels } from '@bitforgehq/ui';
|
|
|
27983
29345
|
| `breadcrumbExpand` | `(count: number) => string` | `` (count) => (count === 1 ? 'Show 1 more level' : `Show ${count} more levels`) `` | bf-breadcrumb's ellipsis button, which shows the levels a `maxItems` trail hides. |
|
|
27984
29346
|
| `navigation` | `string` | `'Main'` | bf-sidebar's and bf-top-nav's navigation name. |
|
|
27985
29347
|
| `navBadge` | `(count: number) => string` | `` (count) => `${count} new` `` | a[bfNavItem]'s announcement of its count badge. |
|
|
29348
|
+
| `sidebarCollapse` | `string` | `'Collapse sidebar'` | bf-sidebar-toggle's name and tooltip while the sidebar is expanded. |
|
|
29349
|
+
| `sidebarExpand` | `string` | `'Expand sidebar'` | bf-sidebar-toggle's name and tooltip while the sidebar is an icon rail. |
|
|
29350
|
+
| `skipToContent` | `string` | `'Skip to main content'` | bf-app-frame's skip link, the first stop of the tab order, which moves focus to the content. |
|
|
27986
29351
|
| `pagination` | `string` | `'Pagination'` | bf-pagination's navigation name. |
|
|
27987
29352
|
| `previousPage` | `string` | `'Previous page'` | bf-pagination's button to the previous page. |
|
|
27988
29353
|
| `nextPage` | `string` | `'Next page'` | bf-pagination's button to the next page. |
|
|
@@ -28311,6 +29676,30 @@ interface BfSelectOption<T = string>
|
|
|
28311
29676
|
|
|
28312
29677
|
---
|
|
28313
29678
|
|
|
29679
|
+
# BfSidebarStateService
|
|
29680
|
+
|
|
29681
|
+
The sidebar's collapsed state, kept in storage between visits of the app
|
|
29682
|
+
|
|
29683
|
+
Service from `@bitforgehq/ui` · since 0.3.2
|
|
29684
|
+
|
|
29685
|
+
Whether the app's `bf-sidebar` is collapsed to an icon rail, remembered between visits: the app
|
|
29686
|
+
binds the sidebar's `[(collapsed)]` to `sidebarState.collapsed`. With `provideBfSidebarState()`
|
|
29687
|
+
every change is stored in `BF_SIDEBAR_STATE_STORAGE` under `bf-sidebar-collapsed`; without it
|
|
29688
|
+
the signal still holds the state but nothing is written. Storage is read in the browser only, so a
|
|
29689
|
+
server render starts expanded and writes nothing. The widgets never inject it.
|
|
29690
|
+
|
|
29691
|
+
```ts
|
|
29692
|
+
import { BfSidebarStateService } from '@bitforgehq/ui';
|
|
29693
|
+
```
|
|
29694
|
+
|
|
29695
|
+
## Properties
|
|
29696
|
+
|
|
29697
|
+
| Name | Type | Default | Description |
|
|
29698
|
+
| --- | --- | --- | --- |
|
|
29699
|
+
| `collapsed` | `WritableSignal<boolean>` | | Whether the sidebar is collapsed: the stored state in the browser, else false. Bind it two-way to `bf-sidebar`'s `collapsed`. |
|
|
29700
|
+
|
|
29701
|
+
---
|
|
29702
|
+
|
|
28314
29703
|
# BfSize
|
|
28315
29704
|
|
|
28316
29705
|
The three height steps every sized field and control shares
|
|
@@ -28361,6 +29750,7 @@ type BfSize = 'sm' | 'md' | 'lg'
|
|
|
28361
29750
|
- [bf-segmented](../components/segmented.md)
|
|
28362
29751
|
- [BfSegmentedSize](bf-segmented-size.md)
|
|
28363
29752
|
- [bf-select](../components/select.md)
|
|
29753
|
+
- [bf-sidebar-toggle](../components/sidebar-toggle.md)
|
|
28364
29754
|
- [bf-tab-group](../components/tab-group.md)
|
|
28365
29755
|
- [bf-table](../components/table.md)
|
|
28366
29756
|
- [bf-textarea](../components/textarea.md)
|
|
@@ -28432,6 +29822,42 @@ type BfSpinnerSize = 'sm' | 'md' | 'lg'
|
|
|
28432
29822
|
|
|
28433
29823
|
---
|
|
28434
29824
|
|
|
29825
|
+
# BfStatAppearance
|
|
29826
|
+
|
|
29827
|
+
The three stat surfaces, plain, an outlined card or a sunken tile
|
|
29828
|
+
|
|
29829
|
+
Type from `@bitforgehq/ui` · since 0.3.2
|
|
29830
|
+
|
|
29831
|
+
The surface of a `bf-stat`, as its `variant` input takes it: a `BfStatVariant` or tile (a
|
|
29832
|
+
sunken, borderless tile for a stats row inside a raised card). A new type, so `Record`s over
|
|
29833
|
+
the released `BfStatVariant` keep compiling.
|
|
29834
|
+
|
|
29835
|
+
```ts
|
|
29836
|
+
import { type BfStatAppearance } from '@bitforgehq/ui';
|
|
29837
|
+
```
|
|
29838
|
+
|
|
29839
|
+
## Signature
|
|
29840
|
+
|
|
29841
|
+
```ts
|
|
29842
|
+
type BfStatAppearance = BfStatVariant | 'tile'
|
|
29843
|
+
```
|
|
29844
|
+
|
|
29845
|
+
## Values
|
|
29846
|
+
|
|
29847
|
+
- `plain`
|
|
29848
|
+
- `card`
|
|
29849
|
+
- `tile`
|
|
29850
|
+
|
|
29851
|
+
## Uses
|
|
29852
|
+
|
|
29853
|
+
- [BfStatVariant](bf-stat-variant.md)
|
|
29854
|
+
|
|
29855
|
+
## Used by
|
|
29856
|
+
|
|
29857
|
+
- [bf-stat](../components/stat.md)
|
|
29858
|
+
|
|
29859
|
+
---
|
|
29860
|
+
|
|
28435
29861
|
# BfStatTrend
|
|
28436
29862
|
|
|
28437
29863
|
The three directions of a stat's delta pill, up, down or no change
|
|
@@ -28517,7 +29943,7 @@ type BfStatVariant = 'plain' | 'card'
|
|
|
28517
29943
|
|
|
28518
29944
|
## Used by
|
|
28519
29945
|
|
|
28520
|
-
- [bf-stat
|
|
29946
|
+
- [BfStatAppearance](bf-stat-appearance.md)
|
|
28521
29947
|
|
|
28522
29948
|
---
|
|
28523
29949
|
|
|
@@ -28825,6 +30251,38 @@ type BfTableSortDirection = 'ascending' | 'descending'
|
|
|
28825
30251
|
|
|
28826
30252
|
---
|
|
28827
30253
|
|
|
30254
|
+
# BfTableStack
|
|
30255
|
+
|
|
30256
|
+
When a table stacks its rows into cards: never, when narrow, or under touch
|
|
30257
|
+
|
|
30258
|
+
Type from `@bitforgehq/ui` · since 0.3.2
|
|
30259
|
+
|
|
30260
|
+
When a `bf-table` renders each row as a card of label/value pairs: `never` (the default),
|
|
30261
|
+
`narrow` while the table is narrower than 36rem (a container query), or `coarse` under a coarse
|
|
30262
|
+
pointer at any width.
|
|
30263
|
+
|
|
30264
|
+
```ts
|
|
30265
|
+
import { type BfTableStack } from '@bitforgehq/ui';
|
|
30266
|
+
```
|
|
30267
|
+
|
|
30268
|
+
## Signature
|
|
30269
|
+
|
|
30270
|
+
```ts
|
|
30271
|
+
type BfTableStack = 'never' | 'narrow' | 'coarse'
|
|
30272
|
+
```
|
|
30273
|
+
|
|
30274
|
+
## Values
|
|
30275
|
+
|
|
30276
|
+
- `never`
|
|
30277
|
+
- `narrow`
|
|
30278
|
+
- `coarse`
|
|
30279
|
+
|
|
30280
|
+
## Used by
|
|
30281
|
+
|
|
30282
|
+
- [bf-table](../components/table.md)
|
|
30283
|
+
|
|
30284
|
+
---
|
|
30285
|
+
|
|
28828
30286
|
# BfTabLinks
|
|
28829
30287
|
|
|
28830
30288
|
The URLs a router-aware parent gives a tab bar to render its tabs as links
|
|
@@ -29032,9 +30490,10 @@ Function from `@bitforgehq/ui` · since 0.3.0
|
|
|
29032
30490
|
Replaces built-in strings, merged over the parent's (validation merged key by key); a key left
|
|
29033
30491
|
out or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers
|
|
29034
30492
|
every string. In a component's `providers` it covers that sub-tree's widgets and form controls
|
|
29035
|
-
(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `
|
|
29036
|
-
`
|
|
29037
|
-
`
|
|
30493
|
+
(`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,
|
|
30494
|
+
`tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,
|
|
30495
|
+
`breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,
|
|
30496
|
+
`skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,
|
|
29038
30497
|
`calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,
|
|
29039
30498
|
`statTrendUp`, `statTrendDown`, `statTrendFlat`, `comboboxEmpty`, `comboboxRemove`,
|
|
29040
30499
|
`dateInputDay`, `dateInputMonth`, `dateInputYear`, `dateInputOpenCalendar`, `dateInputInvalid`,
|
|
@@ -29072,6 +30531,29 @@ function provideBfLabels(overrides: BfLabelsOverrides): Provider
|
|
|
29072
30531
|
|
|
29073
30532
|
---
|
|
29074
30533
|
|
|
30534
|
+
# provideBfSidebarState
|
|
30535
|
+
|
|
30536
|
+
Remembers the collapsed sidebar between visits, for desktop web apps
|
|
30537
|
+
|
|
30538
|
+
Function from `@bitforgehq/ui` · since 0.3.2
|
|
30539
|
+
|
|
30540
|
+
Lets `BfSidebarStateService` remember the collapsed sidebar: it stores every change of its
|
|
30541
|
+
`collapsed` signal (key `bf-sidebar-collapsed`, `localStorage` in the browser), so the app opens
|
|
30542
|
+
with the sidebar as the user left it. Add it to `ApplicationConfig.providers` and bind the
|
|
30543
|
+
sidebar's `[(collapsed)]` to `sidebarState.collapsed`.
|
|
30544
|
+
|
|
30545
|
+
```ts
|
|
30546
|
+
import { provideBfSidebarState } from '@bitforgehq/ui';
|
|
30547
|
+
```
|
|
30548
|
+
|
|
30549
|
+
## Signature
|
|
30550
|
+
|
|
30551
|
+
```ts
|
|
30552
|
+
function provideBfSidebarState(): EnvironmentProviders
|
|
30553
|
+
```
|
|
30554
|
+
|
|
30555
|
+
---
|
|
30556
|
+
|
|
29075
30557
|
# BF_DESKTOP_TOAST_DEFAULTS
|
|
29076
30558
|
|
|
29077
30559
|
The library defaults of the desktop toast settings, bottom end with three visible
|
|
@@ -35794,6 +37276,41 @@ Blurs the native select, which marks a bound form field touched.
|
|
|
35794
37276
|
|
|
35795
37277
|
---
|
|
35796
37278
|
|
|
37279
|
+
# BfSidebarToggleHarness
|
|
37280
|
+
|
|
37281
|
+
Test harness for a sidebar toggle: reads and flips the collapsed sidebar
|
|
37282
|
+
|
|
37283
|
+
Class from `@bitforgehq/ui/testing` · since 0.3.2
|
|
37284
|
+
|
|
37285
|
+
Harness for `bf-sidebar-toggle`, the button that collapses a `bf-sidebar` to an icon rail. Its
|
|
37286
|
+
`label` filter matches the button's name ("Collapse sidebar" or "Expand sidebar").
|
|
37287
|
+
|
|
37288
|
+
```ts
|
|
37289
|
+
import { BfSidebarToggleHarness } from '@bitforgehq/ui/testing';
|
|
37290
|
+
```
|
|
37291
|
+
|
|
37292
|
+
## Signature
|
|
37293
|
+
|
|
37294
|
+
```ts
|
|
37295
|
+
class BfSidebarToggleHarness extends ComponentHarness
|
|
37296
|
+
```
|
|
37297
|
+
|
|
37298
|
+
## Methods
|
|
37299
|
+
|
|
37300
|
+
### `getLabel(): Promise<string>`
|
|
37301
|
+
|
|
37302
|
+
The button's name, which says what the next press does.
|
|
37303
|
+
|
|
37304
|
+
### `isExpanded(): Promise<boolean>`
|
|
37305
|
+
|
|
37306
|
+
Whether the sidebar is at full width (`aria-expanded`).
|
|
37307
|
+
|
|
37308
|
+
### `toggle(): Promise<void>`
|
|
37309
|
+
|
|
37310
|
+
Flips the sidebar with a click.
|
|
37311
|
+
|
|
37312
|
+
---
|
|
37313
|
+
|
|
35797
37314
|
# BfSidePanelHarness
|
|
35798
37315
|
|
|
35799
37316
|
Test harness for a side panel: its title, its close button and its state
|
|
@@ -35886,7 +37403,7 @@ Class from `@bitforgehq/ui/testing` · since 0.3.1
|
|
|
35886
37403
|
|
|
35887
37404
|
Harness for `bf-table`. It works by column header text and row position, so a test never needs
|
|
35888
37405
|
the column keys or the row keys (except `getSort()`, which returns the column's `key`, as the
|
|
35889
|
-
`sort` model holds it).
|
|
37406
|
+
`sort` model holds it). It reads and drives a stacked table (`stack`) the same way.
|
|
35890
37407
|
|
|
35891
37408
|
```ts
|
|
35892
37409
|
import { BfTableHarness } from '@bitforgehq/ui/testing';
|