@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,568 @@
1
+ # bf-table
2
+
3
+ Desktop data table with sortable headers, row selection, custom cells and an empty slot
4
+
5
+ `BfTableComponent` from `@bitforgehq/ui` · Data display · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import {
12
+ BfActionBarComponent,
13
+ BfButtonComponent,
14
+ BfTableComponent,
15
+ type BfTableColumn,
16
+ type BfTableSort,
17
+ } from '@bitforgehq/ui';
18
+
19
+ interface Task {
20
+ id: string;
21
+ title: string;
22
+ project: string;
23
+ hours: number;
24
+ }
25
+
26
+ const TASKS: readonly Task[] = [
27
+ { id: 'brief', title: 'Write the brief', project: 'Atlas', hours: 3 },
28
+ { id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', hours: 6 },
29
+ { id: 'deck', title: 'Pitch deck', project: 'Atlas', hours: 9 },
30
+ { id: 'copy', title: 'Copy review', project: 'Cedar', hours: 2 },
31
+ ];
32
+
33
+ /**
34
+ * The bf-table overview.
35
+ *
36
+ * Live demo of `bf-table`: sortable Task and Hours headers, row selection and a `bf-action-bar`
37
+ * that acts on the selected rows. The table sets `sort`; the page sorts the rows.
38
+ */
39
+ @Component({
40
+ selector: 'app-example-table-overview',
41
+ imports: [BfTableComponent, BfActionBarComponent, BfButtonComponent],
42
+ template: `
43
+ <div class="flex flex-col gap-3">
44
+ <bf-table
45
+ label="Tasks"
46
+ selectable
47
+ [rows]="rows()"
48
+ [columns]="columns"
49
+ [(sort)]="sort"
50
+ [(selection)]="selection"
51
+ />
52
+ @if (selection().length > 0) {
53
+ <bf-action-bar class="self-center" [label]="selection().length + ' selected'">
54
+ <bf-button variant="text" size="sm" (clicked)="selection.set([])">Clear</bf-button>
55
+ <bf-button size="sm" (clicked)="markDone()">Mark done</bf-button>
56
+ </bf-action-bar>
57
+ }
58
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
59
+ </div>
60
+ `,
61
+ })
62
+ export class TableOverviewExampleComponent {
63
+ readonly columns: readonly BfTableColumn[] = [
64
+ { key: 'title', label: 'Task', sortable: true },
65
+ { key: 'project', label: 'Project' },
66
+ { key: 'hours', label: 'Hours', sortable: true, align: 'end' },
67
+ ];
68
+ readonly tasks = signal<readonly Task[]>(TASKS);
69
+ readonly sort = signal<BfTableSort | null>(null);
70
+ readonly selection = signal<readonly string[]>([]);
71
+ readonly done = signal(0);
72
+
73
+ /** The table never sorts: the page sorts its rows from `sort`. */
74
+ readonly rows = computed(() => {
75
+ const sort = this.sort();
76
+ if (!sort) {
77
+ return this.tasks();
78
+ }
79
+ const factor = sort.direction === 'ascending' ? 1 : -1;
80
+ return [...this.tasks()].sort((a, b) =>
81
+ sort.column === 'hours'
82
+ ? (a.hours - b.hours) * factor
83
+ : a.title.localeCompare(b.title) * factor,
84
+ );
85
+ });
86
+
87
+ readonly status = computed(() => {
88
+ const sort = this.sort();
89
+ const sorted = sort ? `Sorted by ${sort.column}, ${sort.direction}` : 'Unsorted';
90
+ return `${sorted} · ${this.selection().length} selected · ${this.done()} done.`;
91
+ });
92
+
93
+ markDone(): void {
94
+ const selected = new Set(this.selection());
95
+ this.tasks.update((tasks) => tasks.filter((task) => !selected.has(task.id)));
96
+ this.done.update((done) => done + selected.size);
97
+ this.selection.set([]);
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Overview
103
+
104
+ Data table for desktop: column headers that set a sort, optional row selection with
105
+ checkboxes, custom cells from `bfTableCell` templates and an empty slot. It never sorts or
106
+ filters itself; the consumer sorts the rows in a `computed()` from `sort`.
107
+
108
+ A data table for desktop pages: rows of records the user compares across columns, with headers that sort, an optional checkbox column that selects rows, custom cells and an empty state. `bf-table` is data-driven: you pass `rows` and `columns`, and it never sorts or filters the rows itself. Its headers set the `sort` model and you sort the rows in a `computed()`, on the client or on the server.
109
+
110
+ ## When to use
111
+
112
+ Use `bf-table` on a desktop page for records the user scans and compares column by column: tasks, invoices, members. On a phone or in the Capacitor shell use [bf-list](https://bitforge-ui.web.app/components/list) rows instead. Use a [bf-list](https://bitforge-ui.web.app/components/list) on desktop too when each record is one line of text with a status, and a description list for the fields of a single record.
113
+
114
+ ## Columns and rows
115
+
116
+ `columns` is an array of `BfTableColumn`: the row property `key`, the header `label`, `sortable` and `align`. A default cell shows the row's `key` property as text when it is a string, a number or a boolean, and is empty otherwise (`null`, `undefined`, an object): render a date or an object with a custom cell. `align: 'end'` lines the header and cells up at the inline end and sets tabular figures, for numbers and amounts.
117
+
118
+ `rowKey` returns the string that identifies a row in `selection` and keeps each row's DOM when the rows change. By default it reads the row's `id` (a string or a number), and falls back to the row's index when a row has none; pass `[rowKey]="byEmail"` when the key is another property.
119
+
120
+ `label` names the table. It renders as a visually hidden `caption`, so a screen reader announces it when entering the table; the page's visible heading stays yours. Give every table a `label`.
121
+
122
+ ## Sorting
123
+
124
+ A `sortable` column's header is a button. The first click sets `sort` to `{ column, direction: 'ascending' }`, the next one to `descending`, then ascending again; clicking another column starts it ascending. The sorted header carries `aria-sort` and a small CSS arrow (up while ascending, down while descending). The table never reorders the rows: bind `[(sort)]` and sort the rows in a `computed()` from it, so a server-side sort works the same way. `sort` is `null` until the user sorts, or set it to start sorted.
125
+
126
+ ## Selection
127
+
128
+ `selectable` adds a first column of `bf-checkbox`es; `selection` is a `model` of the selected rows' keys, so bind `[(selection)]`. The header checkbox selects or clears every row shown (keys of rows filtered out stay as they are) and shows the mixed state while only some are selected. Selected rows take the `primary-soft` fill. A selection drives a [bf-action-bar](https://bitforge-ui.web.app/components/action-bar) with the count and the actions on the selected rows, as in the overview above. Clear the selection after an action that removes its rows.
129
+
130
+ ## Custom cells
131
+
132
+ An `ng-template[bfTableCell]` replaces the default text of one column with your own markup: a title with a subtitle, an avatar, a badge, a link or a row action. `bfTableCell` names the column's `key`, `let-row` is the row and `let-i="index"` its position. Bind `[bfTableCellOf]` to the same rows as the table so the template's row is typed (without it the row is `unknown`).
133
+
134
+ ```ts
135
+ import { Component, signal } from '@angular/core';
136
+ import {
137
+ BfAvatarComponent,
138
+ BfBadgeComponent,
139
+ BfButtonComponent,
140
+ BfTableCellDirective,
141
+ BfTableComponent,
142
+ type BfBadgeAppearance,
143
+ type BfTableColumn,
144
+ } from '@bitforgehq/ui';
145
+
146
+ interface Task {
147
+ id: string;
148
+ title: string;
149
+ project: string;
150
+ owner: string;
151
+ initials: string;
152
+ status: 'Open' | 'Review' | 'Done';
153
+ }
154
+
155
+ const TONE: Record<Task['status'], BfBadgeAppearance> = {
156
+ Open: 'neutral',
157
+ Review: 'warning',
158
+ Done: 'success',
159
+ };
160
+
161
+ /**
162
+ * Custom cells.
163
+ *
164
+ * Each `ng-template[bfTableCell]` replaces the text of one column: a title with its project, an
165
+ * avatar, a status badge and a row action. `[bfTableCellOf]` types `let-task` as a `Task`.
166
+ */
167
+ @Component({
168
+ selector: 'app-example-table-custom-cells',
169
+ imports: [
170
+ BfTableComponent,
171
+ BfTableCellDirective,
172
+ BfAvatarComponent,
173
+ BfBadgeComponent,
174
+ BfButtonComponent,
175
+ ],
176
+ template: `
177
+ <div class="flex flex-col gap-3">
178
+ <bf-table label="Tasks" [rows]="tasks" [columns]="columns">
179
+ <ng-template bfTableCell="title" [bfTableCellOf]="tasks" let-task>
180
+ <span class="flex flex-col">
181
+ <span class="font-bold">{{ task.title }}</span>
182
+ <span class="text-caption text-text-muted">{{ task.project }}</span>
183
+ </span>
184
+ </ng-template>
185
+ <ng-template bfTableCell="owner" [bfTableCellOf]="tasks" let-task>
186
+ <span class="flex items-center gap-2">
187
+ <bf-avatar size="sm" [initials]="task.initials" />
188
+ {{ task.owner }}
189
+ </span>
190
+ </ng-template>
191
+ <ng-template bfTableCell="status" [bfTableCellOf]="tasks" let-task>
192
+ <bf-badge [variant]="tone[task.status]">{{ task.status }}</bf-badge>
193
+ </ng-template>
194
+ <ng-template bfTableCell="open" [bfTableCellOf]="tasks" let-task>
195
+ <bf-button variant="text" size="sm" (clicked)="opened.set(task.title)">Open</bf-button>
196
+ </ng-template>
197
+ </bf-table>
198
+ <p data-status aria-live="polite" class="text-caption text-text-muted">
199
+ {{ opened() ? 'Opened ' + opened() + '.' : 'Open a task.' }}
200
+ </p>
201
+ </div>
202
+ `,
203
+ })
204
+ export class TableCustomCellsExampleComponent {
205
+ readonly tone = TONE;
206
+ readonly columns: readonly BfTableColumn[] = [
207
+ { key: 'title', label: 'Task' },
208
+ { key: 'owner', label: 'Owner' },
209
+ { key: 'status', label: 'Status' },
210
+ { key: 'open', label: 'Actions', align: 'end' },
211
+ ];
212
+ readonly tasks: readonly Task[] = [
213
+ {
214
+ id: 'brief',
215
+ title: 'Write the brief',
216
+ project: 'Atlas',
217
+ owner: 'Lena Meier',
218
+ initials: 'LM',
219
+ status: 'Review',
220
+ },
221
+ {
222
+ id: 'deck',
223
+ title: 'Pitch deck',
224
+ project: 'Atlas',
225
+ owner: 'Jonas Keller',
226
+ initials: 'JK',
227
+ status: 'Open',
228
+ },
229
+ {
230
+ id: 'copy',
231
+ title: 'Copy review',
232
+ project: 'Cedar',
233
+ owner: 'Lena Meier',
234
+ initials: 'LM',
235
+ status: 'Done',
236
+ },
237
+ ];
238
+ readonly opened = signal('');
239
+ }
240
+ ```
241
+
242
+ ## Empty state
243
+
244
+ While `rows` is empty the table shows one full-width row that holds the `empty` slot, such as a `bf-empty-state` with the action that creates the first record. The header stays, so the user still sees what the table holds.
245
+
246
+ ```ts
247
+ import { Component, signal } from '@angular/core';
248
+ import {
249
+ BfButtonComponent,
250
+ BfEmptyStateComponent,
251
+ BfTableComponent,
252
+ type BfTableColumn,
253
+ } from '@bitforgehq/ui';
254
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
255
+ import { ListTodo } from 'lucide';
256
+
257
+ interface Task {
258
+ id: string;
259
+ title: string;
260
+ hours: number;
261
+ }
262
+
263
+ /**
264
+ * An empty table with an empty state.
265
+ *
266
+ * While `rows` is empty the table shows one full-width row holding the `empty` slot, here a
267
+ * `bf-empty-state` whose action adds the first row.
268
+ */
269
+ @Component({
270
+ selector: 'app-example-table-empty',
271
+ imports: [BfTableComponent, BfEmptyStateComponent, BfButtonComponent, BfIconComponent],
272
+ template: `
273
+ <bf-table label="Tasks" [rows]="tasks()" [columns]="columns">
274
+ <bf-empty-state
275
+ bfSlot="empty"
276
+ title="No tasks yet"
277
+ description="Add a task and it shows up here."
278
+ [headingLevel]="3"
279
+ >
280
+ <bf-icon bfSlot="icon" [icon]="listTodo" size="lg" />
281
+ <bf-button bfSlot="actions" (clicked)="add()">New task</bf-button>
282
+ </bf-empty-state>
283
+ </bf-table>
284
+ `,
285
+ })
286
+ export class TableEmptyExampleComponent {
287
+ readonly listTodo = ListTodo;
288
+ readonly columns: readonly BfTableColumn[] = [
289
+ { key: 'title', label: 'Task' },
290
+ { key: 'hours', label: 'Hours', align: 'end' },
291
+ ];
292
+ readonly tasks = signal<readonly Task[]>([]);
293
+
294
+ add(): void {
295
+ this.tasks.update((tasks) => [...tasks, { id: 'brief', title: 'Write the brief', hours: 3 }]);
296
+ }
297
+ }
298
+ ```
299
+
300
+ ## Paging
301
+
302
+ A long table shows one page of rows at a time with a [bf-pagination](https://bitforge-ui.web.app/components/pagination) under it. Bind its `[(page)]` and give it the `total` row count and the `pageSize`; the table gets only the current page's rows, sliced in a `computed()` from the sorted rows (or fetched from the server for that page). Sorting goes back to page 1, so the user starts at the top of the new order. Name the pagination after the table ("Invoice pages"), so its landmark says what it pages.
303
+
304
+ ```ts
305
+ import { Component, computed, signal } from '@angular/core';
306
+ import {
307
+ BfPaginationComponent,
308
+ BfTableComponent,
309
+ type BfTableColumn,
310
+ type BfTableSort,
311
+ } from '@bitforgehq/ui';
312
+
313
+ interface Invoice {
314
+ id: string;
315
+ client: string;
316
+ amount: number;
317
+ }
318
+
319
+ const CLIENTS = ['Atlas', 'Bloom', 'Cedar', 'Delta', 'Ember'];
320
+ const INVOICES: readonly Invoice[] = Array.from({ length: 23 }, (_, i) => ({
321
+ id: `INV-${String(1001 + i)}`,
322
+ client: CLIENTS[i % CLIENTS.length] ?? 'Atlas',
323
+ amount: 120 + ((i * 37) % 90) * 10,
324
+ }));
325
+ const PAGE_SIZE = 5;
326
+
327
+ /**
328
+ * A paged table.
329
+ *
330
+ * Twenty-three invoices, five per page: the table shows the current page's rows, sliced in a
331
+ * `computed()` from the sorted rows, and a `bf-pagination` under it binds the page with `total`
332
+ * and `pageSize`. Sorting goes back to page 1.
333
+ */
334
+ @Component({
335
+ selector: 'app-example-table-paged',
336
+ imports: [BfTableComponent, BfPaginationComponent],
337
+ template: `
338
+ <div class="flex flex-col gap-3">
339
+ <bf-table
340
+ label="Invoices"
341
+ [rows]="pageRows()"
342
+ [columns]="columns"
343
+ [sort]="sort()"
344
+ (sortChange)="sortBy($event)"
345
+ />
346
+ <bf-pagination
347
+ class="self-end"
348
+ label="Invoice pages"
349
+ [(page)]="page"
350
+ [total]="invoices.length"
351
+ [pageSize]="pageSize"
352
+ />
353
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
354
+ </div>
355
+ `,
356
+ })
357
+ export class TablePagedExampleComponent {
358
+ readonly columns: readonly BfTableColumn[] = [
359
+ { key: 'id', label: 'Invoice', sortable: true },
360
+ { key: 'client', label: 'Client' },
361
+ { key: 'amount', label: 'Amount', sortable: true, align: 'end' },
362
+ ];
363
+ readonly invoices = INVOICES;
364
+ readonly pageSize = PAGE_SIZE;
365
+ readonly page = signal(1);
366
+ readonly sort = signal<BfTableSort | null>(null);
367
+
368
+ readonly sorted = computed(() => {
369
+ const sort = this.sort();
370
+ if (!sort) {
371
+ return this.invoices;
372
+ }
373
+ const factor = sort.direction === 'ascending' ? 1 : -1;
374
+ return [...this.invoices].sort((a, b) =>
375
+ sort.column === 'amount' ? (a.amount - b.amount) * factor : a.id.localeCompare(b.id) * factor,
376
+ );
377
+ });
378
+
379
+ /** The current page's rows: the table never sees the others. */
380
+ readonly pageRows = computed(() => {
381
+ const start = (this.page() - 1) * PAGE_SIZE;
382
+ return this.sorted().slice(start, start + PAGE_SIZE);
383
+ });
384
+
385
+ readonly status = computed(() => {
386
+ const start = (this.page() - 1) * PAGE_SIZE + 1;
387
+ const end = Math.min(start + PAGE_SIZE - 1, this.invoices.length);
388
+ return `Invoices ${start}–${end} of ${this.invoices.length}.`;
389
+ });
390
+
391
+ /** A new sort starts at the top of the new order: back to page 1. */
392
+ sortBy(sort: BfTableSort | null): void {
393
+ this.sort.set(sort);
394
+ this.page.set(1);
395
+ }
396
+ }
397
+ ```
398
+
399
+ ## Density
400
+
401
+ `size` sets the row height: `sm` 40 px, `md` 48 px (default), `lg` 56 px, with a 32, 40 or 48 px header. Unset, it follows `provideBfDefaults()`'s `controlSize`, so `{ size: 'sm' }` in a page's `providers` makes a dense table. The checkboxes take the same step. Under a coarse pointer every row is at least 44 px tall, and the sortable header buttons are 44 px targets; a cell grows to fit its content and is never covered by a hit-area extension.
402
+
403
+ ## Narrow screens and touch
404
+
405
+ A table needs width. Below the `md` breakpoint, render the same rows as a divided `bf-list` of `bf-list-item`s (a `md:hidden` wrapper) and the table only from `md` up (a `hidden md:block` wrapper), from the one `rows` array. Inside the Capacitor shell use the list alone. The table's wrapper scrolls sideways when its columns cannot fit, but that is a last resort, not a layout for phones.
406
+
407
+ ```ts
408
+ import { Component } from '@angular/core';
409
+ import {
410
+ BfListComponent,
411
+ BfListItemComponent,
412
+ BfTableComponent,
413
+ type BfTableColumn,
414
+ } from '@bitforgehq/ui';
415
+
416
+ interface Task {
417
+ id: string;
418
+ title: string;
419
+ project: string;
420
+ hours: number;
421
+ }
422
+
423
+ /**
424
+ * The same rows as a list on narrow screens.
425
+ *
426
+ * One `rows` array drives both: the table from the `md` breakpoint up (a `hidden md:block`
427
+ * wrapper), a divided `bf-list` of `bf-list-item` rows below it (`md:hidden`), so a phone never
428
+ * scrolls a table sideways.
429
+ */
430
+ @Component({
431
+ selector: 'app-example-table-narrow-fallback',
432
+ imports: [BfTableComponent, BfListComponent, BfListItemComponent],
433
+ template: `
434
+ <div class="hidden md:block">
435
+ <bf-table label="Tasks" [rows]="tasks" [columns]="columns" />
436
+ </div>
437
+ <div class="md:hidden">
438
+ <bf-list divided>
439
+ @for (task of tasks; track task.id) {
440
+ <bf-list-item [title]="task.title" [subtitle]="task.project">
441
+ <span bfSlot="trailing" class="text-caption text-text-muted tabular-nums">
442
+ {{ task.hours }} h
443
+ </span>
444
+ </bf-list-item>
445
+ }
446
+ </bf-list>
447
+ </div>
448
+ `,
449
+ })
450
+ export class TableNarrowFallbackExampleComponent {
451
+ readonly columns: readonly BfTableColumn[] = [
452
+ { key: 'title', label: 'Task' },
453
+ { key: 'project', label: 'Project' },
454
+ { key: 'hours', label: 'Hours', align: 'end' },
455
+ ];
456
+ readonly tasks: readonly Task[] = [
457
+ { id: 'brief', title: 'Write the brief', project: 'Atlas', hours: 3 },
458
+ { id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', hours: 6 },
459
+ { id: 'copy', title: 'Copy review', project: 'Cedar', hours: 2 },
460
+ ];
461
+ }
462
+ ```
463
+
464
+ ## Desktop
465
+
466
+ A selectable table tints a row `surface-muted` while hovered (a selected row `primary-soft-hover`); a table without selection does not, because its rows are not targets. Sortable headers darken from `text-muted` to `text` on hover and show the pointer. Hover styles apply only on devices that can hover. The focus ring shows on keyboard focus only, on the header button, the checkbox or the control in a custom cell. Desktop density is the default `md` step; `sm` suits a dense admin page ([density guide](https://bitforge-ui.web.app/density)).
467
+
468
+ ## Theming
469
+
470
+ - Header: `text-label-xs`, bold, `text-muted`; a sortable header's hover `text`. A `border` line under the header row.
471
+ - Rows: `text` on the page's surface, divided by `border` lines; hover (selectable only) `surface-muted`; selected `primary-soft`, hover `primary-soft-hover`.
472
+ - Sort arrow: a 1.5 px CSS chevron in the header's text colour.
473
+ - Focus ring: `ring`. Header buttons: `rounded-field`.
474
+ - Text: `text-body` (`text-label-sm` at `sm`), `tabular-nums` in `end` columns.
475
+
476
+ Put the table on a `bf-card` (or another surface) yourself; it has no background or border of its own.
477
+
478
+ ## Accessibility
479
+
480
+ A native `table` with a visually hidden `caption` from `label`, `th scope="col"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort="ascending"` or `"descending"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` ("Select all rows"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.
481
+
482
+ ### Keyboard
483
+
484
+ | Key | Action |
485
+ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
486
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the header buttons, checkboxes and the controls in custom cells in document order |
487
+ | <kbd>ArrowDown</kbd> | On a button, link or checkbox in a row (or the header): moves focus to the control at the same place in the nearest row below that has one; nothing happens at the last row |
488
+ | <kbd>ArrowUp</kbd> | The same, upwards, up to the header row |
489
+ | <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |
490
+ | <kbd>Enter</kbd> | Sorts by the focused header |
491
+
492
+ Text fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.
493
+
494
+ ## Inputs
495
+
496
+ | Name | Type | Default | Description |
497
+ | ---------------- | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
498
+ | `rows` | `readonly T[]` | required | The rows to show, already sorted and filtered by the consumer. |
499
+ | `columns` | `readonly BfTableColumn[]` | required | The columns, in order. |
500
+ | `rowKey` | `BfTableRowKey<T>` | `idRowKey` | The unique string key of a row, used for `selection` and to track rows; by default the row's `id` (a string or a number), else its index. |
501
+ | `label` | `string` | `''` | The table's name, rendered as a visually hidden `caption`. |
502
+ | `selectable` | `boolean` | `false` | Adds a first column of checkboxes that select rows. |
503
+ | `selectAllLabel` | `string` | `''` | Accessible name of the header checkbox; empty falls back to `provideBfLabels()`'s `tableSelectAll` ("Select all rows"). |
504
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Row height step: sm 40, md 48, lg 56 px (the header 32/40/48; at least 44 px under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
505
+ | `sort` | `BfTableSort \| null` | `null` | The sorted column and direction, `null` while unsorted; two-way bindable. A sortable header sets it, and the consumer sorts `rows` from it. Two-way: `[(sort)]`. |
506
+ | `selection` | `readonly string[]` | `[]` | The keys (`rowKey`) of the selected rows; two-way bindable. Two-way: `[(selection)]`. |
507
+
508
+ ## Slots
509
+
510
+ - `empty`: Shown in one full-width row while `rows` is empty, such as a `bf-empty-state`.
511
+
512
+ ## Methods
513
+
514
+ ### `sortBy(key: string): void`
515
+
516
+ Sorts by the column with this key: ascending first, then descending and ascending again.
517
+
518
+ | Parameter | Type | Description |
519
+ | --------- | -------- | ------------------- |
520
+ | `key` | `string` | The column's `key`. |
521
+
522
+ ### `selectAll(selected: boolean): void`
523
+
524
+ Selects or clears every row; keys of rows not shown stay as they are.
525
+
526
+ | Parameter | Type | Description |
527
+ | ---------- | --------- | ---------------------------------------------- |
528
+ | `selected` | `boolean` | True to select every row, false to clear them. |
529
+
530
+ ### `selectRow(key: string, selected: boolean): void`
531
+
532
+ Selects or clears one row.
533
+
534
+ | Parameter | Type | Description |
535
+ | ---------- | --------- | ------------------------------------- |
536
+ | `key` | `string` | The row's key. |
537
+ | `selected` | `boolean` | True to select it, false to clear it. |
538
+
539
+ ## Notes
540
+
541
+ ### Do
542
+
543
+ - Give every table a `label`, and every sortable column a short header.
544
+ - Sort and filter the rows in a `computed()` from `sort` and your filters.
545
+ - Render the rows as `bf-list-item`s below `md` and inside the Capacitor shell.
546
+
547
+ ### Don't
548
+
549
+ - Don't put a table on a phone screen that scrolls sideways; use the list.
550
+ - Don't make a whole row clickable; put a link or button in a custom cell.
551
+
552
+ ## Migration notes
553
+
554
+ ### 0.3.1
555
+
556
+ New `bf-table` and `ng-template[bfTableCell]` (`BfTableCellDirective`) in `@bitforgehq/ui`, a
557
+ data table for desktop pages: `rows`, `columns` (`BfTableColumn`: `key`, `label`, `sortable`,
558
+ `align` of `BfTableAlign`), `rowKey` (`BfTableRowKey`, the row's `id` by default) and a
559
+ visually hidden caption from `label`. Sortable headers set the `sort` model (`BfTableSort`,
560
+ `BfTableSortDirection`) and `aria-sort`; the consumer sorts the rows. `selectable` adds a
561
+ checkbox column bound to the `selection` model of row keys, with a mixed header checkbox named
562
+ by the new `tableSelectAll` label ("Select all rows") or `selectAllLabel`. A `bfTableCell`
563
+ template replaces one column's cells (`BfTableCellContext`; `bfTableCellOf` types the row), the
564
+ `empty` slot shows while there are no rows, `size` follows `controlSize`, and ArrowUp / ArrowDown
565
+ move focus between the controls of a column. New `BfTableHarness` in `@bitforgehq/ui/testing`.
566
+
567
+ The docs gain a Paging section and a `table/paged` example: one page of rows with a
568
+ `bf-pagination` under the table, back to page 1 on a sort.
@@ -79,7 +79,7 @@ else the current tab route), the bar hides while the soft keyboard is open, and
79
79
 
80
80
  ## When to use
81
81
 
82
- Use `bf-tabs` for the tab root of a Capacitor app: one navigation stack per tab behind a floating [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar). Use [bf-stack-outlet](https://bitforge-ui.web.app/components/stack-outlet) instead for an app with one stack and no tabs, and a sidebar of links on desktop.
82
+ Use `bf-tabs` for the tab root of a Capacitor app: one navigation stack per tab behind a floating [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar). Use [bf-stack-outlet](https://bitforge-ui.web.app/components/stack-outlet) instead for an app with one stack and no tabs, and on desktop a [bf-sidebar](https://bitforge-ui.web.app/components/sidebar) or a [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) of `a[bfNavItem]` links over a plain `router-outlet`.
83
83
 
84
84
  ## Basic usage
85
85
 
@@ -194,7 +194,41 @@ fires no keyboard event).
194
194
 
195
195
  ## Desktop
196
196
 
197
- `bf-tabs` is part of the Capacitor shell (`provideBfShell()`). On a desktop web app put the top-level destinations in a sidebar or a top `nav` of links with `routerLinkActive`, and a plain `router-outlet`. Inside the shell under a mouse, the tab links take the `bf-tab` hover and focus styles.
197
+ `bf-tabs` is part of the Capacitor shell (`provideBfShell()`). On a desktop web app, put the top-level destinations in a [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) (a few sections) or a [bf-sidebar](https://bitforge-ui.web.app/components/sidebar) (more, in groups, collapsible to an icon rail) of `a[bfNavItem]` links, each with `routerLink`, `routerLinkActive` and `ariaCurrentWhenActive="page"`, above a plain `router-outlet`. The router sets `aria-current="page"` on the link of the current route and the item shows it. Drive both from one array of destinations: inside the shell the same array feeds the `bf-tab`s of a [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar), and below a breakpoint a [bf-menu-button](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` links (see bf-sidebar, Responsive). Inside the shell under a mouse, the tab links take the `bf-tab` hover and focus styles.
198
+
199
+ ```ts
200
+ import { Component } from '@angular/core';
201
+ import { RouterLink, RouterLinkActive } from '@angular/router';
202
+ import { BfNavItemComponent, BfTopNavComponent } from '@bitforgehq/ui';
203
+
204
+ /**
205
+ * The desktop replacement for tabs: a bf-top-nav.
206
+ *
207
+ * `a[bfNavItem]` links with `routerLinkActive` and `ariaCurrentWhenActive="page"`: the router marks
208
+ * the current page and the item shows it; a plain `router-outlet` below renders the page. With
209
+ * more sections, the same links go in a `bf-sidebar`.
210
+ */
211
+ @Component({
212
+ selector: 'app-example-tabs-desktop-nav',
213
+ imports: [RouterLink, RouterLinkActive, BfNavItemComponent, BfTopNavComponent],
214
+ template: `
215
+ <bf-top-nav label="Navigation components">
216
+ @for (link of links; track link.path) {
217
+ <a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">{{
218
+ link.label
219
+ }}</a>
220
+ }
221
+ </bf-top-nav>
222
+ `,
223
+ })
224
+ export class TabsDesktopNavExampleComponent {
225
+ protected readonly links: readonly { path: string; label: string }[] = [
226
+ { path: '/components/tabs', label: 'Tabs' },
227
+ { path: '/components/tab-bar', label: 'Tab bar' },
228
+ { path: '/components/stack-outlet', label: 'Stack outlet' },
229
+ ];
230
+ }
231
+ ```
198
232
 
199
233
  ## Theming
200
234
 
@@ -331,6 +365,12 @@ content to the top.
331
365
 
332
366
  ## Migration notes
333
367
 
368
+ ### 0.3.1
369
+
370
+ The `## Desktop` docs name the desktop replacement: a `bf-top-nav` or `bf-sidebar` of
371
+ `a[bfNavItem]` links with `routerLinkActive` and `ariaCurrentWhenActive="page"`, driven by the
372
+ same destinations array as the shell's `bf-tab-bar`, with an example. No code change.
373
+
334
374
  ### 0.3.0
335
375
 
336
376
  **Breaking:** (ARIA contract) `bf-tabs` provides `BF_TAB_LINKS`, so the `bf-tab-bar` projected