@bitforgehq/ui 0.3.0 → 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 +598 -0
- package/README.md +18 -11
- package/ai/MANIFEST.index.json +306 -0
- package/ai/MANIFEST.json +8915 -3788
- package/ai/REFERENCE.index.json +476 -0
- package/ai/REFERENCE.json +5810 -2905
- package/ai/UI_GUIDE.md +212 -58
- package/ai/components/app-frame.md +328 -0
- 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 +119 -4
- 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/empty-state.md +99 -6
- package/ai/components/header.md +71 -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-toggle.md +287 -0
- package/ai/components/sidebar.md +541 -0
- package/ai/components/skeleton.md +215 -0
- package/ai/components/sparkline.md +188 -0
- package/ai/components/stat.md +297 -0
- package/ai/components/tab-group.md +226 -0
- package/ai/components/tab-panel.md +163 -0
- package/ai/components/table.md +697 -0
- package/ai/components/tabs.md +46 -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 +354 -0
- package/ai/components/tooltip.md +230 -0
- package/ai/components/top-nav.md +159 -0
- 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-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-empty-state-heading-level.md +1 -1
- package/ai/reference/bf-empty-state-heading.md +33 -0
- package/ai/reference/bf-labels.md +73 -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-sidebar-state-service.md +21 -0
- package/ai/reference/bf-sidebar-toggle-harness.md +32 -0
- package/ai/reference/bf-size.md +13 -0
- package/ai/reference/bf-skeleton-shape.md +28 -0
- package/ai/reference/bf-stat-appearance.md +33 -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-table-stack.md +29 -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_sidebar_state_storage.md +20 -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 +13 -4
- package/ai/reference/provide-bf-sidebar-state.md +20 -0
- package/ai/reference/slide-in-from-end.md +24 -0
- package/ai/reference/slide-out-to-end.md +24 -0
- package/bin/bf-ui.js +433 -3
- 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 +6566 -38
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +634 -1
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +5708 -137
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +26766 -14470
- package/llms.txt +106 -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 +1895 -7
- package/types/bitforgehq-ui-testing.d.ts +782 -1
- package/types/bitforgehq-ui.d.ts +2393 -45
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# bf-popover
|
|
2
|
+
|
|
3
|
+
Anchored, non-modal panel for filters, details or a small form
|
|
4
|
+
|
|
5
|
+
`BfPopoverComponent` from `@bitforgehq/ui/overlay` · Overlays · since 0.3.1
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, computed, signal } from '@angular/core';
|
|
11
|
+
import { BfButtonComponent, BfCheckboxComponent } from '@bitforgehq/ui';
|
|
12
|
+
import { BfPopoverComponent, BfPopoverTriggerDirective } from '@bitforgehq/ui/overlay';
|
|
13
|
+
|
|
14
|
+
type Status = 'open' | 'progress' | 'done';
|
|
15
|
+
|
|
16
|
+
const STATUS_LABELS: Record<Status, string> = {
|
|
17
|
+
open: 'Open',
|
|
18
|
+
progress: 'In progress',
|
|
19
|
+
done: 'Done',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The bf-popover overview.
|
|
24
|
+
*
|
|
25
|
+
* Live demo of `bf-popover`: a Filters button opens a small filter form anchored below it. Focus
|
|
26
|
+
* moves to the first checkbox; Apply copies the choices to the task list and closes the popover
|
|
27
|
+
* through the exported trigger, while `Escape` or a click outside closes it without applying. The
|
|
28
|
+
* result line reports the filter in effect.
|
|
29
|
+
*/
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'app-example-popover-overview',
|
|
32
|
+
imports: [BfButtonComponent, BfCheckboxComponent, BfPopoverComponent, BfPopoverTriggerDirective],
|
|
33
|
+
template: `
|
|
34
|
+
<div class="flex flex-col items-start gap-3">
|
|
35
|
+
<bf-button
|
|
36
|
+
variant="outline"
|
|
37
|
+
[bfPopoverTriggerFor]="filters"
|
|
38
|
+
#filtersTrigger="bfPopoverTrigger"
|
|
39
|
+
(bfPopoverOpened)="draft.set(applied())"
|
|
40
|
+
>
|
|
41
|
+
Filters
|
|
42
|
+
</bf-button>
|
|
43
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<ng-template #filters>
|
|
47
|
+
<bf-popover class="w-72">
|
|
48
|
+
<h2 class="text-label text-text mb-3">Filter tasks</h2>
|
|
49
|
+
<div role="group" aria-label="Status" class="flex flex-col gap-2">
|
|
50
|
+
@for (status of statuses; track status) {
|
|
51
|
+
<bf-checkbox
|
|
52
|
+
[label]="labels[status]"
|
|
53
|
+
[checked]="draft().includes(status)"
|
|
54
|
+
(changed)="toggle(status, $event)"
|
|
55
|
+
/>
|
|
56
|
+
}
|
|
57
|
+
</div>
|
|
58
|
+
<div class="mt-4 flex justify-end gap-2">
|
|
59
|
+
<bf-button variant="text" size="sm" (clicked)="draft.set([])">Clear</bf-button>
|
|
60
|
+
<bf-button size="sm" (clicked)="applied.set(draft()); filtersTrigger.close()">
|
|
61
|
+
Apply
|
|
62
|
+
</bf-button>
|
|
63
|
+
</div>
|
|
64
|
+
</bf-popover>
|
|
65
|
+
</ng-template>
|
|
66
|
+
`,
|
|
67
|
+
})
|
|
68
|
+
export class PopoverOverviewExampleComponent {
|
|
69
|
+
protected readonly statuses: readonly Status[] = ['open', 'progress', 'done'];
|
|
70
|
+
protected readonly labels = STATUS_LABELS;
|
|
71
|
+
readonly applied = signal<readonly Status[]>(['open', 'progress']);
|
|
72
|
+
readonly draft = signal<readonly Status[]>(['open', 'progress']);
|
|
73
|
+
|
|
74
|
+
readonly status = computed(() => {
|
|
75
|
+
const applied = this.applied();
|
|
76
|
+
return applied.length
|
|
77
|
+
? `Showing ${applied.map((s) => STATUS_LABELS[s]).join(', ')} tasks.`
|
|
78
|
+
: 'Showing all tasks.';
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
toggle(status: Status, checked: boolean): void {
|
|
82
|
+
this.draft.update((draft) =>
|
|
83
|
+
checked ? [...draft, status] : draft.filter((s) => s !== status),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Overview
|
|
90
|
+
|
|
91
|
+
Non-modal panel anchored to a trigger, for filters, details or a small form (role `dialog`).
|
|
92
|
+
Put it in an `ng-template` that a `[bfPopoverTriggerFor]` opens; it takes the id the trigger's
|
|
93
|
+
`aria-controls` points at and is named by `bfPopoverLabel`, else by its first heading, else by
|
|
94
|
+
the trigger.
|
|
95
|
+
|
|
96
|
+
Anchored panels for desktop and web apps, from `@bitforgehq/ui/overlay` (needs `@angular/cdk`, no
|
|
97
|
+
Capacitor). A `bf-popover` is the panel; put it in an `ng-template` and open it from a button with
|
|
98
|
+
`[bfPopoverTriggerFor]`. It opens beside its trigger, flips when it has no room, and leaves the
|
|
99
|
+
page usable: it is non-modal, so there is no backdrop, focus is not trapped and the page does not
|
|
100
|
+
become inert. It follows the
|
|
101
|
+
[APG disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) pattern with a non-modal
|
|
102
|
+
`dialog` panel. Popovers open in the CDK overlay container, so they work inside a
|
|
103
|
+
`BfDialogService` dialog.
|
|
104
|
+
|
|
105
|
+
Import `BfPopoverComponent` and `BfPopoverTriggerDirective` from `@bitforgehq/ui/overlay`.
|
|
106
|
+
|
|
107
|
+
## When to use
|
|
108
|
+
|
|
109
|
+
Use `bf-popover` for a few fields or details that belong to one control and are used for a moment:
|
|
110
|
+
filters for a list, the details of a value, a small form such as "Rename". Use
|
|
111
|
+
[bf-menu](https://bitforge-ui.web.app/components/menu) instead for a list of actions or view settings,
|
|
112
|
+
[BfDialogService](https://bitforge-ui.web.app/api/bf-dialog-service) for a task that needs the user's full attention or more
|
|
113
|
+
than a few fields, a [bf-tooltip](https://bitforge-ui.web.app/components/tooltip) for a short hint with nothing to interact
|
|
114
|
+
with, and in a Capacitor app a [bf-sheet](https://bitforge-ui.web.app/components/sheet).
|
|
115
|
+
|
|
116
|
+
## Opening and closing
|
|
117
|
+
|
|
118
|
+
The trigger toggles the popover on a click (and on `Enter` or `Space`, which click a button).
|
|
119
|
+
Focus moves to the first control inside (or to the element marked `autofocus`, or to the panel
|
|
120
|
+
when it holds no control). `Escape`, a click outside, `Tab` past the panel's last control or a
|
|
121
|
+
navigation to another URL path close it. `Escape` and a click on nothing focusable put focus back
|
|
122
|
+
on the trigger; a click on another control leaves focus there.
|
|
123
|
+
|
|
124
|
+
Export the trigger as `bfPopoverTrigger` to close the popover from inside it, for example from an
|
|
125
|
+
Apply button: `#trigger="bfPopoverTrigger"`, then `trigger.close()`. It also has `open()`,
|
|
126
|
+
`toggle()` and `isOpen()`. `(bfPopoverOpened)` and `(bfPopoverClosed)` report its state.
|
|
127
|
+
|
|
128
|
+
The trigger can be a native `button` or a `bf-button`: on a host that does not take focus itself,
|
|
129
|
+
the ARIA state goes on the first control inside it.
|
|
130
|
+
|
|
131
|
+
## Placement
|
|
132
|
+
|
|
133
|
+
`bfPopoverPlacement` sets where the panel opens: `'below-start'` (default), `'below-end'`,
|
|
134
|
+
`'above-start'` or `'above-end'`, 4 px from the trigger. `start` and `end` follow the text
|
|
135
|
+
direction. When the panel would overflow the viewport, it flips to the other side vertically
|
|
136
|
+
first, then to the other alignment. It scrolls past the viewport height minus 2 rem. The
|
|
137
|
+
placement is read each time it opens.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { Component } from '@angular/core';
|
|
141
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
142
|
+
import { BfPopoverComponent, BfPopoverTriggerDirective } from '@bitforgehq/ui/overlay';
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A details popover aligned to the trigger's end.
|
|
146
|
+
*
|
|
147
|
+
* `bfPopoverPlacement="below-end"` opens the panel under the trigger, aligned to its inline end;
|
|
148
|
+
* `bfPopoverLabel` names it when it has no heading.
|
|
149
|
+
*/
|
|
150
|
+
@Component({
|
|
151
|
+
selector: 'app-example-popover-placement',
|
|
152
|
+
imports: [BfButtonComponent, BfPopoverComponent, BfPopoverTriggerDirective],
|
|
153
|
+
template: `
|
|
154
|
+
<div class="flex justify-end">
|
|
155
|
+
<bf-button
|
|
156
|
+
variant="text"
|
|
157
|
+
[bfPopoverTriggerFor]="details"
|
|
158
|
+
bfPopoverPlacement="below-end"
|
|
159
|
+
bfPopoverLabel="Delivery details"
|
|
160
|
+
>
|
|
161
|
+
Delivery
|
|
162
|
+
</bf-button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<ng-template #details>
|
|
166
|
+
<bf-popover class="w-64">
|
|
167
|
+
<p class="text-body text-text">Arrives Thursday between 9 and 12.</p>
|
|
168
|
+
<p class="text-caption text-text-muted mt-1">Tracking updates every hour.</p>
|
|
169
|
+
</bf-popover>
|
|
170
|
+
</ng-template>
|
|
171
|
+
`,
|
|
172
|
+
})
|
|
173
|
+
export class PopoverPlacementExampleComponent {}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Naming
|
|
177
|
+
|
|
178
|
+
The panel is a `dialog` named by `bfPopoverLabel` on the trigger when you set it, else by the
|
|
179
|
+
first heading inside it, else by the trigger's text. Give a popover a heading when it holds a
|
|
180
|
+
form, so its purpose is announced when focus moves in.
|
|
181
|
+
|
|
182
|
+
## Desktop
|
|
183
|
+
|
|
184
|
+
Built for a mouse and a keyboard: the panel opens next to its trigger and the page stays usable
|
|
185
|
+
around it, so a click elsewhere closes it and lands where it was aimed. `Tab` walks through the
|
|
186
|
+
panel and on to the control after the trigger, closing the panel, so keyboard users never get
|
|
187
|
+
stuck in it. Set the panel's width with a class on `bf-popover` (`w-72`); it never grows past the
|
|
188
|
+
viewport width minus 2 rem.
|
|
189
|
+
|
|
190
|
+
Under a coarse pointer it works the same, with the panel's controls at their own touch sizes;
|
|
191
|
+
in the Capacitor shell a `bf-sheet` suits phone screens better.
|
|
192
|
+
|
|
193
|
+
## Theming
|
|
194
|
+
|
|
195
|
+
- Panel: `bg-surface`, `text-text`, a 1 px `border-border`, `rounded-card`, `shadow-floating`,
|
|
196
|
+
`p-card` (18 px).
|
|
197
|
+
- The content is yours: use the library's type and field tokens inside it.
|
|
198
|
+
|
|
199
|
+
## Accessibility
|
|
200
|
+
|
|
201
|
+
- The trigger's control has `aria-haspopup="dialog"`, `aria-expanded` and, while open,
|
|
202
|
+
`aria-controls` pointing at the panel.
|
|
203
|
+
- The panel is `role="dialog"` without `aria-modal`, named as described under Naming. It gets the
|
|
204
|
+
id that `aria-controls` names.
|
|
205
|
+
- Focus: opening moves focus into the panel; `Escape` returns it to the trigger. `Tab` is not
|
|
206
|
+
trapped: past the last control the panel closes and focus continues after the trigger, and
|
|
207
|
+
`Shift+Tab` before the first control closes it onto the trigger.
|
|
208
|
+
- `Escape` goes to the popover first: inside a dialog, the first `Escape` closes the popover and
|
|
209
|
+
the next one the dialog.
|
|
210
|
+
|
|
211
|
+
### Keyboard
|
|
212
|
+
|
|
213
|
+
| Key | Where | Action |
|
|
214
|
+
| ----------------------------------- | ---------------- | ------------------------------------------------------- |
|
|
215
|
+
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Trigger | Opens the popover (closes it when open), focus moves in |
|
|
216
|
+
| <kbd>Tab</kbd> | Panel | Next control; past the last one closes, focus moves on |
|
|
217
|
+
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | First control | Closes the popover, focus back on the trigger |
|
|
218
|
+
| <kbd>Escape</kbd> | Panel or trigger | Closes the popover, focus back on the trigger |
|
|
219
|
+
|
|
220
|
+
## Slots
|
|
221
|
+
|
|
222
|
+
- `default`: The panel's content: a heading, text, fields and actions.
|
|
223
|
+
|
|
224
|
+
## Notes
|
|
225
|
+
|
|
226
|
+
### Do
|
|
227
|
+
|
|
228
|
+
- Keep a popover small: a heading, a few fields and one or two actions.
|
|
229
|
+
- Give a popover with a form a heading, or set `bfPopoverLabel`.
|
|
230
|
+
- Apply a filter from an explicit button, or as each field changes, and say which in the panel.
|
|
231
|
+
|
|
232
|
+
### Don't
|
|
233
|
+
|
|
234
|
+
- Don't put a long form or a multi-step task in a popover; use a dialog.
|
|
235
|
+
- Don't open a popover on hover; it opens on a click.
|
|
236
|
+
- Don't use a popover for a list of actions; use a menu.
|
|
237
|
+
- Don't nest popovers.
|
|
238
|
+
|
|
239
|
+
## Migration notes
|
|
240
|
+
|
|
241
|
+
### 0.3.1
|
|
242
|
+
|
|
243
|
+
New `[bfPopoverTriggerFor]` (`BfPopoverTriggerDirective`, exported as `bfPopoverTrigger`) and the
|
|
244
|
+
`bf-popover` panel (`BfPopoverComponent`) in `@bitforgehq/ui/overlay`: a non-modal `role="dialog"`
|
|
245
|
+
panel anchored to its trigger, for filters, details or a small form. `bfPopoverPlacement`
|
|
246
|
+
(`BfPopoverPlacement`, the menu's four placements), `bfPopoverLabel`, `(bfPopoverOpened)` and
|
|
247
|
+
`(bfPopoverClosed)`, and `open()`, `close()`, `toggle()`, `isOpen()`. The trigger's control gets
|
|
248
|
+
`aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`; focus moves in on open, `Tab` is not
|
|
249
|
+
trapped (it closes the panel past either end), and `Escape`, a click outside or a navigation close
|
|
250
|
+
it and return focus. New `BfPopoverHarness` in `@bitforgehq/ui/testing`.
|
|
@@ -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`.
|