@bitforgehq/ui 0.3.0 → 0.3.1
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 +482 -0
- package/README.md +17 -11
- package/ai/MANIFEST.index.json +288 -0
- package/ai/MANIFEST.json +8716 -3926
- package/ai/REFERENCE.index.json +413 -0
- package/ai/REFERENCE.json +4710 -2047
- package/ai/UI_GUIDE.md +185 -57
- package/ai/components/back-button.md +42 -2
- package/ai/components/badge.md +17 -7
- package/ai/components/bar-chart.md +188 -0
- package/ai/components/breadcrumb-item.md +121 -0
- package/ai/components/breadcrumb.md +277 -0
- package/ai/components/button.md +82 -15
- package/ai/components/calendar.md +300 -0
- package/ai/components/card.md +63 -2
- package/ai/components/chart-card.md +283 -0
- package/ai/components/combobox.md +411 -0
- package/ai/components/date-input.md +396 -0
- package/ai/components/datetime-input.md +215 -0
- package/ai/components/description-item.md +111 -0
- package/ai/components/description-list.md +160 -0
- package/ai/components/donut-chart.md +169 -0
- package/ai/components/header.md +67 -2
- package/ai/components/icon-button.md +74 -16
- package/ai/components/icon.md +63 -7
- package/ai/components/line-chart.md +234 -0
- package/ai/components/list.md +70 -6
- package/ai/components/menu.md +5 -0
- package/ai/components/multi-combobox.md +257 -0
- package/ai/components/nav-group.md +178 -0
- package/ai/components/nav-item.md +210 -0
- package/ai/components/page-header.md +237 -0
- package/ai/components/pagination.md +173 -0
- package/ai/components/popover.md +250 -0
- package/ai/components/side-panel.md +391 -0
- package/ai/components/sidebar.md +363 -0
- package/ai/components/skeleton.md +215 -0
- package/ai/components/sparkline.md +188 -0
- package/ai/components/stat.md +233 -0
- package/ai/components/tab-group.md +226 -0
- package/ai/components/tab-panel.md +163 -0
- package/ai/components/table.md +568 -0
- package/ai/components/tabs.md +42 -2
- package/ai/components/theme-menu-button.md +182 -0
- package/ai/components/theme-switch.md +170 -0
- package/ai/components/time-input.md +240 -0
- package/ai/components/toolbar.md +289 -0
- package/ai/components/tooltip.md +230 -0
- package/ai/components/top-nav.md +159 -0
- package/ai/reference/bf-badge-appearance.md +36 -0
- package/ai/reference/bf-badge-variant.md +1 -1
- package/ai/reference/bf-bar-chart-orientation.md +27 -0
- package/ai/reference/bf-button-appearance.md +36 -0
- package/ai/reference/bf-button-variant.md +1 -1
- package/ai/reference/bf-calendar-disabled-date.md +23 -0
- package/ai/reference/bf-calendar-harness.md +51 -0
- package/ai/reference/bf-calendar-weekday.md +31 -0
- package/ai/reference/bf-chart-card-heading-level.md +26 -0
- package/ai/reference/bf-chart-curve.md +28 -0
- package/ai/reference/bf-chart-harness.md +51 -0
- package/ai/reference/bf-chart-legend-position.md +30 -0
- package/ai/reference/bf-chart-series.md +23 -0
- package/ai/reference/bf-chart-value-format.md +23 -0
- package/ai/reference/bf-color-scheme-service.md +30 -0
- package/ai/reference/bf-color-scheme.md +31 -0
- package/ai/reference/bf-combobox-filter.md +27 -0
- package/ai/reference/bf-combobox-harness.md +89 -0
- package/ai/reference/bf-combobox-option.md +33 -0
- package/ai/reference/bf-compare-with.md +2 -0
- package/ai/reference/bf-date-input-harness.md +67 -0
- package/ai/reference/bf-date-input-picker.md +27 -0
- package/ai/reference/bf-date-time-input-harness.md +76 -0
- package/ai/reference/bf-date-time-input-picker.md +27 -0
- package/ai/reference/bf-description-list-layout.md +26 -0
- package/ai/reference/bf-donut-slice.md +22 -0
- package/ai/reference/bf-labels.md +67 -20
- package/ai/reference/bf-menu-button-variant.md +1 -0
- package/ai/reference/bf-menu-placement.md +1 -0
- package/ai/reference/bf-overlay-options.md +28 -27
- package/ai/reference/bf-overlay-placement.md +34 -0
- package/ai/reference/bf-overlay-position.md +1 -1
- package/ai/reference/bf-overlay-service.md +5 -3
- package/ai/reference/bf-page-header-heading-level.md +26 -0
- package/ai/reference/bf-pagination-harness.md +61 -0
- package/ai/reference/bf-platform-service.md +17 -15
- package/ai/reference/bf-popover-harness.md +42 -0
- package/ai/reference/bf-popover-placement.md +28 -0
- package/ai/reference/bf-popover-trigger-directive.md +56 -0
- package/ai/reference/bf-side-panel-config.md +35 -0
- package/ai/reference/bf-side-panel-harness.md +34 -0
- package/ai/reference/bf-side-panel-ref.md +44 -0
- package/ai/reference/bf-side-panel-service.md +43 -0
- package/ai/reference/bf-side-panel-template-context.md +32 -0
- package/ai/reference/bf-side-panel-width.md +28 -0
- package/ai/reference/bf-size.md +12 -0
- package/ai/reference/bf-skeleton-shape.md +28 -0
- package/ai/reference/bf-stat-trend-intent.md +27 -0
- package/ai/reference/bf-stat-trend.md +27 -0
- package/ai/reference/bf-stat-variant.md +26 -0
- package/ai/reference/bf-tab-group-harness.md +44 -0
- package/ai/reference/bf-table-align.md +26 -0
- package/ai/reference/bf-table-cell-context.md +24 -0
- package/ai/reference/bf-table-cell-directive.md +22 -0
- package/ai/reference/bf-table-column.md +28 -0
- package/ai/reference/bf-table-harness.md +77 -0
- package/ai/reference/bf-table-row-key.md +21 -0
- package/ai/reference/bf-table-sort-direction.md +26 -0
- package/ai/reference/bf-table-sort.md +27 -0
- package/ai/reference/bf-theme.md +5 -4
- package/ai/reference/bf-time-input-harness.md +67 -0
- package/ai/reference/bf-time-input-hour-cycle.md +27 -0
- package/ai/reference/bf-time-input-picker.md +27 -0
- package/ai/reference/bf-tooltip-directive.md +43 -0
- package/ai/reference/bf-tooltip-harness.md +43 -0
- package/ai/reference/bf-tooltip-placement.md +28 -0
- package/ai/reference/bf_color_scheme_storage.md +20 -0
- package/ai/reference/bf_side_panel_data.md +17 -0
- package/ai/reference/bf_theme_storage.md +3 -1
- package/ai/reference/provide-bf-color-scheme.md +20 -0
- package/ai/reference/provide-bf-labels.md +12 -4
- package/ai/reference/slide-in-from-end.md +24 -0
- package/ai/reference/slide-out-to-end.md +24 -0
- package/fesm2022/bitforgehq-ui-charts.mjs +3045 -0
- package/fesm2022/bitforgehq-ui-charts.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-icons.mjs +107 -3
- package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-mobile.mjs +14 -59
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +6436 -33
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +618 -1
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +5092 -153
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +25177 -14398
- package/llms.txt +95 -1
- package/package.json +7 -3
- package/theme.css +101 -0
- package/types/bitforgehq-ui-charts.d.ts +967 -0
- package/types/bitforgehq-ui-icons.d.ts +13 -1
- package/types/bitforgehq-ui-mobile.d.ts +25 -18
- package/types/bitforgehq-ui-overlay.d.ts +1844 -7
- package/types/bitforgehq-ui-testing.d.ts +748 -1
- package/types/bitforgehq-ui.d.ts +2136 -44
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# bf-sidebar
|
|
2
|
+
|
|
3
|
+
Vertical app navigation with brand, grouped links, footer and an icon rail mode
|
|
4
|
+
|
|
5
|
+
`BfSidebarComponent` from `@bitforgehq/ui` · Navigation · since 0.3.1
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, computed, signal } from '@angular/core';
|
|
11
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
12
|
+
import {
|
|
13
|
+
BfIconButtonComponent,
|
|
14
|
+
BfNavGroupComponent,
|
|
15
|
+
BfNavItemComponent,
|
|
16
|
+
BfSidebarComponent,
|
|
17
|
+
} from '@bitforgehq/ui';
|
|
18
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
19
|
+
import {
|
|
20
|
+
BookOpen,
|
|
21
|
+
Layers,
|
|
22
|
+
type IconNode,
|
|
23
|
+
PanelLeftClose,
|
|
24
|
+
PanelLeftOpen,
|
|
25
|
+
PanelTop,
|
|
26
|
+
Rows3,
|
|
27
|
+
Settings,
|
|
28
|
+
Smartphone,
|
|
29
|
+
} from 'lucide';
|
|
30
|
+
|
|
31
|
+
interface NavLink {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly icon: IconNode;
|
|
35
|
+
readonly badge?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The bf-sidebar overview.
|
|
40
|
+
*
|
|
41
|
+
* Live demo of `bf-sidebar`: a brand and a collapse toggle in the header, two `bf-nav-group`s of
|
|
42
|
+
* `a[bfNavItem]` router links (this page is the current one, one item has a count badge) and a
|
|
43
|
+
* footer link. The toggle binds `[(collapsed)]`; collapsed, the items show their icon only and
|
|
44
|
+
* name themselves in a tooltip. The result line reports the state.
|
|
45
|
+
*/
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'app-example-sidebar-overview',
|
|
48
|
+
imports: [
|
|
49
|
+
RouterLink,
|
|
50
|
+
RouterLinkActive,
|
|
51
|
+
BfIconComponent,
|
|
52
|
+
BfIconButtonComponent,
|
|
53
|
+
BfNavGroupComponent,
|
|
54
|
+
BfNavItemComponent,
|
|
55
|
+
BfSidebarComponent,
|
|
56
|
+
],
|
|
57
|
+
template: `
|
|
58
|
+
<div class="border-border bg-bg rounded-card flex h-96 overflow-hidden border">
|
|
59
|
+
<bf-sidebar label="Docs" [(collapsed)]="collapsed">
|
|
60
|
+
<div bfSlot="header" class="flex items-center justify-between gap-2">
|
|
61
|
+
@if (!collapsed()) {
|
|
62
|
+
<span class="text-heading text-text ps-3">Atlas</span>
|
|
63
|
+
}
|
|
64
|
+
<bf-icon-button
|
|
65
|
+
variant="text"
|
|
66
|
+
[label]="collapsed() ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
67
|
+
(clicked)="collapsed.set(!collapsed())"
|
|
68
|
+
>
|
|
69
|
+
<bf-icon [icon]="collapsed() ? panelOpen : panelClose" />
|
|
70
|
+
</bf-icon-button>
|
|
71
|
+
</div>
|
|
72
|
+
<bf-nav-group label="Desktop">
|
|
73
|
+
@for (link of desktop; track link.path) {
|
|
74
|
+
<a
|
|
75
|
+
bfNavItem
|
|
76
|
+
[routerLink]="link.path"
|
|
77
|
+
routerLinkActive
|
|
78
|
+
ariaCurrentWhenActive="page"
|
|
79
|
+
[badge]="link.badge ?? null"
|
|
80
|
+
>
|
|
81
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
82
|
+
{{ link.label }}
|
|
83
|
+
</a>
|
|
84
|
+
}
|
|
85
|
+
</bf-nav-group>
|
|
86
|
+
<bf-nav-group label="Shell">
|
|
87
|
+
@for (link of shell; track link.path) {
|
|
88
|
+
<a
|
|
89
|
+
bfNavItem
|
|
90
|
+
[routerLink]="link.path"
|
|
91
|
+
routerLinkActive
|
|
92
|
+
ariaCurrentWhenActive="page"
|
|
93
|
+
[badge]="link.badge ?? null"
|
|
94
|
+
>
|
|
95
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
96
|
+
{{ link.label }}
|
|
97
|
+
</a>
|
|
98
|
+
}
|
|
99
|
+
</bf-nav-group>
|
|
100
|
+
<a
|
|
101
|
+
bfSlot="footer"
|
|
102
|
+
bfNavItem
|
|
103
|
+
routerLink="/theming"
|
|
104
|
+
routerLinkActive
|
|
105
|
+
ariaCurrentWhenActive="page"
|
|
106
|
+
>
|
|
107
|
+
<bf-icon bfSlot="icon" [icon]="settings" />
|
|
108
|
+
Theming
|
|
109
|
+
</a>
|
|
110
|
+
</bf-sidebar>
|
|
111
|
+
<div class="min-w-0 flex-1 p-4">
|
|
112
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
113
|
+
{{ status() }}
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
`,
|
|
118
|
+
})
|
|
119
|
+
export class SidebarOverviewExampleComponent {
|
|
120
|
+
readonly collapsed = signal(false);
|
|
121
|
+
readonly status = computed(() =>
|
|
122
|
+
this.collapsed() ? 'Sidebar collapsed to an icon rail.' : 'Sidebar expanded.',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
protected readonly desktop: readonly NavLink[] = [
|
|
126
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: PanelTop },
|
|
127
|
+
{ path: '/components/top-nav', label: 'Top nav', icon: Rows3 },
|
|
128
|
+
{ path: '/components/nav-group', label: 'Nav group', icon: Layers },
|
|
129
|
+
];
|
|
130
|
+
protected readonly shell: readonly NavLink[] = [
|
|
131
|
+
{ path: '/components/tabs', label: 'Tabs', icon: Smartphone, badge: 3 },
|
|
132
|
+
{ path: '/mobile', label: 'Mobile guide', icon: BookOpen },
|
|
133
|
+
];
|
|
134
|
+
protected readonly settings = Settings;
|
|
135
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
136
|
+
protected readonly panelClose = PanelLeftClose;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Overview
|
|
141
|
+
|
|
142
|
+
The vertical navigation of a desktop app: a `nav` landmark with a `header` slot (the brand),
|
|
143
|
+
the destinations (`a[bfNavItem]` links, optionally in `bf-nav-group`s) and a `footer` slot.
|
|
144
|
+
`collapsed` turns it into an icon rail; the consumer owns the toggle button and binds
|
|
145
|
+
`[(collapsed)]`. Below a breakpoint hide it (`class="hidden md:flex"`) and drive a
|
|
146
|
+
`bf-menu-button` from the same routes.
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
## When to use
|
|
151
|
+
|
|
152
|
+
Use `bf-sidebar` for the top-level destinations of a desktop web app with more than four or five sections, or with sections that fall into groups. Use [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) instead for an app with a handful of sections, and [bf-segmented](https://bitforge-ui.web.app/components/segmented) to switch a view in place. In the Capacitor shell use [bf-tabs](https://bitforge-ui.web.app/components/tabs) with [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar) instead.
|
|
153
|
+
|
|
154
|
+
## Basic usage
|
|
155
|
+
|
|
156
|
+
Put `bfNavItem` on your own `<a routerLink routerLinkActive ariaCurrentWhenActive="page">`: the router sets `aria-current="page"` on the current page's link and the item shows it in the tonal fill. The widget never imports the router. Group the links with `bf-nav-group`, put the brand in `bfSlot="header"` and secondary links in `bfSlot="footer"`. Name the landmark with `label` (it falls back to `provideBfLabels()`'s `navigation`, "Main").
|
|
157
|
+
|
|
158
|
+
## Collapsing to an icon rail
|
|
159
|
+
|
|
160
|
+
`collapsed` is a model: bind `[(collapsed)]` to your own toggle button, such as a text `bf-icon-button` in the header whose label says what the next press does ("Collapse sidebar", "Expand sidebar"). The widget has no toggle of its own, because the button needs an icon and a widget never imports one. 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).
|
|
161
|
+
|
|
162
|
+
## Responsive layouts
|
|
163
|
+
|
|
164
|
+
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`.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { Component } from '@angular/core';
|
|
168
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
169
|
+
import { BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
170
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
171
|
+
import {
|
|
172
|
+
BfMenuButtonComponent,
|
|
173
|
+
BfMenuComponent,
|
|
174
|
+
BfMenuItemDirective,
|
|
175
|
+
} from '@bitforgehq/ui/overlay';
|
|
176
|
+
import { type IconNode, Layers, Menu, PanelTop, Rows3 } from 'lucide';
|
|
177
|
+
|
|
178
|
+
/** One top-level destination of the app. */
|
|
179
|
+
interface Destination {
|
|
180
|
+
readonly path: string;
|
|
181
|
+
readonly label: string;
|
|
182
|
+
readonly icon: IconNode;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* One route array, two surfaces.
|
|
187
|
+
*
|
|
188
|
+
* The same destinations drive the sidebar from the `md` breakpoint up (`hidden md:flex`) and a
|
|
189
|
+
* `bf-menu-button` of `a[bfMenuItem]` router links below it (`md:hidden`). Each surface keeps
|
|
190
|
+
* the router's `routerLinkActive`; resize the window to switch between them.
|
|
191
|
+
*/
|
|
192
|
+
@Component({
|
|
193
|
+
selector: 'app-example-sidebar-responsive',
|
|
194
|
+
imports: [
|
|
195
|
+
RouterLink,
|
|
196
|
+
RouterLinkActive,
|
|
197
|
+
BfIconComponent,
|
|
198
|
+
BfMenuButtonComponent,
|
|
199
|
+
BfMenuComponent,
|
|
200
|
+
BfMenuItemDirective,
|
|
201
|
+
BfNavItemComponent,
|
|
202
|
+
BfSidebarComponent,
|
|
203
|
+
],
|
|
204
|
+
template: `
|
|
205
|
+
<div class="flex flex-col gap-3 md:flex-row">
|
|
206
|
+
<bf-sidebar label="Desktop components" class="hidden md:flex">
|
|
207
|
+
@for (destination of destinations; track destination.path) {
|
|
208
|
+
<a
|
|
209
|
+
bfNavItem
|
|
210
|
+
[routerLink]="destination.path"
|
|
211
|
+
routerLinkActive
|
|
212
|
+
ariaCurrentWhenActive="page"
|
|
213
|
+
>
|
|
214
|
+
<bf-icon bfSlot="icon" [icon]="destination.icon" />
|
|
215
|
+
{{ destination.label }}
|
|
216
|
+
</a>
|
|
217
|
+
}
|
|
218
|
+
</bf-sidebar>
|
|
219
|
+
<bf-menu-button class="md:hidden" [menu]="navMenu">
|
|
220
|
+
<bf-icon bfSlot="icon-start" [icon]="menu" />
|
|
221
|
+
Sections
|
|
222
|
+
</bf-menu-button>
|
|
223
|
+
<p class="text-caption text-text-muted min-w-0 flex-1">
|
|
224
|
+
The page content sits here, beside the sidebar or under the menu button.
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<ng-template #navMenu>
|
|
229
|
+
<bf-menu>
|
|
230
|
+
@for (destination of destinations; track destination.path) {
|
|
231
|
+
<a
|
|
232
|
+
bfMenuItem
|
|
233
|
+
[routerLink]="destination.path"
|
|
234
|
+
routerLinkActive
|
|
235
|
+
ariaCurrentWhenActive="page"
|
|
236
|
+
>{{ destination.label }}</a
|
|
237
|
+
>
|
|
238
|
+
}
|
|
239
|
+
</bf-menu>
|
|
240
|
+
</ng-template>
|
|
241
|
+
`,
|
|
242
|
+
})
|
|
243
|
+
export class SidebarResponsiveExampleComponent {
|
|
244
|
+
/** In an app, derive this from the route config, so a new route appears on every surface. */
|
|
245
|
+
protected readonly destinations: readonly Destination[] = [
|
|
246
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: PanelTop },
|
|
247
|
+
{ path: '/components/top-nav', label: 'Top nav', icon: Rows3 },
|
|
248
|
+
{ path: '/components/nav-group', label: 'Nav group', icon: Layers },
|
|
249
|
+
];
|
|
250
|
+
protected readonly menu = Menu;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Inside the Capacitor shell the same array drives the `bf-tab`s of a `bf-tab-bar` in `bf-tabs`; each destination's path is a tab id and a child route of the tabs.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { Component } from '@angular/core';
|
|
258
|
+
import { BfTabBarComponent, BfTabComponent } from '@bitforgehq/ui';
|
|
259
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
260
|
+
import { BfStackOutletComponent, BfTabsComponent } from '@bitforgehq/ui/mobile';
|
|
261
|
+
import { Folder, type IconNode, Inbox, Settings } from 'lucide';
|
|
262
|
+
|
|
263
|
+
/** One top-level destination: `path` is the route under the tabs, and the tab's id. */
|
|
264
|
+
interface Destination {
|
|
265
|
+
readonly path: string;
|
|
266
|
+
readonly label: string;
|
|
267
|
+
readonly icon: IconNode;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The same destinations inside the Capacitor shell.
|
|
272
|
+
*
|
|
273
|
+
* In the shell the destinations array that drives the desktop sidebar drives a `bf-tab-bar`
|
|
274
|
+
* inside `bf-tabs` instead: each destination's `path` is a child route of `app` and the tab's id.
|
|
275
|
+
* On desktop the same array feeds `a[bfNavItem]` links (`routerLink="/app/{path}"`).
|
|
276
|
+
*
|
|
277
|
+
* @preview none
|
|
278
|
+
*/
|
|
279
|
+
@Component({
|
|
280
|
+
selector: 'app-example-sidebar-shell',
|
|
281
|
+
imports: [
|
|
282
|
+
BfIconComponent,
|
|
283
|
+
BfStackOutletComponent,
|
|
284
|
+
BfTabBarComponent,
|
|
285
|
+
BfTabComponent,
|
|
286
|
+
BfTabsComponent,
|
|
287
|
+
],
|
|
288
|
+
template: `
|
|
289
|
+
<bf-tabs tabsPath="app">
|
|
290
|
+
<bf-stack-outlet />
|
|
291
|
+
<bf-tab-bar>
|
|
292
|
+
@for (destination of destinations; track destination.path) {
|
|
293
|
+
<bf-tab [id]="destination.path" [label]="destination.label"
|
|
294
|
+
><bf-icon [icon]="destination.icon"
|
|
295
|
+
/></bf-tab>
|
|
296
|
+
}
|
|
297
|
+
</bf-tab-bar>
|
|
298
|
+
</bf-tabs>
|
|
299
|
+
`,
|
|
300
|
+
})
|
|
301
|
+
export class SidebarShellExampleComponent {
|
|
302
|
+
protected readonly destinations: readonly Destination[] = [
|
|
303
|
+
{ path: 'inbox', label: 'Inbox', icon: Inbox },
|
|
304
|
+
{ path: 'projects', label: 'Projects', icon: Folder },
|
|
305
|
+
{ path: 'settings', label: 'Settings', icon: Settings },
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Desktop
|
|
311
|
+
|
|
312
|
+
The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the pointer on hover, a `surface-pressed` fill while pressed and the focus ring on keyboard focus only (`focus-visible`); the current page keeps its tonal fill with its own hover and pressed steps. The destinations scroll on their own when they outgrow the height, while the header and footer stay put; a 4 px inset keeps the focus ring inside the scroll box. Item height follows `provideBfDefaults()` (`controlSize`); under a coarse pointer (a touchscreen laptop) every item is at least 44 px tall. Collapsed, the `title` tooltip names each item on hover. A link keeps the browser's behaviour: Ctrl/Cmd-click and a middle click open it in a new tab.
|
|
313
|
+
|
|
314
|
+
## Theming
|
|
315
|
+
|
|
316
|
+
- Surface: `surface` with a `border` line on its inline-end edge (`border-e`); 12 px padding and 24 px between the header, the groups and the footer.
|
|
317
|
+
- Width: `w-60` (240 px) expanded, the icon rail's own width collapsed.
|
|
318
|
+
- The items' and the groups' tokens are on the [nav item](https://bitforge-ui.web.app/components/nav-item#theming) and [nav group](https://bitforge-ui.web.app/components/nav-group#theming) pages.
|
|
319
|
+
|
|
320
|
+
## Accessibility
|
|
321
|
+
|
|
322
|
+
`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. The consumer's toggle button is a native button; name it by what it does ("Collapse sidebar") or give it `aria-pressed`, and keep it in the sidebar or right before it.
|
|
323
|
+
|
|
324
|
+
### Keyboard
|
|
325
|
+
|
|
326
|
+
| Key | Action |
|
|
327
|
+
| ------------------------------------------------ | -------------------------------------------------- |
|
|
328
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the header, the links and the footer |
|
|
329
|
+
| <kbd>Enter</kbd> | Follows the focused link |
|
|
330
|
+
|
|
331
|
+
## Inputs
|
|
332
|
+
|
|
333
|
+
| Name | Type | Default | Description |
|
|
334
|
+
| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
335
|
+
| `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. |
|
|
336
|
+
| `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. Bind `[(collapsed)]` to your own toggle button. |
|
|
337
|
+
|
|
338
|
+
## Slots
|
|
339
|
+
|
|
340
|
+
- `header`: The brand or app switcher above the destinations.
|
|
341
|
+
- `default`: The destinations: `a[bfNavItem]` links and `bf-nav-group`s.
|
|
342
|
+
- `footer`: Content under the destinations, such as settings or the account.
|
|
343
|
+
|
|
344
|
+
## Notes
|
|
345
|
+
|
|
346
|
+
### Do
|
|
347
|
+
|
|
348
|
+
- Mark the current page with `routerLinkActive` and `ariaCurrentWhenActive="page"` on every link.
|
|
349
|
+
- Drive the sidebar, the small-screen menu and the shell's tab bar from one array of destinations.
|
|
350
|
+
|
|
351
|
+
### Don't
|
|
352
|
+
|
|
353
|
+
- Don't put actions (Create, Log out) among the destinations; a sidebar is for places, so put an action in the header or footer as a button.
|
|
354
|
+
- Don't use `bf-sidebar` inside the Capacitor shell; use `bf-tabs` with a `bf-tab-bar` there.
|
|
355
|
+
|
|
356
|
+
## Migration notes
|
|
357
|
+
|
|
358
|
+
### 0.3.1
|
|
359
|
+
|
|
360
|
+
New `bf-sidebar`: the vertical navigation of a desktop app, a `nav` landmark with `header`,
|
|
361
|
+
default and `footer` slots, named by `label` (else the new `navigation` label, "Main").
|
|
362
|
+
`collapsed` (a model the app binds to its own toggle button) turns it into an icon rail. Below a
|
|
363
|
+
breakpoint, hide it with `hidden md:flex` and drive a `bf-menu-button` from the same routes.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# bf-skeleton
|
|
2
|
+
|
|
3
|
+
Loading placeholder shaped as text lines, a rectangle or a circle, with a shimmer
|
|
4
|
+
|
|
5
|
+
`BfSkeletonComponent` from `@bitforgehq/ui` · Feedback · since 0.3.1
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component } from '@angular/core';
|
|
11
|
+
import { BfSkeletonComponent } from '@bitforgehq/ui';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bf-skeleton overview.
|
|
15
|
+
*
|
|
16
|
+
* Live demo of `bf-skeleton`: the three shapes (a circle for an avatar, text lines with a shorter
|
|
17
|
+
* last line, a rect for an image) inside a region marked `aria-busy="true"` while it loads.
|
|
18
|
+
*/
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'app-example-skeleton-overview',
|
|
21
|
+
imports: [BfSkeletonComponent],
|
|
22
|
+
template: `
|
|
23
|
+
<div aria-busy="true" class="flex max-w-md flex-col gap-4">
|
|
24
|
+
<div class="flex items-center gap-3">
|
|
25
|
+
<bf-skeleton shape="circle" />
|
|
26
|
+
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
|
27
|
+
<bf-skeleton width="40%" height="0.875rem" />
|
|
28
|
+
<bf-skeleton width="25%" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<bf-skeleton [lines]="3" height="0.875rem" />
|
|
32
|
+
<bf-skeleton shape="rect" height="8rem" />
|
|
33
|
+
</div>
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
export class SkeletonOverviewExampleComponent {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
Loading placeholder in the shape of the content to come: text lines, a rectangle or a circle,
|
|
42
|
+
on the sunken surface with a shimmer. It is hidden from assistive technology; mark the region it
|
|
43
|
+
stands in for with `aria-busy="true"` while it loads.
|
|
44
|
+
|
|
45
|
+
A skeleton is a placeholder in the shape of the content that is still loading: a few text lines, a rectangle for an image or a chart, a circle for an avatar. It sits on the `surface-sunken` fill with a soft shimmer, so the page keeps its layout while the data arrives and nothing jumps when it does. `bf-skeleton` is hidden from assistive technology; the region it stands in for says that it is busy. It works the same in both shells: in a desktop web app and in the Capacitor shell.
|
|
46
|
+
|
|
47
|
+
## When to use
|
|
48
|
+
|
|
49
|
+
Use `bf-skeleton` while a screen or a section loads content whose shape you know: a card, a list, the rows of a table. Use [bf-spinner](https://bitforge-ui.web.app/components/spinner) instead for a short wait with no known shape, or inside a button, and [bf-progress](https://bitforge-ui.web.app/components/progress) when you can say how far the work is. Once loading has failed, replace the skeletons with a [bf-alert](https://bitforge-ui.web.app/components/alert); when it returned nothing, with a [bf-empty-state](https://bitforge-ui.web.app/components/empty-state).
|
|
50
|
+
|
|
51
|
+
## Shapes and sizes
|
|
52
|
+
|
|
53
|
+
`shape` picks the placeholder. `text` (the default) draws one line, fully rounded; `lines` draws several, the last one shorter, as a paragraph's last line is. `rect` is one bar with the field radius, for an image, a chart or a field. `circle` is a disc for an avatar.
|
|
54
|
+
|
|
55
|
+
`width` and `height` take CSS lengths (`12rem`, `60%`, `48px`). Without a `width` a text or rect skeleton fills its container and a circle is 40 px; a circle's height always follows its width. `height` is the height of each text line (default `0.75rem`) or of the rect (default `6rem`); a circle ignores it. Match the size of the content to come, such as `0.875rem` lines for `text-body`, so the page does not move when it arrives.
|
|
56
|
+
|
|
57
|
+
## A card while it loads
|
|
58
|
+
|
|
59
|
+
Build the placeholder from the same layout as the loaded card: the same `bf-card`, the same gaps, a skeleton where each piece of content goes. Mark the region the skeletons stand in for with `aria-busy="true"` while it loads, and drop the attribute (or set it to `false`) when the content is in.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Component } from '@angular/core';
|
|
63
|
+
import { BfAvatarComponent, BfCardComponent, BfSkeletonComponent } from '@bitforgehq/ui';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A card while it loads, beside the loaded card.
|
|
67
|
+
*
|
|
68
|
+
* The loading card has the loaded one's layout, with a skeleton where each piece of content goes:
|
|
69
|
+
* a 40 px circle for the avatar, two short lines for the name and the role, three lines of text.
|
|
70
|
+
* Only the loading card carries `aria-busy="true"`.
|
|
71
|
+
*/
|
|
72
|
+
@Component({
|
|
73
|
+
selector: 'app-example-skeleton-card',
|
|
74
|
+
imports: [BfCardComponent, BfAvatarComponent, BfSkeletonComponent],
|
|
75
|
+
template: `
|
|
76
|
+
<div class="grid grid-cols-[repeat(auto-fit,minmax(15rem,1fr))] gap-4">
|
|
77
|
+
<bf-card aria-busy="true">
|
|
78
|
+
<div class="flex flex-col gap-4">
|
|
79
|
+
<div class="flex items-center gap-3">
|
|
80
|
+
<bf-skeleton shape="circle" />
|
|
81
|
+
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
|
82
|
+
<bf-skeleton width="55%" height="1rem" />
|
|
83
|
+
<bf-skeleton width="35%" height="0.75rem" />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<bf-skeleton [lines]="3" height="0.875rem" />
|
|
87
|
+
</div>
|
|
88
|
+
</bf-card>
|
|
89
|
+
<bf-card>
|
|
90
|
+
<div class="flex flex-col gap-4">
|
|
91
|
+
<div class="flex items-center gap-3">
|
|
92
|
+
<bf-avatar initials="MK" />
|
|
93
|
+
<div class="flex min-w-0 flex-col">
|
|
94
|
+
<p class="text-label text-text">Mara Keller</p>
|
|
95
|
+
<p class="text-caption text-text-muted">Design lead</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<p class="text-body text-text-muted">
|
|
99
|
+
Runs the Atlas rebrand and the design reviews on Thursdays. Ask her about the type
|
|
100
|
+
scale.
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
</bf-card>
|
|
104
|
+
</div>
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class SkeletonCardExampleComponent {}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Table rows
|
|
111
|
+
|
|
112
|
+
A loading `bf-table` keeps its real header and shows skeleton rows: pass a few placeholder rows and render a `bf-skeleton` in each column through `bfTableCell` templates, with widths that differ from row to row. The header tells the user what is coming; the table's wrapper carries `aria-busy="true"`. In a list, render a few `bf-list-item`s with a circle and two lines the same way.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { Component } from '@angular/core';
|
|
116
|
+
import {
|
|
117
|
+
BfSkeletonComponent,
|
|
118
|
+
BfTableCellDirective,
|
|
119
|
+
BfTableComponent,
|
|
120
|
+
type BfTableColumn,
|
|
121
|
+
} from '@bitforgehq/ui';
|
|
122
|
+
|
|
123
|
+
interface PlaceholderRow {
|
|
124
|
+
id: string;
|
|
125
|
+
/** The width of each column's skeleton in this row, so the rows do not look stamped. */
|
|
126
|
+
widths: Readonly<Record<'title' | 'client' | 'amount', string>>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Skeleton rows in a bf-table.
|
|
131
|
+
*
|
|
132
|
+
* The table keeps its real header while the rows load: four placeholder rows render a
|
|
133
|
+
* `bf-skeleton` in every column through `bfTableCell` templates, at widths that differ from row to
|
|
134
|
+
* row, and the wrapper carries `aria-busy="true"`.
|
|
135
|
+
*/
|
|
136
|
+
@Component({
|
|
137
|
+
selector: 'app-example-skeleton-table',
|
|
138
|
+
imports: [BfTableComponent, BfTableCellDirective, BfSkeletonComponent],
|
|
139
|
+
template: `
|
|
140
|
+
<div aria-busy="true">
|
|
141
|
+
<bf-table label="Invoices" [rows]="rows" [columns]="columns">
|
|
142
|
+
<ng-template bfTableCell="title" [bfTableCellOf]="rows" let-row>
|
|
143
|
+
<bf-skeleton [width]="row.widths.title" height="0.875rem" />
|
|
144
|
+
</ng-template>
|
|
145
|
+
<ng-template bfTableCell="client" [bfTableCellOf]="rows" let-row>
|
|
146
|
+
<bf-skeleton [width]="row.widths.client" height="0.875rem" />
|
|
147
|
+
</ng-template>
|
|
148
|
+
<ng-template bfTableCell="amount" [bfTableCellOf]="rows" let-row>
|
|
149
|
+
<bf-skeleton class="ms-auto" [width]="row.widths.amount" height="0.875rem" />
|
|
150
|
+
</ng-template>
|
|
151
|
+
</bf-table>
|
|
152
|
+
</div>
|
|
153
|
+
`,
|
|
154
|
+
})
|
|
155
|
+
export class SkeletonTableExampleComponent {
|
|
156
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
157
|
+
{ key: 'title', label: 'Invoice' },
|
|
158
|
+
{ key: 'client', label: 'Client' },
|
|
159
|
+
{ key: 'amount', label: 'Amount', align: 'end' },
|
|
160
|
+
];
|
|
161
|
+
readonly rows: readonly PlaceholderRow[] = [
|
|
162
|
+
{ id: 'a', widths: { title: '70%', client: '55%', amount: '4rem' } },
|
|
163
|
+
{ id: 'b', widths: { title: '55%', client: '70%', amount: '3rem' } },
|
|
164
|
+
{ id: 'c', widths: { title: '80%', client: '45%', amount: '4.5rem' } },
|
|
165
|
+
{ id: 'd', widths: { title: '60%', client: '60%', amount: '3.5rem' } },
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Desktop
|
|
171
|
+
|
|
172
|
+
A skeleton is not interactive: no hover, no pointer cursor, no focus stop, and its size does not follow `provideBfDefaults()`. On desktop and in the Capacitor shell it looks and behaves the same. Show skeletons only for a wait that lasts: when a load usually finishes in a few hundred milliseconds, start showing them after a short delay (a timer in the state service), so a fast connection never sees a flash of placeholders.
|
|
173
|
+
|
|
174
|
+
## Theming
|
|
175
|
+
|
|
176
|
+
- Fill: `surface-sunken`. Shimmer: a `surface` highlight at 70 % opacity that sweeps across from the inline start, with `--animate-skeleton-shimmer` (1.6 s, ease-in-out, endless) and `--animate-skeleton-shimmer-rtl` under `dir="rtl"`.
|
|
177
|
+
- Radius: text lines and the circle `rounded-full`; the rect `rounded-field`.
|
|
178
|
+
- Under `prefers-reduced-motion: reduce` the shimmer is hidden and the placeholder stays still. Under forced colours a `GrayText` border draws the shape, as backgrounds are dropped there.
|
|
179
|
+
|
|
180
|
+
## Accessibility
|
|
181
|
+
|
|
182
|
+
Every `bf-skeleton` carries `aria-hidden="true"`: a placeholder has nothing to say, and a screen reader that read out a row of shapes would only make noise. Say that the content is loading on the region it stands in for instead: `aria-busy="true"` on that element tells assistive technology the region is being updated, and dropping it lets the finished content be read. For a load the user started and waits for (a search), also put a short polite status in the page, such as "Loading results", from a `role="status"` element or a `bf-spinner`, which announces its `label` (default "Loading"). The skeleton takes no focus, so keyboard focus stays where the user left it.
|
|
183
|
+
|
|
184
|
+
## Inputs
|
|
185
|
+
|
|
186
|
+
| Name | Type | Default | Description |
|
|
187
|
+
| -------- | -------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `shape` | `BfSkeletonShape` (`'text' \| 'rect' \| 'circle'`) | `'text'` | What it stands in for: `text` lines (default), a `rect` (an image, a chart) or a `circle` (an avatar). |
|
|
189
|
+
| `lines` | `number` | `1` | How many lines a `text` skeleton draws, the last one shorter when there are several; ignored by the other shapes. |
|
|
190
|
+
| `width` | `string` | `''` | Width as a CSS length (`12rem`, `60%`); empty fills the container, a circle is 40 px. A circle's height follows its width. |
|
|
191
|
+
| `height` | `string` | `''` | Height as a CSS length: of each line of `text` (default `0.75rem`) or of the `rect` (default `6rem`); a circle ignores it. |
|
|
192
|
+
|
|
193
|
+
## Notes
|
|
194
|
+
|
|
195
|
+
### Do
|
|
196
|
+
|
|
197
|
+
- Shape the skeleton like the content to come, at its size, so nothing jumps when it loads.
|
|
198
|
+
- Put `aria-busy="true"` on the loading region, and remove it once the content is there.
|
|
199
|
+
- Show a few rows or cards, not a whole page of them.
|
|
200
|
+
|
|
201
|
+
### Don't
|
|
202
|
+
|
|
203
|
+
- Don't show a skeleton for a wait that has no known shape; use a `bf-spinner`.
|
|
204
|
+
- Don't leave skeletons up after a load fails or returns nothing; show the error or the empty state.
|
|
205
|
+
|
|
206
|
+
## Migration notes
|
|
207
|
+
|
|
208
|
+
### 0.3.1
|
|
209
|
+
|
|
210
|
+
New `bf-skeleton` in `@bitforgehq/ui`, a loading placeholder for both shells: `shape` (the new
|
|
211
|
+
`BfSkeletonShape`: `text` lines, a `rect` or a `circle`), `lines` for several text lines with a
|
|
212
|
+
shorter last one, and `width` / `height` as CSS lengths. It draws the `surface-sunken` fill with a
|
|
213
|
+
`surface` shimmer that sweeps from the inline start (hidden under reduced motion) and carries
|
|
214
|
+
`aria-hidden="true"`; the docs mark the loading region with `aria-busy="true"` and show a card and
|
|
215
|
+
a table of skeleton rows.
|