@bitforgehq/ui 0.3.1 → 0.3.3
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 +157 -0
- package/README.md +9 -8
- package/ai/MANIFEST.index.json +18 -0
- package/ai/MANIFEST.json +380 -31
- package/ai/REFERENCE.index.json +63 -0
- package/ai/REFERENCE.json +265 -7
- package/ai/UI_GUIDE.md +59 -33
- package/ai/components/app-frame.md +335 -0
- package/ai/components/card.md +56 -2
- package/ai/components/empty-state.md +99 -6
- package/ai/components/header.md +14 -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 +152 -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 +7 -0
- package/ai/reference/bf-select-harness.md +4 -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 +7 -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-mobile.mjs +11 -2
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +571 -446
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +22 -2
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +777 -127
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +1605 -43
- package/llms.txt +11 -0
- package/package.json +1 -1
- package/theme.css +13 -0
- package/types/bitforgehq-ui-overlay.d.ts +51 -0
- package/types/bitforgehq-ui-testing.d.ts +49 -1
- package/types/bitforgehq-ui.d.ts +284 -20
package/ai/UI_GUIDE.md
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
|
|
|
@@ -1228,27 +1252,29 @@ notes.
|
|
|
1228
1252
|
|
|
1229
1253
|
### Navigation
|
|
1230
1254
|
|
|
1231
|
-
| Component | Entry point
|
|
1232
|
-
| ---------------------- |
|
|
1233
|
-
| `bf-
|
|
1234
|
-
| `bf-
|
|
1235
|
-
| `
|
|
1236
|
-
| `
|
|
1237
|
-
| `bf-
|
|
1238
|
-
| `bf-
|
|
1239
|
-
| `
|
|
1240
|
-
| `
|
|
1241
|
-
| `bf-page
|
|
1242
|
-
| `bf-
|
|
1243
|
-
| `bf-
|
|
1244
|
-
| `bf-
|
|
1245
|
-
| `bf-
|
|
1246
|
-
| `bf-
|
|
1247
|
-
| `bf-
|
|
1248
|
-
| `bf-tab
|
|
1249
|
-
| `bf-tab-
|
|
1250
|
-
| `bf-
|
|
1251
|
-
| `bf-
|
|
1255
|
+
| Component | Entry point | Purpose |
|
|
1256
|
+
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------- |
|
|
1257
|
+
| `bf-app-frame` | `@bitforgehq/ui` | Desktop app frame with skip link, sticky sidebar, narrow header and main content |
|
|
1258
|
+
| `bf-back-button` | `@bitforgehq/ui/mobile` | Header back button that pops the page stack or falls back to a default route |
|
|
1259
|
+
| `bf-breadcrumb` | `@bitforgehq/ui` | Desktop breadcrumb trail of links to the current page, with optional collapsing |
|
|
1260
|
+
| `li[bfBreadcrumbItem]` | `@bitforgehq/ui` | One level of a breadcrumb trail: a link, or the current page as text |
|
|
1261
|
+
| `bf-content` | `@bitforgehq/ui/mobile` | Page's single scroll container that keeps focused fields clear of the soft keyboard |
|
|
1262
|
+
| `bf-header` | `@bitforgehq/ui/mobile` | Page header toolbar with a large title that collapses into the toolbar on scroll |
|
|
1263
|
+
| `bf-nav-group` | `@bitforgehq/ui` | A titled group of sidebar links with a small heading that names it |
|
|
1264
|
+
| `a[bfNavItem]` | `@bitforgehq/ui` | Navigation link for a sidebar or top nav, with icon, label and count |
|
|
1265
|
+
| `bf-page` | `@bitforgehq/ui/mobile` | Frame for one stacked mobile screen that supplies lifecycle events and shared page state |
|
|
1266
|
+
| `bf-page-header` | `@bitforgehq/ui` | Desktop page header with breadcrumb, title, subtitle, badges and a row of actions |
|
|
1267
|
+
| `bf-pagination` | `@bitforgehq/ui` | Previous, next and numbered page buttons for a long list or table |
|
|
1268
|
+
| `bf-refresher` | `@bitforgehq/ui/mobile` | Pull-to-refresh indicator with haptic feedback, native iOS bounce and an Android-style pull |
|
|
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 |
|
|
1271
|
+
| `bf-stack-outlet` | `@bitforgehq/ui/mobile` | Router outlet keeping a live page stack with slide transitions and iOS swipe back |
|
|
1272
|
+
| `bf-tab` | `@bitforgehq/ui` | One tab bar destination with an icon, a label and an optional count badge |
|
|
1273
|
+
| `bf-tab-bar` | `@bitforgehq/ui` | Floating pill tab bar for mobile apps with arrow-key selection and count badges |
|
|
1274
|
+
| `bf-tab-group` | `@bitforgehq/ui` | In-page tabs that switch between content panels, with keyboard navigation and badges |
|
|
1275
|
+
| `bf-tab-panel` | `@bitforgehq/ui` | One tab of a tab group with its label, badge, icon and lazy content |
|
|
1276
|
+
| `bf-tabs` | `@bitforgehq/ui/mobile` | Tabs page with one navigation stack per tab, wiring the tab bar to the router |
|
|
1277
|
+
| `bf-top-nav` | `@bitforgehq/ui` | Horizontal app navigation, a wrapping row of links for apps with few sections |
|
|
1252
1278
|
|
|
1253
1279
|
### Overlays
|
|
1254
1280
|
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# bf-app-frame
|
|
2
|
+
|
|
3
|
+
Desktop app frame with skip link, sticky sidebar, narrow header and main content
|
|
4
|
+
|
|
5
|
+
`BfAppFrameComponent` from `@bitforgehq/ui` · Navigation · since 0.3.2
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, signal } from '@angular/core';
|
|
11
|
+
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
12
|
+
import {
|
|
13
|
+
BfAppFrameComponent,
|
|
14
|
+
BfIconButtonComponent,
|
|
15
|
+
BfNavItemComponent,
|
|
16
|
+
BfSidebarComponent,
|
|
17
|
+
} from '@bitforgehq/ui';
|
|
18
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
19
|
+
import {
|
|
20
|
+
BfMenuButtonComponent,
|
|
21
|
+
BfMenuComponent,
|
|
22
|
+
BfMenuItemDirective,
|
|
23
|
+
} from '@bitforgehq/ui/overlay';
|
|
24
|
+
import { Bell, type IconNode, Layers, Menu, PanelLeft, PanelTop } from 'lucide';
|
|
25
|
+
|
|
26
|
+
/** One top-level destination of the app, shared by the sidebar and the narrow-screen menu. */
|
|
27
|
+
interface Destination {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly icon: IconNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The bf-app-frame overview.
|
|
35
|
+
*
|
|
36
|
+
* Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the
|
|
37
|
+
* `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`, at the header's inline
|
|
38
|
+
* start; the brand follows it, and a notifications action sits at the end. Tab into the frame to reveal its skip link; Enter moves focus
|
|
39
|
+
* to the content. The result line reports it.
|
|
40
|
+
*/
|
|
41
|
+
@Component({
|
|
42
|
+
selector: 'app-example-app-frame-overview',
|
|
43
|
+
imports: [
|
|
44
|
+
RouterLink,
|
|
45
|
+
RouterLinkActive,
|
|
46
|
+
BfAppFrameComponent,
|
|
47
|
+
BfIconButtonComponent,
|
|
48
|
+
BfIconComponent,
|
|
49
|
+
BfMenuButtonComponent,
|
|
50
|
+
BfMenuComponent,
|
|
51
|
+
BfMenuItemDirective,
|
|
52
|
+
BfNavItemComponent,
|
|
53
|
+
BfSidebarComponent,
|
|
54
|
+
],
|
|
55
|
+
template: `
|
|
56
|
+
<!-- In an app the frame is the root of the page; here a fixed height stands in for the
|
|
57
|
+
viewport, so the sidebar and the header bar stick inside the box. -->
|
|
58
|
+
<div (focusin)="onFocus($event)">
|
|
59
|
+
<bf-app-frame
|
|
60
|
+
mainId="app-frame-demo-content"
|
|
61
|
+
class="border-border rounded-card h-96 overflow-auto border"
|
|
62
|
+
>
|
|
63
|
+
<bf-sidebar bfSlot="sidebar" label="App frame demo">
|
|
64
|
+
<span bfSlot="header" class="text-heading ps-3">Atlas</span>
|
|
65
|
+
@for (destination of destinations; track destination.path) {
|
|
66
|
+
<a
|
|
67
|
+
bfNavItem
|
|
68
|
+
[routerLink]="destination.path"
|
|
69
|
+
routerLinkActive
|
|
70
|
+
ariaCurrentWhenActive="page"
|
|
71
|
+
>
|
|
72
|
+
<bf-icon bfSlot="icon" [icon]="destination.icon" />
|
|
73
|
+
{{ destination.label }}
|
|
74
|
+
</a>
|
|
75
|
+
}
|
|
76
|
+
</bf-sidebar>
|
|
77
|
+
|
|
78
|
+
<bf-menu-button bfSlot="menu" variant="text" [menu]="navMenu">
|
|
79
|
+
<bf-icon bfSlot="icon-start" [icon]="menu" />
|
|
80
|
+
Sections
|
|
81
|
+
</bf-menu-button>
|
|
82
|
+
<span bfSlot="brand" class="text-heading">Atlas</span>
|
|
83
|
+
<bf-icon-button
|
|
84
|
+
bfSlot="actions"
|
|
85
|
+
variant="text"
|
|
86
|
+
label="Notifications"
|
|
87
|
+
(clicked)="status.set('Notifications opened.')"
|
|
88
|
+
>
|
|
89
|
+
<bf-icon [icon]="bell" />
|
|
90
|
+
</bf-icon-button>
|
|
91
|
+
|
|
92
|
+
<div class="flex flex-col gap-3">
|
|
93
|
+
<h2 class="text-title">Inbox</h2>
|
|
94
|
+
<p class="text-body text-text-muted max-w-prose">
|
|
95
|
+
The page sits in the content column, beside the sidebar from the md breakpoint up and
|
|
96
|
+
under the header bar below it.
|
|
97
|
+
</p>
|
|
98
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
99
|
+
@for (row of rows; track row) {
|
|
100
|
+
<div class="bg-surface rounded-tile text-body p-4">{{ row }}</div>
|
|
101
|
+
}
|
|
102
|
+
</div>
|
|
103
|
+
</bf-app-frame>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<ng-template #navMenu>
|
|
107
|
+
<bf-menu>
|
|
108
|
+
@for (destination of destinations; track destination.path) {
|
|
109
|
+
<a
|
|
110
|
+
bfMenuItem
|
|
111
|
+
[routerLink]="destination.path"
|
|
112
|
+
routerLinkActive
|
|
113
|
+
ariaCurrentWhenActive="page"
|
|
114
|
+
>{{ destination.label }}</a
|
|
115
|
+
>
|
|
116
|
+
}
|
|
117
|
+
</bf-menu>
|
|
118
|
+
</ng-template>
|
|
119
|
+
`,
|
|
120
|
+
})
|
|
121
|
+
export class AppFrameOverviewExampleComponent {
|
|
122
|
+
/** In an app, derive this from the route config, so a new route appears on every surface. */
|
|
123
|
+
protected readonly destinations: readonly Destination[] = [
|
|
124
|
+
{ path: '/components/app-frame', label: 'App frame', icon: PanelLeft },
|
|
125
|
+
{ path: '/components/sidebar', label: 'Sidebar', icon: PanelTop },
|
|
126
|
+
{ path: '/components/menu-button', label: 'Menu button', icon: Layers },
|
|
127
|
+
];
|
|
128
|
+
protected readonly rows = [
|
|
129
|
+
'Review the brand brief',
|
|
130
|
+
'Plan the launch',
|
|
131
|
+
'Write the release notes',
|
|
132
|
+
];
|
|
133
|
+
protected readonly bell = Bell;
|
|
134
|
+
protected readonly menu = Menu;
|
|
135
|
+
readonly status = signal('Tab into the frame to reveal the skip link.');
|
|
136
|
+
|
|
137
|
+
/** Reports when focus lands on the content, as the skip link moves it there. */
|
|
138
|
+
protected onFocus(event: FocusEvent): void {
|
|
139
|
+
if (event.target instanceof HTMLElement && event.target.id === 'app-frame-demo-content') {
|
|
140
|
+
this.status.set('Skipped to the content.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Overview
|
|
147
|
+
|
|
148
|
+
The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the
|
|
149
|
+
breakpoint up, a header bar with the narrow-screen menu, the brand and actions below it, and
|
|
150
|
+
the `main` landmark with a content column of a set maximum width.
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
## When to use
|
|
155
|
+
|
|
156
|
+
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).
|
|
157
|
+
|
|
158
|
+
## Slots and layout
|
|
159
|
+
|
|
160
|
+
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 menu at the inline start, then the brand, and the actions at the end: the top app bar convention, and the menu comes first in the tab order too. The order is fixed, whatever order the slots have in your template. Keep the brand in the sidebar's own `header` slot too, for the wide layout.
|
|
161
|
+
|
|
162
|
+
## Breakpoint and content width
|
|
163
|
+
|
|
164
|
+
`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.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { Component } from '@angular/core';
|
|
168
|
+
import { RouterOutlet } from '@angular/router';
|
|
169
|
+
import { BfAppFrameComponent, BfNavItemComponent, BfSidebarComponent } from '@bitforgehq/ui';
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A narrow reading column, with the sidebar from lg up.
|
|
173
|
+
*
|
|
174
|
+
* A settings or reading app keeps its lines short: `contentWidth="md"` caps the content column at
|
|
175
|
+
* 48rem (`lg` 64rem, the default `xl` 80rem, `full` no limit), and `breakpoint="lg"` keeps the
|
|
176
|
+
* header bar up to 1024 px, for a sidebar that would crowd a tablet. `mainId` names the main
|
|
177
|
+
* landmark, the skip link's target.
|
|
178
|
+
*
|
|
179
|
+
* @preview none
|
|
180
|
+
*/
|
|
181
|
+
@Component({
|
|
182
|
+
selector: 'app-example-app-frame-content-width',
|
|
183
|
+
imports: [RouterOutlet, BfAppFrameComponent, BfNavItemComponent, BfSidebarComponent],
|
|
184
|
+
template: `
|
|
185
|
+
<bf-app-frame contentWidth="md" breakpoint="lg" mainId="content">
|
|
186
|
+
<bf-sidebar bfSlot="sidebar" label="Settings">
|
|
187
|
+
<a bfNavItem href="/account">Account</a>
|
|
188
|
+
<a bfNavItem href="/billing">Billing</a>
|
|
189
|
+
</bf-sidebar>
|
|
190
|
+
<span bfSlot="brand" class="text-heading">Settings</span>
|
|
191
|
+
<router-outlet />
|
|
192
|
+
</bf-app-frame>
|
|
193
|
+
`,
|
|
194
|
+
})
|
|
195
|
+
export class AppFrameContentWidthExampleComponent {}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Skip link and the main landmark
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
## One array of destinations
|
|
203
|
+
|
|
204
|
+
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:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { Component } from '@angular/core';
|
|
208
|
+
import { BfTabBarComponent, BfTabComponent } from '@bitforgehq/ui';
|
|
209
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
210
|
+
import { BfStackOutletComponent, BfTabsComponent } from '@bitforgehq/ui/mobile';
|
|
211
|
+
import { Folder, type IconNode, Inbox, Settings } from 'lucide';
|
|
212
|
+
|
|
213
|
+
/** One top-level destination: `path` is the route under the tabs, and the tab's id. */
|
|
214
|
+
interface Destination {
|
|
215
|
+
readonly path: string;
|
|
216
|
+
readonly label: string;
|
|
217
|
+
readonly icon: IconNode;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The same destinations in the Capacitor shell.
|
|
222
|
+
*
|
|
223
|
+
* Inside `provideBfShell()` there is no `bf-app-frame`: the array that feeds the frame's sidebar
|
|
224
|
+
* and narrow menu drives the `bf-tab`s of a `bf-tab-bar` in `bf-tabs` instead, and each page
|
|
225
|
+
* brings its own `bf-page` with a `bf-header`. Each destination's `path` is a child route of
|
|
226
|
+
* `app` and the tab's id; on desktop the same array feeds `a[bfNavItem]` and `a[bfMenuItem]`
|
|
227
|
+
* links (`routerLink="/app/{path}"`).
|
|
228
|
+
*
|
|
229
|
+
* @preview none
|
|
230
|
+
*/
|
|
231
|
+
@Component({
|
|
232
|
+
selector: 'app-example-app-frame-shell',
|
|
233
|
+
imports: [
|
|
234
|
+
BfIconComponent,
|
|
235
|
+
BfStackOutletComponent,
|
|
236
|
+
BfTabBarComponent,
|
|
237
|
+
BfTabComponent,
|
|
238
|
+
BfTabsComponent,
|
|
239
|
+
],
|
|
240
|
+
template: `
|
|
241
|
+
<bf-tabs tabsPath="app">
|
|
242
|
+
<bf-stack-outlet />
|
|
243
|
+
<bf-tab-bar>
|
|
244
|
+
@for (destination of destinations; track destination.path) {
|
|
245
|
+
<bf-tab [id]="destination.path" [label]="destination.label"
|
|
246
|
+
><bf-icon [icon]="destination.icon"
|
|
247
|
+
/></bf-tab>
|
|
248
|
+
}
|
|
249
|
+
</bf-tab-bar>
|
|
250
|
+
</bf-tabs>
|
|
251
|
+
`,
|
|
252
|
+
})
|
|
253
|
+
export class AppFrameShellExampleComponent {
|
|
254
|
+
protected readonly destinations: readonly Destination[] = [
|
|
255
|
+
{ path: 'inbox', label: 'Inbox', icon: Inbox },
|
|
256
|
+
{ path: 'projects', label: 'Projects', icon: Folder },
|
|
257
|
+
{ path: 'settings', label: 'Settings', icon: Settings },
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Desktop
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
## Theming
|
|
267
|
+
|
|
268
|
+
- Frame: `bg` background, `text` colour.
|
|
269
|
+
- Narrow header bar: `surface` with a `border` line at the bottom, at least 56 px tall.
|
|
270
|
+
- Skip link: `surface` background, `text`, `shadow-floating`, the `control` (pill) radius and the `ring` focus ring.
|
|
271
|
+
- 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`.
|
|
272
|
+
- The sidebar's own tokens are on the [sidebar](https://bitforge-ui.web.app/components/sidebar#theming) page.
|
|
273
|
+
|
|
274
|
+
## Accessibility
|
|
275
|
+
|
|
276
|
+
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.
|
|
277
|
+
|
|
278
|
+
### Keyboard
|
|
279
|
+
|
|
280
|
+
| Key | Action |
|
|
281
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
282
|
+
| <kbd>Tab</kbd> | From the top of the page, focuses the skip link first and reveals it |
|
|
283
|
+
| <kbd>Enter</kbd> | On the skip link, moves focus to the main content |
|
|
284
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the sidebar or the header bar, then the page, in that order |
|
|
285
|
+
|
|
286
|
+
## Inputs
|
|
287
|
+
|
|
288
|
+
| Name | Type | Default | Description |
|
|
289
|
+
| -------------- | ----------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------- |
|
|
290
|
+
| `breakpoint` | `BfAppFrameBreakpoint` (`'md' \| 'lg'`) | `'md'` | The viewport width from which the sidebar column replaces the narrow header bar. |
|
|
291
|
+
| `contentWidth` | `BfAppFrameContentWidth` (`'md' \| 'lg' \| 'xl' \| 'full'`) | `'xl'` | The maximum width of the content column (`md` 48rem, `lg` 64rem, `xl` 80rem, `full`). |
|
|
292
|
+
| `mainId` | `string` | `generated` | The id of the `main` landmark, the skip link's target; generated when left out. |
|
|
293
|
+
|
|
294
|
+
## Slots
|
|
295
|
+
|
|
296
|
+
- `sidebar`: The `bf-sidebar`, shown from the breakpoint up; it fills the column's height.
|
|
297
|
+
- `menu`: The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links, at the header bar's inline start.
|
|
298
|
+
- `brand`: The app's name or logo in the narrow header bar, after the menu.
|
|
299
|
+
- `actions`: End actions of the narrow header bar, such as the theme menu.
|
|
300
|
+
- `default`: The page: what the router shows, inside the `main` landmark.
|
|
301
|
+
|
|
302
|
+
## Notes
|
|
303
|
+
|
|
304
|
+
### Do
|
|
305
|
+
|
|
306
|
+
- Put the frame at the root of the app, with the `router-outlet` as its content, so the sidebar stays put between pages.
|
|
307
|
+
- Drive the sidebar, the narrow menu and the shell's tab bar from one array of destinations.
|
|
308
|
+
- Keep one `h1` per page in the content; the frame renders no heading.
|
|
309
|
+
|
|
310
|
+
### Don't
|
|
311
|
+
|
|
312
|
+
- Don't use `bf-app-frame` inside the Capacitor shell; use `bf-tabs` with a `bf-tab-bar` and `bf-page` there.
|
|
313
|
+
- Don't add your own skip link or hide the `bf-sidebar` with `hidden md:flex` inside the frame; the frame does both.
|
|
314
|
+
|
|
315
|
+
## Migration notes
|
|
316
|
+
|
|
317
|
+
### 0.3.3
|
|
318
|
+
|
|
319
|
+
Fix: the narrow header bar puts the `menu` slot at the inline start, before the `brand` (0.3.2 put
|
|
320
|
+
it after the brand), with the actions at the end as before: the top app bar convention. The menu
|
|
321
|
+
also comes first in the tab order. No API change; an app needs no change unless it relied on the
|
|
322
|
+
brand coming first.
|
|
323
|
+
|
|
324
|
+
### 0.3.2
|
|
325
|
+
|
|
326
|
+
New `bf-app-frame` in `@bitforgehq/ui`: the frame of a desktop web app. Slots `sidebar` (a
|
|
327
|
+
`bf-sidebar`, in a column that sticks to the viewport at full height from the breakpoint up),
|
|
328
|
+
`brand`, `menu` (the narrow-screen `bf-menu-button`) and `actions` (the header bar shown below
|
|
329
|
+
the breakpoint) and the default slot (the page, in the `main` landmark). `breakpoint`
|
|
330
|
+
(`BfAppFrameBreakpoint`: `'md'` default, `'lg'`), `contentWidth` (`BfAppFrameContentWidth`:
|
|
331
|
+
`'md'` 48rem, `'lg'` 64rem, `'xl'` 80rem default, `'full'`) and `mainId`. A skip link, first in
|
|
332
|
+
the tab order and hidden until focused, moves focus to the main landmark by script, so it works
|
|
333
|
+
under `<base href="/">`; new label key `skipToContent` ("Skip to main content"). Inside another
|
|
334
|
+
`main` landmark the content drops its `main` role. Desktop only: the Capacitor shell keeps
|
|
335
|
+
`bf-tabs`.
|
package/ai/components/card.md
CHANGED
|
@@ -95,7 +95,7 @@ Surface for a piece of content: a project summary, a stat, a focus prompt. Four
|
|
|
95
95
|
the design: raised (default), tint, ink and flat. To make the whole card clickable, put a
|
|
96
96
|
`bfAction` link or button in its heading (the stretched-link pattern).
|
|
97
97
|
|
|
98
|
-
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
|
|
98
|
+
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.
|
|
99
99
|
|
|
100
100
|
## When to use
|
|
101
101
|
|
|
@@ -148,6 +148,53 @@ import { BfCardComponent } from '@bitforgehq/ui';
|
|
|
148
148
|
export class CardVariantsExampleComponent {}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
## Nesting surfaces
|
|
152
|
+
|
|
153
|
+
Each level of a page takes one surface style, and a level inside a card steps down, never up:
|
|
154
|
+
|
|
155
|
+
- **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.
|
|
156
|
+
- **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.
|
|
157
|
+
- **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.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { Component } from '@angular/core';
|
|
161
|
+
import { BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
|
|
162
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
163
|
+
import { CircleCheck, Clock, ListTodo } from 'lucide';
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A raised card holding a row of sunken tiles.
|
|
167
|
+
*
|
|
168
|
+
* One surface per level: the card is raised on the page, and the stats inside it are sunken,
|
|
169
|
+
* borderless tiles (`bf-stat variant="tile"`), never outlined surfaces.
|
|
170
|
+
*/
|
|
171
|
+
@Component({
|
|
172
|
+
selector: 'app-example-card-nesting',
|
|
173
|
+
imports: [BfCardComponent, BfIconComponent, BfStatComponent],
|
|
174
|
+
template: `
|
|
175
|
+
<bf-card padding="lg">
|
|
176
|
+
<h3 class="text-heading">This week</h3>
|
|
177
|
+
<div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
178
|
+
<bf-stat label="Open tasks" value="14" variant="tile">
|
|
179
|
+
<bf-icon bfSlot="icon" [icon]="listTodo" />
|
|
180
|
+
</bf-stat>
|
|
181
|
+
<bf-stat label="Done" value="23" variant="tile">
|
|
182
|
+
<bf-icon bfSlot="icon" [icon]="circleCheck" />
|
|
183
|
+
</bf-stat>
|
|
184
|
+
<bf-stat label="Time logged" value="31 h" variant="tile">
|
|
185
|
+
<bf-icon bfSlot="icon" [icon]="clock" />
|
|
186
|
+
</bf-stat>
|
|
187
|
+
</div>
|
|
188
|
+
</bf-card>
|
|
189
|
+
`,
|
|
190
|
+
})
|
|
191
|
+
export class CardNestingExampleComponent {
|
|
192
|
+
protected readonly listTodo = ListTodo;
|
|
193
|
+
protected readonly circleCheck = CircleCheck;
|
|
194
|
+
protected readonly clock = Clock;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
151
198
|
## Footer
|
|
152
199
|
|
|
153
200
|
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.
|
|
@@ -319,13 +366,20 @@ The card renders a plain `<div>` with no role. A clickable card follows the stre
|
|
|
319
366
|
|
|
320
367
|
### Don't
|
|
321
368
|
|
|
322
|
-
- Don't nest raised cards; use `
|
|
369
|
+
- 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.
|
|
370
|
+
- Don't mix surface styles on one level of a dashboard; keep one per level.
|
|
323
371
|
- Don't use `interactive` (deprecated) or wrap a card in an `<a>`; both hide the heading and nest interactive content.
|
|
324
372
|
- Don't put more than one `bfAction` in a card.
|
|
325
373
|
- Don't put a `text` button on an ink card — its primary colour fails contrast on ink; use `tonal` or `outline`.
|
|
326
374
|
|
|
327
375
|
## Migration notes
|
|
328
376
|
|
|
377
|
+
### 0.3.2
|
|
378
|
+
|
|
379
|
+
New "Nesting surfaces" docs section: a raised card holds sunken tiles, never an outlined
|
|
380
|
+
surface (`flat`, a `card` stat), and each level of a dashboard keeps one surface style. New
|
|
381
|
+
`card/nesting` example. No code change.
|
|
382
|
+
|
|
329
383
|
### 0.3.1
|
|
330
384
|
|
|
331
385
|
The footer (`bfSlot="footer"`) sits on the card's bottom edge (`mt-auto`), so in a grid of cards
|