@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +482 -0
  2. package/README.md +17 -11
  3. package/ai/MANIFEST.index.json +288 -0
  4. package/ai/MANIFEST.json +8716 -3926
  5. package/ai/REFERENCE.index.json +413 -0
  6. package/ai/REFERENCE.json +4710 -2047
  7. package/ai/UI_GUIDE.md +185 -57
  8. package/ai/components/back-button.md +42 -2
  9. package/ai/components/badge.md +17 -7
  10. package/ai/components/bar-chart.md +188 -0
  11. package/ai/components/breadcrumb-item.md +121 -0
  12. package/ai/components/breadcrumb.md +277 -0
  13. package/ai/components/button.md +82 -15
  14. package/ai/components/calendar.md +300 -0
  15. package/ai/components/card.md +63 -2
  16. package/ai/components/chart-card.md +283 -0
  17. package/ai/components/combobox.md +411 -0
  18. package/ai/components/date-input.md +396 -0
  19. package/ai/components/datetime-input.md +215 -0
  20. package/ai/components/description-item.md +111 -0
  21. package/ai/components/description-list.md +160 -0
  22. package/ai/components/donut-chart.md +169 -0
  23. package/ai/components/header.md +67 -2
  24. package/ai/components/icon-button.md +74 -16
  25. package/ai/components/icon.md +63 -7
  26. package/ai/components/line-chart.md +234 -0
  27. package/ai/components/list.md +70 -6
  28. package/ai/components/menu.md +5 -0
  29. package/ai/components/multi-combobox.md +257 -0
  30. package/ai/components/nav-group.md +178 -0
  31. package/ai/components/nav-item.md +210 -0
  32. package/ai/components/page-header.md +237 -0
  33. package/ai/components/pagination.md +173 -0
  34. package/ai/components/popover.md +250 -0
  35. package/ai/components/side-panel.md +391 -0
  36. package/ai/components/sidebar.md +363 -0
  37. package/ai/components/skeleton.md +215 -0
  38. package/ai/components/sparkline.md +188 -0
  39. package/ai/components/stat.md +233 -0
  40. package/ai/components/tab-group.md +226 -0
  41. package/ai/components/tab-panel.md +163 -0
  42. package/ai/components/table.md +568 -0
  43. package/ai/components/tabs.md +42 -2
  44. package/ai/components/theme-menu-button.md +182 -0
  45. package/ai/components/theme-switch.md +170 -0
  46. package/ai/components/time-input.md +240 -0
  47. package/ai/components/toolbar.md +289 -0
  48. package/ai/components/tooltip.md +230 -0
  49. package/ai/components/top-nav.md +159 -0
  50. package/ai/reference/bf-badge-appearance.md +36 -0
  51. package/ai/reference/bf-badge-variant.md +1 -1
  52. package/ai/reference/bf-bar-chart-orientation.md +27 -0
  53. package/ai/reference/bf-button-appearance.md +36 -0
  54. package/ai/reference/bf-button-variant.md +1 -1
  55. package/ai/reference/bf-calendar-disabled-date.md +23 -0
  56. package/ai/reference/bf-calendar-harness.md +51 -0
  57. package/ai/reference/bf-calendar-weekday.md +31 -0
  58. package/ai/reference/bf-chart-card-heading-level.md +26 -0
  59. package/ai/reference/bf-chart-curve.md +28 -0
  60. package/ai/reference/bf-chart-harness.md +51 -0
  61. package/ai/reference/bf-chart-legend-position.md +30 -0
  62. package/ai/reference/bf-chart-series.md +23 -0
  63. package/ai/reference/bf-chart-value-format.md +23 -0
  64. package/ai/reference/bf-color-scheme-service.md +30 -0
  65. package/ai/reference/bf-color-scheme.md +31 -0
  66. package/ai/reference/bf-combobox-filter.md +27 -0
  67. package/ai/reference/bf-combobox-harness.md +89 -0
  68. package/ai/reference/bf-combobox-option.md +33 -0
  69. package/ai/reference/bf-compare-with.md +2 -0
  70. package/ai/reference/bf-date-input-harness.md +67 -0
  71. package/ai/reference/bf-date-input-picker.md +27 -0
  72. package/ai/reference/bf-date-time-input-harness.md +76 -0
  73. package/ai/reference/bf-date-time-input-picker.md +27 -0
  74. package/ai/reference/bf-description-list-layout.md +26 -0
  75. package/ai/reference/bf-donut-slice.md +22 -0
  76. package/ai/reference/bf-labels.md +67 -20
  77. package/ai/reference/bf-menu-button-variant.md +1 -0
  78. package/ai/reference/bf-menu-placement.md +1 -0
  79. package/ai/reference/bf-overlay-options.md +28 -27
  80. package/ai/reference/bf-overlay-placement.md +34 -0
  81. package/ai/reference/bf-overlay-position.md +1 -1
  82. package/ai/reference/bf-overlay-service.md +5 -3
  83. package/ai/reference/bf-page-header-heading-level.md +26 -0
  84. package/ai/reference/bf-pagination-harness.md +61 -0
  85. package/ai/reference/bf-platform-service.md +17 -15
  86. package/ai/reference/bf-popover-harness.md +42 -0
  87. package/ai/reference/bf-popover-placement.md +28 -0
  88. package/ai/reference/bf-popover-trigger-directive.md +56 -0
  89. package/ai/reference/bf-side-panel-config.md +35 -0
  90. package/ai/reference/bf-side-panel-harness.md +34 -0
  91. package/ai/reference/bf-side-panel-ref.md +44 -0
  92. package/ai/reference/bf-side-panel-service.md +43 -0
  93. package/ai/reference/bf-side-panel-template-context.md +32 -0
  94. package/ai/reference/bf-side-panel-width.md +28 -0
  95. package/ai/reference/bf-size.md +12 -0
  96. package/ai/reference/bf-skeleton-shape.md +28 -0
  97. package/ai/reference/bf-stat-trend-intent.md +27 -0
  98. package/ai/reference/bf-stat-trend.md +27 -0
  99. package/ai/reference/bf-stat-variant.md +26 -0
  100. package/ai/reference/bf-tab-group-harness.md +44 -0
  101. package/ai/reference/bf-table-align.md +26 -0
  102. package/ai/reference/bf-table-cell-context.md +24 -0
  103. package/ai/reference/bf-table-cell-directive.md +22 -0
  104. package/ai/reference/bf-table-column.md +28 -0
  105. package/ai/reference/bf-table-harness.md +77 -0
  106. package/ai/reference/bf-table-row-key.md +21 -0
  107. package/ai/reference/bf-table-sort-direction.md +26 -0
  108. package/ai/reference/bf-table-sort.md +27 -0
  109. package/ai/reference/bf-theme.md +5 -4
  110. package/ai/reference/bf-time-input-harness.md +67 -0
  111. package/ai/reference/bf-time-input-hour-cycle.md +27 -0
  112. package/ai/reference/bf-time-input-picker.md +27 -0
  113. package/ai/reference/bf-tooltip-directive.md +43 -0
  114. package/ai/reference/bf-tooltip-harness.md +43 -0
  115. package/ai/reference/bf-tooltip-placement.md +28 -0
  116. package/ai/reference/bf_color_scheme_storage.md +20 -0
  117. package/ai/reference/bf_side_panel_data.md +17 -0
  118. package/ai/reference/bf_theme_storage.md +3 -1
  119. package/ai/reference/provide-bf-color-scheme.md +20 -0
  120. package/ai/reference/provide-bf-labels.md +12 -4
  121. package/ai/reference/slide-in-from-end.md +24 -0
  122. package/ai/reference/slide-out-to-end.md +24 -0
  123. package/fesm2022/bitforgehq-ui-charts.mjs +3045 -0
  124. package/fesm2022/bitforgehq-ui-charts.mjs.map +1 -0
  125. package/fesm2022/bitforgehq-ui-icons.mjs +107 -3
  126. package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -1
  127. package/fesm2022/bitforgehq-ui-mobile.mjs +14 -59
  128. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  129. package/fesm2022/bitforgehq-ui-overlay.mjs +6436 -33
  130. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  131. package/fesm2022/bitforgehq-ui-testing.mjs +618 -1
  132. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  133. package/fesm2022/bitforgehq-ui.mjs +5092 -153
  134. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  135. package/llms-full.txt +25177 -14398
  136. package/llms.txt +95 -1
  137. package/package.json +7 -3
  138. package/theme.css +101 -0
  139. package/types/bitforgehq-ui-charts.d.ts +967 -0
  140. package/types/bitforgehq-ui-icons.d.ts +13 -1
  141. package/types/bitforgehq-ui-mobile.d.ts +25 -18
  142. package/types/bitforgehq-ui-overlay.d.ts +1844 -7
  143. package/types/bitforgehq-ui-testing.d.ts +748 -1
  144. 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`.