@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,391 @@
|
|
|
1
|
+
# bf-side-panel
|
|
2
|
+
|
|
3
|
+
Desktop panel sliding in from the end edge, with a title, body and footer actions
|
|
4
|
+
|
|
5
|
+
`BfSidePanelComponent` from `@bitforgehq/ui/overlay` · Overlays · since 0.3.1
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, inject, signal } from '@angular/core';
|
|
11
|
+
import {
|
|
12
|
+
BfButtonComponent,
|
|
13
|
+
BfInputComponent,
|
|
14
|
+
BfListComponent,
|
|
15
|
+
BfListItemComponent,
|
|
16
|
+
} from '@bitforgehq/ui';
|
|
17
|
+
import {
|
|
18
|
+
BF_SIDE_PANEL_DATA,
|
|
19
|
+
BfSidePanelComponent,
|
|
20
|
+
BfSidePanelRef,
|
|
21
|
+
BfSidePanelService,
|
|
22
|
+
} from '@bitforgehq/ui/overlay';
|
|
23
|
+
|
|
24
|
+
/** An invoice of the demo list. */
|
|
25
|
+
export interface DemoInvoice {
|
|
26
|
+
id: string;
|
|
27
|
+
customer: string;
|
|
28
|
+
amount: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The details panel of the demo: two fields, and Cancel and Save in the footer. */
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'app-side-panel-demo-invoice',
|
|
34
|
+
imports: [BfSidePanelComponent, BfButtonComponent, BfInputComponent],
|
|
35
|
+
template: `
|
|
36
|
+
<bf-side-panel [title]="invoice.id">
|
|
37
|
+
<bf-input label="Customer" [(value)]="customer" />
|
|
38
|
+
<bf-input label="Amount" [(value)]="amount" />
|
|
39
|
+
<div bfSlot="footer">
|
|
40
|
+
<bf-button variant="outline" (clicked)="ref.close()">Cancel</bf-button>
|
|
41
|
+
<bf-button (clicked)="save()">Save</bf-button>
|
|
42
|
+
</div>
|
|
43
|
+
</bf-side-panel>
|
|
44
|
+
`,
|
|
45
|
+
})
|
|
46
|
+
export class SidePanelDemoInvoiceComponent {
|
|
47
|
+
protected readonly ref = inject<BfSidePanelRef<DemoInvoice>>(BfSidePanelRef);
|
|
48
|
+
protected readonly invoice = inject<DemoInvoice>(BF_SIDE_PANEL_DATA);
|
|
49
|
+
|
|
50
|
+
protected readonly customer = signal(this.invoice.customer);
|
|
51
|
+
protected readonly amount = signal(this.invoice.amount);
|
|
52
|
+
|
|
53
|
+
protected save(): void {
|
|
54
|
+
this.ref.close({ ...this.invoice, customer: this.customer(), amount: this.amount() });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bf-side-panel overview.
|
|
60
|
+
*
|
|
61
|
+
* Live demo of `bf-side-panel` and `BfSidePanelService`: a row of the invoice list opens its
|
|
62
|
+
* details in a modal side panel from the end edge, the footer saves the edit back into the list,
|
|
63
|
+
* and the result line announces each outcome.
|
|
64
|
+
*/
|
|
65
|
+
@Component({
|
|
66
|
+
selector: 'app-example-side-panel-overview',
|
|
67
|
+
imports: [BfListComponent, BfListItemComponent],
|
|
68
|
+
template: `
|
|
69
|
+
<div class="flex flex-col gap-3">
|
|
70
|
+
<bf-list>
|
|
71
|
+
@for (invoice of invoices(); track invoice.id) {
|
|
72
|
+
<bf-list-item
|
|
73
|
+
[title]="invoice.id"
|
|
74
|
+
[subtitle]="invoice.customer + ' · ' + invoice.amount"
|
|
75
|
+
[interactive]="true"
|
|
76
|
+
[chevron]="true"
|
|
77
|
+
(clicked)="open(invoice)"
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
</bf-list>
|
|
81
|
+
<p data-status class="text-caption text-text-muted" aria-live="polite">{{ status() }}</p>
|
|
82
|
+
</div>
|
|
83
|
+
`,
|
|
84
|
+
})
|
|
85
|
+
export class SidePanelOverviewExampleComponent {
|
|
86
|
+
private readonly panels = inject(BfSidePanelService);
|
|
87
|
+
|
|
88
|
+
readonly invoices = signal<readonly DemoInvoice[]>([
|
|
89
|
+
{ id: 'INV-204', customer: 'Atlas Studio', amount: 'CHF 1,200' },
|
|
90
|
+
{ id: 'INV-205', customer: 'Bloom Bakery', amount: 'CHF 340' },
|
|
91
|
+
{ id: 'INV-206', customer: 'Kite Labs', amount: 'CHF 2,750' },
|
|
92
|
+
]);
|
|
93
|
+
readonly status = signal('Open an invoice to edit it.');
|
|
94
|
+
|
|
95
|
+
async open(invoice: DemoInvoice): Promise<void> {
|
|
96
|
+
const ref = this.panels.open<SidePanelDemoInvoiceComponent, DemoInvoice, DemoInvoice>(
|
|
97
|
+
SidePanelDemoInvoiceComponent,
|
|
98
|
+
{ data: invoice },
|
|
99
|
+
);
|
|
100
|
+
const saved = await ref.afterClosed;
|
|
101
|
+
if (saved) {
|
|
102
|
+
this.invoices.update((list) => list.map((item) => (item.id === saved.id ? saved : item)));
|
|
103
|
+
this.status.set(`Saved ${saved.id}.`);
|
|
104
|
+
} else {
|
|
105
|
+
this.status.set(`${invoice.id} closed without changes.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Overview
|
|
112
|
+
|
|
113
|
+
Layout of a side panel opened with `BfSidePanelService`: a title row with a close button, a
|
|
114
|
+
scrolling body and a footer of actions kept at the bottom. The panel is labelled by the title,
|
|
115
|
+
and the body becomes focusable only while its content overflows, so keyboard users can scroll
|
|
116
|
+
it (a region named by the title).
|
|
117
|
+
|
|
118
|
+
A full-height panel that slides in from the inline end edge, for desktop and web apps, from
|
|
119
|
+
`@bitforgehq/ui/overlay` (needs `@angular/cdk`, no Capacitor). `BfSidePanelService.open()`
|
|
120
|
+
renders your component or template in the panel and returns a typed `BfSidePanelRef`;
|
|
121
|
+
`bf-side-panel` inside the content lays out the title, the close button, the scrolling body and
|
|
122
|
+
the footer actions.
|
|
123
|
+
|
|
124
|
+
## When to use
|
|
125
|
+
|
|
126
|
+
Use `BfSidePanelService` to show or edit one record of a list or a table without leaving it: the
|
|
127
|
+
details of an invoice, a task, a user, or a filter panel. Use [bf-dialog](https://bitforge-ui.web.app/components/dialog)
|
|
128
|
+
instead for a short task or a decision that needs an answer before the page goes on, and
|
|
129
|
+
[bf-menu](https://bitforge-ui.web.app/components/menu) for a few actions. In the Capacitor shell, use
|
|
130
|
+
[bf-sheet](https://bitforge-ui.web.app/components/sheet) or [bf-modal](https://bitforge-ui.web.app/components/modal) instead: a side panel is a desktop
|
|
131
|
+
layout.
|
|
132
|
+
|
|
133
|
+
## Modal and non-modal
|
|
134
|
+
|
|
135
|
+
A side panel is modal by default: a scrim covers the page, focus stays inside the panel, the page
|
|
136
|
+
behind it is inert and does not scroll, and a click on the scrim closes it. Pass `modal: false`
|
|
137
|
+
to keep the page usable beside the panel: no scrim, no focus trap, the page scrolls and takes
|
|
138
|
+
clicks, and the panel is not `aria-modal`. Focus still moves into the panel when it opens, and
|
|
139
|
+
`Escape` closes it while focus is inside it. To show another record in a non-modal panel, close
|
|
140
|
+
the open one and open the next.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { Component, inject, signal } from '@angular/core';
|
|
144
|
+
import { BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
|
|
145
|
+
import {
|
|
146
|
+
BF_SIDE_PANEL_DATA,
|
|
147
|
+
BfSidePanelComponent,
|
|
148
|
+
type BfSidePanelRef,
|
|
149
|
+
BfSidePanelService,
|
|
150
|
+
} from '@bitforgehq/ui/overlay';
|
|
151
|
+
|
|
152
|
+
/** A task of the demo list. */
|
|
153
|
+
export interface DemoTask {
|
|
154
|
+
title: string;
|
|
155
|
+
owner: string;
|
|
156
|
+
due: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The read-only details of a task. */
|
|
160
|
+
@Component({
|
|
161
|
+
selector: 'app-side-panel-demo-task',
|
|
162
|
+
imports: [BfSidePanelComponent],
|
|
163
|
+
template: `
|
|
164
|
+
<bf-side-panel [title]="task.title">
|
|
165
|
+
<p class="text-body text-text-secondary">Owner: {{ task.owner }}</p>
|
|
166
|
+
<p class="text-body text-text-secondary">Due: {{ task.due }}</p>
|
|
167
|
+
</bf-side-panel>
|
|
168
|
+
`,
|
|
169
|
+
})
|
|
170
|
+
export class SidePanelDemoTaskComponent {
|
|
171
|
+
protected readonly task = inject<DemoTask>(BF_SIDE_PANEL_DATA);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A non-modal side panel.
|
|
176
|
+
*
|
|
177
|
+
* `modal: false` adds no scrim and leaves the list usable: pick another task while the panel is
|
|
178
|
+
* open and it shows that one instead.
|
|
179
|
+
*/
|
|
180
|
+
@Component({
|
|
181
|
+
selector: 'app-example-side-panel-non-modal',
|
|
182
|
+
imports: [BfListComponent, BfListItemComponent],
|
|
183
|
+
template: `
|
|
184
|
+
<div class="flex flex-col gap-3">
|
|
185
|
+
<bf-list>
|
|
186
|
+
@for (task of tasks; track task.title) {
|
|
187
|
+
<bf-list-item
|
|
188
|
+
[title]="task.title"
|
|
189
|
+
[subtitle]="task.owner"
|
|
190
|
+
[interactive]="true"
|
|
191
|
+
(clicked)="show(task)"
|
|
192
|
+
/>
|
|
193
|
+
}
|
|
194
|
+
</bf-list>
|
|
195
|
+
<p data-status class="text-caption text-text-muted" aria-live="polite">{{ status() }}</p>
|
|
196
|
+
</div>
|
|
197
|
+
`,
|
|
198
|
+
})
|
|
199
|
+
export class SidePanelNonModalExampleComponent {
|
|
200
|
+
private readonly panels = inject(BfSidePanelService);
|
|
201
|
+
private current: BfSidePanelRef | null = null;
|
|
202
|
+
|
|
203
|
+
readonly tasks: readonly DemoTask[] = [
|
|
204
|
+
{ title: 'Draft the brief', owner: 'Mia Keller', due: 'Friday' },
|
|
205
|
+
{ title: 'Review the logo', owner: 'Jonas Weber', due: 'Monday' },
|
|
206
|
+
];
|
|
207
|
+
readonly status = signal('Pick a task.');
|
|
208
|
+
|
|
209
|
+
async show(task: DemoTask): Promise<void> {
|
|
210
|
+
this.current?.close();
|
|
211
|
+
const ref = this.panels.open(SidePanelDemoTaskComponent, {
|
|
212
|
+
data: task,
|
|
213
|
+
width: 'sm',
|
|
214
|
+
modal: false,
|
|
215
|
+
});
|
|
216
|
+
this.current = ref;
|
|
217
|
+
this.status.set(`Showing ${task.title}.`);
|
|
218
|
+
await ref.afterClosed;
|
|
219
|
+
if (this.current === ref) {
|
|
220
|
+
this.current = null;
|
|
221
|
+
this.status.set('Details closed.');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Widths
|
|
228
|
+
|
|
229
|
+
`width` sets the panel's width on its own scale: `sm` 360 px, `md` 480 px (default), `lg` 640 px.
|
|
230
|
+
Each is capped at the viewport width, so on a narrow window the panel covers the page edge to
|
|
231
|
+
edge. The panel always fills the viewport height; the body scrolls when the content is taller.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { Component, type TemplateRef, inject, viewChild } from '@angular/core';
|
|
235
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
236
|
+
import {
|
|
237
|
+
BfSidePanelComponent,
|
|
238
|
+
BfSidePanelService,
|
|
239
|
+
type BfSidePanelTemplateContext,
|
|
240
|
+
type BfSidePanelWidth,
|
|
241
|
+
} from '@bitforgehq/ui/overlay';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The three side panel widths, from a template.
|
|
245
|
+
*
|
|
246
|
+
* `sm` is 360 px, `md` 480 px and `lg` 640 px, each capped at the viewport. The `ng-template`
|
|
247
|
+
* receives `{ $implicit: ref, data }`.
|
|
248
|
+
*/
|
|
249
|
+
@Component({
|
|
250
|
+
selector: 'app-example-side-panel-widths',
|
|
251
|
+
imports: [BfButtonComponent, BfSidePanelComponent],
|
|
252
|
+
template: `
|
|
253
|
+
<div class="flex flex-wrap gap-2">
|
|
254
|
+
@for (width of widths; track width) {
|
|
255
|
+
<bf-button variant="outline" (clicked)="open(width)">Open {{ width }}</bf-button>
|
|
256
|
+
}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<ng-template #panel let-ref let-data="data">
|
|
260
|
+
<bf-side-panel [title]="'Width ' + data.width">
|
|
261
|
+
<p class="text-body text-text-secondary">{{ data.hint }}</p>
|
|
262
|
+
<div bfSlot="footer"><bf-button (clicked)="ref.close()">Done</bf-button></div>
|
|
263
|
+
</bf-side-panel>
|
|
264
|
+
</ng-template>
|
|
265
|
+
`,
|
|
266
|
+
})
|
|
267
|
+
export class SidePanelWidthsExampleComponent {
|
|
268
|
+
private readonly panels = inject(BfSidePanelService);
|
|
269
|
+
readonly panel =
|
|
270
|
+
viewChild.required<TemplateRef<BfSidePanelTemplateContext<{ width: string; hint: string }>>>(
|
|
271
|
+
'panel',
|
|
272
|
+
);
|
|
273
|
+
readonly widths: readonly BfSidePanelWidth[] = ['sm', 'md', 'lg'];
|
|
274
|
+
|
|
275
|
+
private readonly hints: Record<BfSidePanelWidth, string> = {
|
|
276
|
+
sm: 'A short list of details or filters.',
|
|
277
|
+
md: 'A record with a few fields to edit.',
|
|
278
|
+
lg: 'A longer form or a preview beside the list.',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
open(width: BfSidePanelWidth): void {
|
|
282
|
+
this.panels.open(this.panel(), { width, data: { width, hint: this.hints[width] } });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Header, body and footer
|
|
288
|
+
|
|
289
|
+
`title` renders as the `h2` the panel is labelled by. Anything with `bfSlot="header"` sits after
|
|
290
|
+
the title in the header row (a status badge). The default slot is the body, which scrolls on its
|
|
291
|
+
own; `bfSlot="footer"` is an end-aligned row of actions below the body, kept at the bottom of the
|
|
292
|
+
panel while the body scrolls. The close button (×) at the top end closes with no result.
|
|
293
|
+
|
|
294
|
+
## Templates and results
|
|
295
|
+
|
|
296
|
+
A template opens with the same options; its context is `{ $implicit: ref, data }` (see the
|
|
297
|
+
widths example above). A component injects `BfSidePanelRef` to close itself and
|
|
298
|
+
`BF_SIDE_PANEL_DATA` for its input. `close(result)` resolves `afterClosed` with the result;
|
|
299
|
+
`Escape`, a scrim click, the close button and a navigation to another URL path resolve
|
|
300
|
+
`undefined`. `closed()` turns true once the panel is gone, and `componentInstance` is the content
|
|
301
|
+
component (`null` for a template). The service is a root singleton: pass
|
|
302
|
+
`injector: inject(Injector)` so a feature-level `provideBfLabels()` names the close button
|
|
303
|
+
(`close`) and the fallback (`dialog`).
|
|
304
|
+
|
|
305
|
+
## Desktop
|
|
306
|
+
|
|
307
|
+
Built for desktop: a full-height panel against the end edge, 360 to 640 px wide, beside or over a
|
|
308
|
+
list. It slides in from the end edge in 260 ms and out the same way; under
|
|
309
|
+
`prefers-reduced-motion` it cross-fades instead. Under `dir="rtl"` it opens on the left. The close
|
|
310
|
+
button (×) is a `text` icon button: hover tints it, and its ring shows on keyboard focus only.
|
|
311
|
+
The footer buttons follow `provideBfDefaults()` like any `bf-button`.
|
|
312
|
+
|
|
313
|
+
## Theming
|
|
314
|
+
|
|
315
|
+
- Panel: `bg-surface`, `text-text`, `shadow-floating`, a `border-s` in `border-border` between
|
|
316
|
+
the page and the panel; widths 22.5/30/40 rem capped at `100vw`, full viewport height.
|
|
317
|
+
- Title: `text-heading`; header `px-6 py-4` with a `border-b`, body `px-6 py-4`, footer
|
|
318
|
+
`px-6 py-4` with a `border-t` and `gap-2` between actions.
|
|
319
|
+
- Close button: a `text` `bf-icon-button` (`sm`) with a lucide `X`.
|
|
320
|
+
- Scrim (modal only): `bf-overlay-backdrop` from the overlay core (a black scrim: 40 % in light
|
|
321
|
+
mode, 60 % in dark).
|
|
322
|
+
|
|
323
|
+
## Accessibility
|
|
324
|
+
|
|
325
|
+
The overlay panel is `role="dialog"`, with `aria-modal="true"` only when modal, and a name:
|
|
326
|
+
`ariaLabel` when given; else `aria-labelledby` pointing at the `bf-side-panel` title; else the
|
|
327
|
+
first heading in the content; else the `dialog` label ("Dialog"). Its ids come from the
|
|
328
|
+
library's shared counter, so they never repeat one in the page.
|
|
329
|
+
|
|
330
|
+
On open, focus moves into the panel: to the element marked `autofocus` (or `cdkFocusInitial`),
|
|
331
|
+
else the first field or button of the body, never the ×, which comes last in the tab order. A
|
|
332
|
+
modal panel traps Tab and Shift+Tab inside it and makes the rest of the page `inert`. A
|
|
333
|
+
non-modal panel leaves the page in the tab order: Tab moves out of the panel and back. On close,
|
|
334
|
+
focus returns to the element that opened it; from a non-modal panel only when focus was inside
|
|
335
|
+
it, so focus the user moved to the page stays there. When the body overflows, it becomes
|
|
336
|
+
focusable (`tabindex="0"`, a `role="region"` labelled by the title), so keyboard users can scroll
|
|
337
|
+
it with the arrow keys.
|
|
338
|
+
|
|
339
|
+
### Keyboard
|
|
340
|
+
|
|
341
|
+
| Key | Action |
|
|
342
|
+
| ------------------------------------------------ | ----------------------------------------------------------------- |
|
|
343
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Next / previous focusable element (wrapping inside a modal panel) |
|
|
344
|
+
| <kbd>Escape</kbd> | Closes the panel (a non-modal one only while focus is inside it) |
|
|
345
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Activates the focused button |
|
|
346
|
+
|
|
347
|
+
## Inputs
|
|
348
|
+
|
|
349
|
+
| Name | Type | Default | Description |
|
|
350
|
+
| ------------ | -------- | ------- | ---------------------------------------------------------------------------------------- |
|
|
351
|
+
| `title` | `string` | `''` | Heading of the panel, rendered as the `h2` the side panel is labelled by. |
|
|
352
|
+
| `closeLabel` | `string` | `''` | Accessible name of the close button; empty uses `provideBfLabels()`'s `close` ("Close"). |
|
|
353
|
+
|
|
354
|
+
## Slots
|
|
355
|
+
|
|
356
|
+
- `header`: More header content after the title, such as a status badge.
|
|
357
|
+
- `default`: The panel body.
|
|
358
|
+
- `footer`: The actions, aligned at the end and kept at the bottom.
|
|
359
|
+
|
|
360
|
+
## Notes
|
|
361
|
+
|
|
362
|
+
### Do
|
|
363
|
+
|
|
364
|
+
- Give every side panel a `title` naming the record it shows ("INV-204").
|
|
365
|
+
- Put the cancel action first in the footer and the primary action last.
|
|
366
|
+
- Use `modal: false` for details the user reads while working in the list beside it.
|
|
367
|
+
- Pass `injector: inject(Injector)` when the opener sits under its own `provideBfLabels()`.
|
|
368
|
+
|
|
369
|
+
### Don't
|
|
370
|
+
|
|
371
|
+
- Don't open a side panel for a yes/no decision; use `BfDialogService.confirm()`.
|
|
372
|
+
- Don't stack side panels; close the open one before showing the next record.
|
|
373
|
+
- Don't use a side panel in the Capacitor shell; use a `bf-sheet` or a `bf-modal`.
|
|
374
|
+
|
|
375
|
+
## Migration notes
|
|
376
|
+
|
|
377
|
+
### 0.3.1
|
|
378
|
+
|
|
379
|
+
New `BfSidePanelService` and `bf-side-panel` in `@bitforgehq/ui/overlay`: a full-height panel
|
|
380
|
+
that slides in from the inline end edge (the left under `dir="rtl"`; a cross-fade under reduced
|
|
381
|
+
motion). `open(component | template, { data, width, modal, ariaLabel, injector })` returns a
|
|
382
|
+
`BfSidePanelRef<R, C>` (`close(result)`, `afterClosed`, `closed()`, `componentInstance`); the
|
|
383
|
+
content injects it and `BF_SIDE_PANEL_DATA`, and a template gets `{ $implicit: ref, data }`
|
|
384
|
+
(`BfSidePanelTemplateContext`). `width` is a `BfSidePanelWidth`: `sm` 360 px, `md` 480 px
|
|
385
|
+
(default), `lg` 640 px, capped at the viewport. Modal by default (a scrim that closes it, a focus
|
|
386
|
+
trap, an inert page, no page scroll); `modal: false` keeps the page usable beside it, and
|
|
387
|
+
`Escape` closes it only while focus is inside it. `bf-side-panel` renders the `title` as the
|
|
388
|
+
`h2` the panel is labelled by, a close button named by the `close` label (`closeLabel`), a
|
|
389
|
+
scrolling body and a `footer` slot kept at the bottom. New `BfSidePanelHarness` in
|
|
390
|
+
`@bitforgehq/ui/testing` (`getTitle()`, `close()`, `isOpen()`). Desktop only; in the Capacitor
|
|
391
|
+
shell use `bf-sheet` or `bf-modal`.
|