@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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# bf-sidebar-toggle
|
|
2
|
+
|
|
3
|
+
Icon button inside a sidebar that collapses it to an icon rail and back
|
|
4
|
+
|
|
5
|
+
`BfSidebarToggleComponent` from `@bitforgehq/ui/overlay` · Navigation · since 0.3.2
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, computed, signal } from '@angular/core';
|
|
11
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
12
|
+
import { BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
13
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
14
|
+
import { BfSidebarToggleComponent } from '@bitforgehq/ui/overlay';
|
|
15
|
+
import { type IconNode, Layers, PanelLeftClose, PanelLeftOpen, PanelTop } from 'lucide';
|
|
16
|
+
|
|
17
|
+
interface NavLink {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly icon: IconNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The bf-sidebar-toggle overview.
|
|
25
|
+
*
|
|
26
|
+
* Live demo of `bf-sidebar-toggle` in the header of a `bf-sidebar`: a press collapses the
|
|
27
|
+
* sidebar to an icon rail and the next one expands it again. Its name and tooltip say what the
|
|
28
|
+
* next press does, and only the matching icon slot shows. The sidebar's `[(collapsed)]` keeps
|
|
29
|
+
* the state in a signal, and the result line reports it.
|
|
30
|
+
*/
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'app-example-sidebar-toggle-overview',
|
|
33
|
+
imports: [
|
|
34
|
+
RouterLink,
|
|
35
|
+
RouterLinkActive,
|
|
36
|
+
BfIconComponent,
|
|
37
|
+
BfNavItemComponent,
|
|
38
|
+
BfSidebarComponent,
|
|
39
|
+
BfSidebarToggleComponent,
|
|
40
|
+
],
|
|
41
|
+
template: `
|
|
42
|
+
<div class="border-border bg-bg rounded-card flex h-72 overflow-hidden border">
|
|
43
|
+
<bf-sidebar label="Toggle demo" [(collapsed)]="collapsed">
|
|
44
|
+
<div bfSlot="header" class="flex items-center justify-between gap-2">
|
|
45
|
+
@if (!collapsed()) {
|
|
46
|
+
<span class="text-heading text-text ps-3">Atlas</span>
|
|
47
|
+
}
|
|
48
|
+
<bf-sidebar-toggle>
|
|
49
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
50
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
51
|
+
</bf-sidebar-toggle>
|
|
52
|
+
</div>
|
|
53
|
+
@for (link of links; track link.path) {
|
|
54
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
55
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
56
|
+
{{ link.label }}
|
|
57
|
+
</a>
|
|
58
|
+
}
|
|
59
|
+
</bf-sidebar>
|
|
60
|
+
<div class="min-w-0 flex-1 p-4">
|
|
61
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
62
|
+
{{ status() }}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
`,
|
|
67
|
+
})
|
|
68
|
+
export class SidebarToggleOverviewExampleComponent {
|
|
69
|
+
readonly collapsed = signal(false);
|
|
70
|
+
readonly status = computed(() =>
|
|
71
|
+
this.collapsed() ? 'Sidebar collapsed to an icon rail.' : 'Sidebar expanded.',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
protected readonly links: readonly NavLink[] = [
|
|
75
|
+
{ path: '/components/sidebar-toggle', label: 'Sidebar toggle', icon: PanelTop },
|
|
76
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: Layers },
|
|
77
|
+
];
|
|
78
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
79
|
+
protected readonly panelClose = PanelLeftClose;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Overview
|
|
84
|
+
|
|
85
|
+
The collapse button of a `bf-sidebar`: a text icon button placed inside the sidebar (usually in
|
|
86
|
+
its header or footer) that flips the sidebar between full width and an icon rail. Its name and
|
|
87
|
+
tooltip say what the next press does ("Collapse sidebar", "Expand sidebar"), `aria-expanded`
|
|
88
|
+
follows the sidebar and `aria-controls` points at its `nav`. It shows the `collapse-icon` slot
|
|
89
|
+
while expanded and the `expand-icon` slot while collapsed. It reads and writes the sidebar's
|
|
90
|
+
`collapsed` model, so `[(collapsed)]` on the sidebar sees every press. Outside a `bf-sidebar`
|
|
91
|
+
(in a `bf-top-nav`, or on its own) it renders nothing.
|
|
92
|
+
|
|
93
|
+
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`.
|
|
94
|
+
|
|
95
|
+
## When to use
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
## Icons
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
## Placement
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Component, computed, signal } from '@angular/core';
|
|
109
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
110
|
+
import { type BfColorScheme, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
111
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
112
|
+
import { BfSidebarToggleComponent, BfThemeMenuButtonComponent } from '@bitforgehq/ui/overlay';
|
|
113
|
+
import {
|
|
114
|
+
Inbox,
|
|
115
|
+
type IconNode,
|
|
116
|
+
Moon,
|
|
117
|
+
PanelLeftClose,
|
|
118
|
+
PanelLeftOpen,
|
|
119
|
+
Settings,
|
|
120
|
+
Sun,
|
|
121
|
+
SunMoon,
|
|
122
|
+
Users,
|
|
123
|
+
} from 'lucide';
|
|
124
|
+
|
|
125
|
+
interface NavLink {
|
|
126
|
+
readonly path: string;
|
|
127
|
+
readonly label: string;
|
|
128
|
+
readonly icon: IconNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A sidebar footer: settings, theme, toggle and version.
|
|
133
|
+
*
|
|
134
|
+
* The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
|
|
135
|
+
* `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
|
|
136
|
+
* buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
|
|
137
|
+
* line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
|
|
138
|
+
* `BfColorSchemeService.scheme`.
|
|
139
|
+
*/
|
|
140
|
+
@Component({
|
|
141
|
+
selector: 'app-example-sidebar-footer',
|
|
142
|
+
imports: [
|
|
143
|
+
RouterLink,
|
|
144
|
+
RouterLinkActive,
|
|
145
|
+
BfIconComponent,
|
|
146
|
+
BfNavItemComponent,
|
|
147
|
+
BfSidebarComponent,
|
|
148
|
+
BfSidebarToggleComponent,
|
|
149
|
+
BfThemeMenuButtonComponent,
|
|
150
|
+
],
|
|
151
|
+
template: `
|
|
152
|
+
<div class="border-border bg-bg rounded-card flex h-[26rem] overflow-hidden border">
|
|
153
|
+
<bf-sidebar label="Footer demo" [(collapsed)]="collapsed">
|
|
154
|
+
@for (link of links; track link.path) {
|
|
155
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
156
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
157
|
+
{{ link.label }}
|
|
158
|
+
</a>
|
|
159
|
+
}
|
|
160
|
+
<a
|
|
161
|
+
bfSlot="footer"
|
|
162
|
+
bfNavItem
|
|
163
|
+
routerLink="/theming"
|
|
164
|
+
routerLinkActive
|
|
165
|
+
ariaCurrentWhenActive="page"
|
|
166
|
+
>
|
|
167
|
+
<bf-icon bfSlot="icon" [icon]="settings" />
|
|
168
|
+
Settings
|
|
169
|
+
</a>
|
|
170
|
+
<div bfSlot="footer" [class]="rowClasses()">
|
|
171
|
+
<bf-theme-menu-button iconOnly variant="text" placement="above-start" [(value)]="scheme">
|
|
172
|
+
<bf-icon bfSlot="icon" [icon]="themeIcon()" />
|
|
173
|
+
</bf-theme-menu-button>
|
|
174
|
+
<bf-sidebar-toggle>
|
|
175
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
176
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
177
|
+
</bf-sidebar-toggle>
|
|
178
|
+
</div>
|
|
179
|
+
<p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
|
|
180
|
+
Version 0.3.2
|
|
181
|
+
</p>
|
|
182
|
+
</bf-sidebar>
|
|
183
|
+
<div class="min-w-0 flex-1 p-4">
|
|
184
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
185
|
+
{{ collapsed() ? 'Icon rail' : 'Expanded' }}, theme {{ scheme() }}.
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
`,
|
|
190
|
+
})
|
|
191
|
+
export class SidebarFooterExampleComponent {
|
|
192
|
+
readonly collapsed = signal(false);
|
|
193
|
+
/** Stands in for `BfColorSchemeService.scheme`. */
|
|
194
|
+
readonly scheme = signal<BfColorScheme>('system');
|
|
195
|
+
|
|
196
|
+
/** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
|
|
197
|
+
protected readonly rowClasses = computed(() =>
|
|
198
|
+
this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
|
|
199
|
+
);
|
|
200
|
+
protected readonly themeIcon = computed(
|
|
201
|
+
() => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
|
|
202
|
+
);
|
|
203
|
+
protected readonly links: readonly NavLink[] = [
|
|
204
|
+
{ path: '/components/nav-item', label: 'Inbox', icon: Inbox },
|
|
205
|
+
{ path: '/components/nav-group', label: 'Team', icon: Users },
|
|
206
|
+
];
|
|
207
|
+
protected readonly settings = Settings;
|
|
208
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
209
|
+
protected readonly panelClose = PanelLeftClose;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Remembering the state
|
|
214
|
+
|
|
215
|
+
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)).
|
|
216
|
+
|
|
217
|
+
## Sizes
|
|
218
|
+
|
|
219
|
+
`size` is `sm` 32, `md` 40 or `lg` 48 px, square; unset it follows `provideBfDefaults()`'s `controlSize`, as `bf-icon-button` does.
|
|
220
|
+
|
|
221
|
+
## Labels
|
|
222
|
+
|
|
223
|
+
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()`.
|
|
224
|
+
|
|
225
|
+
## Testing
|
|
226
|
+
|
|
227
|
+
`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'`.
|
|
228
|
+
|
|
229
|
+
## Desktop
|
|
230
|
+
|
|
231
|
+
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.
|
|
232
|
+
|
|
233
|
+
## Theming
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
## Accessibility
|
|
238
|
+
|
|
239
|
+
- 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.
|
|
240
|
+
- `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`.
|
|
241
|
+
- The projected icons add nothing to the name (`bf-icon` is `aria-hidden`).
|
|
242
|
+
- Collapsing keeps focus on the toggle, and every nav item keeps its accessible name, so nothing a screen reader user relies on moves.
|
|
243
|
+
|
|
244
|
+
### Keyboard
|
|
245
|
+
|
|
246
|
+
| Key | Action |
|
|
247
|
+
| ----------------------------------- | ------------------------------------------------ |
|
|
248
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Collapses or expands the sidebar |
|
|
249
|
+
| <kbd>Escape</kbd> | Hides the tooltip while it shows |
|
|
250
|
+
| <kbd>Tab</kbd> | Moves on to the next control, hiding the tooltip |
|
|
251
|
+
|
|
252
|
+
## Inputs
|
|
253
|
+
|
|
254
|
+
| Name | Type | Default | Description |
|
|
255
|
+
| ------ | ----------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `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). |
|
|
257
|
+
|
|
258
|
+
## Slots
|
|
259
|
+
|
|
260
|
+
- `collapse-icon`: The icon shown while the sidebar is expanded (a panel closing).
|
|
261
|
+
- `expand-icon`: The icon shown while the sidebar is an icon rail (a panel opening).
|
|
262
|
+
|
|
263
|
+
## Notes
|
|
264
|
+
|
|
265
|
+
### Do
|
|
266
|
+
|
|
267
|
+
- Put the toggle inside the `bf-sidebar` it collapses, in its header or footer.
|
|
268
|
+
- Project both icon slots, a panel closing and a panel opening.
|
|
269
|
+
- Bind the sidebar to `BfSidebarStateService.collapsed` with `provideBfSidebarState()`, so the choice survives a reload.
|
|
270
|
+
|
|
271
|
+
### Don't
|
|
272
|
+
|
|
273
|
+
- Don't put it outside a `bf-sidebar`; it renders nothing there. Use a `bf-icon-button` bound to `[(collapsed)]` instead.
|
|
274
|
+
- Don't add your own `aria-label` or tooltip; the toggle names itself by what the next press does.
|
|
275
|
+
|
|
276
|
+
## Migration notes
|
|
277
|
+
|
|
278
|
+
### 0.3.2
|
|
279
|
+
|
|
280
|
+
New `bf-sidebar-toggle` in `@bitforgehq/ui/overlay`: a text icon button placed inside a
|
|
281
|
+
`bf-sidebar` (header or footer) that flips the sidebar between full width and an icon rail by
|
|
282
|
+
writing its `collapsed` model, so `[(collapsed)]` sees every press. Its name and `bfTooltip` say
|
|
283
|
+
what the next press does (new label keys `sidebarCollapse` "Collapse sidebar" and `sidebarExpand`
|
|
284
|
+
"Expand sidebar"); `aria-expanded` follows the sidebar and `aria-controls` points at its `nav`.
|
|
285
|
+
Icons are the `collapse-icon` and `expand-icon` slots, only the matching one shown; `size` as
|
|
286
|
+
`bf-icon-button`. Outside a sidebar (in a `bf-top-nav`, or alone) it renders nothing. A
|
|
287
|
+
`BfSidebarToggleHarness` in `@bitforgehq/ui/testing` (`getLabel()`, `isExpanded()`, `toggle()`).
|
package/ai/components/sidebar.md
CHANGED
|
@@ -141,9 +141,9 @@ export class SidebarOverviewExampleComponent {
|
|
|
141
141
|
|
|
142
142
|
The vertical navigation of a desktop app: a `nav` landmark with a `header` slot (the brand),
|
|
143
143
|
the destinations (`a[bfNavItem]` links, optionally in `bf-nav-group`s) and a `footer` slot.
|
|
144
|
-
`collapsed` turns it into an icon rail
|
|
145
|
-
`[(collapsed)]
|
|
146
|
-
`bf-menu-button` from the same routes.
|
|
144
|
+
`collapsed` turns it into an icon rail: put a `bf-sidebar-toggle` (`@bitforgehq/ui/overlay`) in
|
|
145
|
+
it, or bind `[(collapsed)]` to your own button. Below a breakpoint hide it
|
|
146
|
+
(`class="hidden md:flex"`) and drive a `bf-menu-button` from the same routes.
|
|
147
147
|
|
|
148
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
149
|
|
|
@@ -157,11 +157,178 @@ Put `bfNavItem` on your own `<a routerLink routerLinkActive ariaCurrentWhenActiv
|
|
|
157
157
|
|
|
158
158
|
## Collapsing to an icon rail
|
|
159
159
|
|
|
160
|
-
`collapsed` is a model
|
|
160
|
+
`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).
|
|
161
|
+
|
|
162
|
+
## Remember the collapsed sidebar
|
|
163
|
+
|
|
164
|
+
`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.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { Component, inject } from '@angular/core';
|
|
168
|
+
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
169
|
+
import { BfNavItemComponent, BfSidebarComponent, BfSidebarStateService } from '@bitforgehq/ui';
|
|
170
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
171
|
+
import { BfSidebarToggleComponent } from '@bitforgehq/ui/overlay';
|
|
172
|
+
import { Inbox, PanelLeftClose, PanelLeftOpen } from 'lucide';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A sidebar that opens collapsed or expanded, as the user left it.
|
|
176
|
+
*
|
|
177
|
+
* `[(collapsed)]` binds `BfSidebarStateService.collapsed`; with `provideBfSidebarState()` in the
|
|
178
|
+
* app's providers every change is stored under `bf-sidebar-collapsed` (`localStorage` in the
|
|
179
|
+
* browser) and read back at the next start.
|
|
180
|
+
*
|
|
181
|
+
* @preview none
|
|
182
|
+
*/
|
|
183
|
+
@Component({
|
|
184
|
+
selector: 'app-example-sidebar-remembered',
|
|
185
|
+
imports: [
|
|
186
|
+
RouterLink,
|
|
187
|
+
RouterLinkActive,
|
|
188
|
+
RouterOutlet,
|
|
189
|
+
BfIconComponent,
|
|
190
|
+
BfNavItemComponent,
|
|
191
|
+
BfSidebarComponent,
|
|
192
|
+
BfSidebarToggleComponent,
|
|
193
|
+
],
|
|
194
|
+
template: `
|
|
195
|
+
<div class="flex min-h-dvh">
|
|
196
|
+
<bf-sidebar [(collapsed)]="sidebarState.collapsed" class="sticky top-0 h-dvh">
|
|
197
|
+
<a bfNavItem routerLink="/inbox" routerLinkActive ariaCurrentWhenActive="page">
|
|
198
|
+
<bf-icon bfSlot="icon" [icon]="inbox" />
|
|
199
|
+
Inbox
|
|
200
|
+
</a>
|
|
201
|
+
<bf-sidebar-toggle bfSlot="footer">
|
|
202
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
203
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
204
|
+
</bf-sidebar-toggle>
|
|
205
|
+
</bf-sidebar>
|
|
206
|
+
<main class="min-w-0 flex-1 p-6"><router-outlet /></main>
|
|
207
|
+
</div>
|
|
208
|
+
`,
|
|
209
|
+
})
|
|
210
|
+
export class SidebarRememberedExampleComponent {
|
|
211
|
+
// app.config.ts: providers: [provideBfSidebarState()]
|
|
212
|
+
protected readonly sidebarState = inject(BfSidebarStateService);
|
|
213
|
+
protected readonly inbox = Inbox;
|
|
214
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
215
|
+
protected readonly panelClose = PanelLeftClose;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Footer
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { Component, computed, signal } from '@angular/core';
|
|
225
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
226
|
+
import { type BfColorScheme, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
227
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
228
|
+
import { BfSidebarToggleComponent, BfThemeMenuButtonComponent } from '@bitforgehq/ui/overlay';
|
|
229
|
+
import {
|
|
230
|
+
Inbox,
|
|
231
|
+
type IconNode,
|
|
232
|
+
Moon,
|
|
233
|
+
PanelLeftClose,
|
|
234
|
+
PanelLeftOpen,
|
|
235
|
+
Settings,
|
|
236
|
+
Sun,
|
|
237
|
+
SunMoon,
|
|
238
|
+
Users,
|
|
239
|
+
} from 'lucide';
|
|
240
|
+
|
|
241
|
+
interface NavLink {
|
|
242
|
+
readonly path: string;
|
|
243
|
+
readonly label: string;
|
|
244
|
+
readonly icon: IconNode;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A sidebar footer: settings, theme, toggle and version.
|
|
249
|
+
*
|
|
250
|
+
* The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
|
|
251
|
+
* `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
|
|
252
|
+
* buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
|
|
253
|
+
* line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
|
|
254
|
+
* `BfColorSchemeService.scheme`.
|
|
255
|
+
*/
|
|
256
|
+
@Component({
|
|
257
|
+
selector: 'app-example-sidebar-footer',
|
|
258
|
+
imports: [
|
|
259
|
+
RouterLink,
|
|
260
|
+
RouterLinkActive,
|
|
261
|
+
BfIconComponent,
|
|
262
|
+
BfNavItemComponent,
|
|
263
|
+
BfSidebarComponent,
|
|
264
|
+
BfSidebarToggleComponent,
|
|
265
|
+
BfThemeMenuButtonComponent,
|
|
266
|
+
],
|
|
267
|
+
template: `
|
|
268
|
+
<div class="border-border bg-bg rounded-card flex h-[26rem] overflow-hidden border">
|
|
269
|
+
<bf-sidebar label="Footer demo" [(collapsed)]="collapsed">
|
|
270
|
+
@for (link of links; track link.path) {
|
|
271
|
+
<a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">
|
|
272
|
+
<bf-icon bfSlot="icon" [icon]="link.icon" />
|
|
273
|
+
{{ link.label }}
|
|
274
|
+
</a>
|
|
275
|
+
}
|
|
276
|
+
<a
|
|
277
|
+
bfSlot="footer"
|
|
278
|
+
bfNavItem
|
|
279
|
+
routerLink="/theming"
|
|
280
|
+
routerLinkActive
|
|
281
|
+
ariaCurrentWhenActive="page"
|
|
282
|
+
>
|
|
283
|
+
<bf-icon bfSlot="icon" [icon]="settings" />
|
|
284
|
+
Settings
|
|
285
|
+
</a>
|
|
286
|
+
<div bfSlot="footer" [class]="rowClasses()">
|
|
287
|
+
<bf-theme-menu-button iconOnly variant="text" placement="above-start" [(value)]="scheme">
|
|
288
|
+
<bf-icon bfSlot="icon" [icon]="themeIcon()" />
|
|
289
|
+
</bf-theme-menu-button>
|
|
290
|
+
<bf-sidebar-toggle>
|
|
291
|
+
<bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
|
|
292
|
+
<bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
|
|
293
|
+
</bf-sidebar-toggle>
|
|
294
|
+
</div>
|
|
295
|
+
<p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
|
|
296
|
+
Version 0.3.2
|
|
297
|
+
</p>
|
|
298
|
+
</bf-sidebar>
|
|
299
|
+
<div class="min-w-0 flex-1 p-4">
|
|
300
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted wrap-anywhere">
|
|
301
|
+
{{ collapsed() ? 'Icon rail' : 'Expanded' }}, theme {{ scheme() }}.
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
`,
|
|
306
|
+
})
|
|
307
|
+
export class SidebarFooterExampleComponent {
|
|
308
|
+
readonly collapsed = signal(false);
|
|
309
|
+
/** Stands in for `BfColorSchemeService.scheme`. */
|
|
310
|
+
readonly scheme = signal<BfColorScheme>('system');
|
|
311
|
+
|
|
312
|
+
/** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
|
|
313
|
+
protected readonly rowClasses = computed(() =>
|
|
314
|
+
this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
|
|
315
|
+
);
|
|
316
|
+
protected readonly themeIcon = computed(
|
|
317
|
+
() => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
|
|
318
|
+
);
|
|
319
|
+
protected readonly links: readonly NavLink[] = [
|
|
320
|
+
{ path: '/components/nav-item', label: 'Inbox', icon: Inbox },
|
|
321
|
+
{ path: '/components/nav-group', label: 'Team', icon: Users },
|
|
322
|
+
];
|
|
323
|
+
protected readonly settings = Settings;
|
|
324
|
+
protected readonly panelOpen = PanelLeftOpen;
|
|
325
|
+
protected readonly panelClose = PanelLeftClose;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
161
328
|
|
|
162
329
|
## Responsive layouts
|
|
163
330
|
|
|
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`.
|
|
331
|
+
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.
|
|
165
332
|
|
|
166
333
|
```ts
|
|
167
334
|
import { Component } from '@angular/core';
|
|
@@ -319,7 +486,7 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
319
486
|
|
|
320
487
|
## Accessibility
|
|
321
488
|
|
|
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.
|
|
489
|
+
`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.
|
|
323
490
|
|
|
324
491
|
### Keyboard
|
|
325
492
|
|
|
@@ -330,10 +497,10 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
330
497
|
|
|
331
498
|
## Inputs
|
|
332
499
|
|
|
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.
|
|
500
|
+
| Name | Type | Default | Description |
|
|
501
|
+
| ----------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
502
|
+
| `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. |
|
|
503
|
+
| `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. |
|
|
337
504
|
|
|
338
505
|
## Slots
|
|
339
506
|
|
|
@@ -347,6 +514,7 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
347
514
|
|
|
348
515
|
- Mark the current page with `routerLinkActive` and `ariaCurrentWhenActive="page"` on every link.
|
|
349
516
|
- Drive the sidebar, the small-screen menu and the shell's tab bar from one array of destinations.
|
|
517
|
+
- Remember the collapsed state with `BfSidebarStateService` and `provideBfSidebarState()`.
|
|
350
518
|
|
|
351
519
|
### Don't
|
|
352
520
|
|
|
@@ -355,6 +523,16 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
|
|
|
355
523
|
|
|
356
524
|
## Migration notes
|
|
357
525
|
|
|
526
|
+
### 0.3.2
|
|
527
|
+
|
|
528
|
+
New `BfSidebarStateService` (a `collapsed` signal) and `provideBfSidebarState()` in
|
|
529
|
+
`@bitforgehq/ui`: bind the sidebar's `[(collapsed)]` to `sidebarState.collapsed` and the provider
|
|
530
|
+
stores every change under `bf-sidebar-collapsed` in `BF_SIDEBAR_STATE_STORAGE` (`localStorage` in
|
|
531
|
+
the browser, memory elsewhere), read back at the next start; without the provider nothing is
|
|
532
|
+
written, and on the server nothing is read. The sidebar's `nav` now has an id, for a toggle's
|
|
533
|
+
`aria-controls`. The docs gain "Remember the collapsed sidebar" and a "Footer" composition
|
|
534
|
+
(Settings link, theme menu, toggle, version line) that works expanded and as an icon rail.
|
|
535
|
+
|
|
358
536
|
### 0.3.1
|
|
359
537
|
|
|
360
538
|
New `bf-sidebar`: the vertical navigation of a desktop app, a `nav` landmark with `header`,
|