@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,697 @@
|
|
|
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`. With `stack` it
|
|
107
|
+
renders each row as a tile of label/value pairs in a narrow space or under touch.
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
## When to use
|
|
112
|
+
|
|
113
|
+
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, or let the table stack its rows into tiles with `stack` (see Stacked rows). 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.
|
|
114
|
+
|
|
115
|
+
## Columns and rows
|
|
116
|
+
|
|
117
|
+
`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.
|
|
118
|
+
|
|
119
|
+
`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.
|
|
120
|
+
|
|
121
|
+
`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`.
|
|
122
|
+
|
|
123
|
+
## Sorting
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
## Selection
|
|
128
|
+
|
|
129
|
+
`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.
|
|
130
|
+
|
|
131
|
+
## Custom cells
|
|
132
|
+
|
|
133
|
+
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`).
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { Component, signal } from '@angular/core';
|
|
137
|
+
import {
|
|
138
|
+
BfAvatarComponent,
|
|
139
|
+
BfBadgeComponent,
|
|
140
|
+
BfButtonComponent,
|
|
141
|
+
BfTableCellDirective,
|
|
142
|
+
BfTableComponent,
|
|
143
|
+
type BfBadgeAppearance,
|
|
144
|
+
type BfTableColumn,
|
|
145
|
+
} from '@bitforgehq/ui';
|
|
146
|
+
|
|
147
|
+
interface Task {
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
project: string;
|
|
151
|
+
owner: string;
|
|
152
|
+
initials: string;
|
|
153
|
+
status: 'Open' | 'Review' | 'Done';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const TONE: Record<Task['status'], BfBadgeAppearance> = {
|
|
157
|
+
Open: 'neutral',
|
|
158
|
+
Review: 'warning',
|
|
159
|
+
Done: 'success',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Custom cells.
|
|
164
|
+
*
|
|
165
|
+
* Each `ng-template[bfTableCell]` replaces the text of one column: a title with its project, an
|
|
166
|
+
* avatar, a status badge and a row action. `[bfTableCellOf]` types `let-task` as a `Task`.
|
|
167
|
+
*/
|
|
168
|
+
@Component({
|
|
169
|
+
selector: 'app-example-table-custom-cells',
|
|
170
|
+
imports: [
|
|
171
|
+
BfTableComponent,
|
|
172
|
+
BfTableCellDirective,
|
|
173
|
+
BfAvatarComponent,
|
|
174
|
+
BfBadgeComponent,
|
|
175
|
+
BfButtonComponent,
|
|
176
|
+
],
|
|
177
|
+
template: `
|
|
178
|
+
<div class="flex flex-col gap-3">
|
|
179
|
+
<bf-table label="Tasks" [rows]="tasks" [columns]="columns">
|
|
180
|
+
<ng-template bfTableCell="title" [bfTableCellOf]="tasks" let-task>
|
|
181
|
+
<span class="flex flex-col">
|
|
182
|
+
<span class="font-bold">{{ task.title }}</span>
|
|
183
|
+
<span class="text-caption text-text-muted">{{ task.project }}</span>
|
|
184
|
+
</span>
|
|
185
|
+
</ng-template>
|
|
186
|
+
<ng-template bfTableCell="owner" [bfTableCellOf]="tasks" let-task>
|
|
187
|
+
<span class="flex items-center gap-2">
|
|
188
|
+
<bf-avatar size="sm" [initials]="task.initials" />
|
|
189
|
+
{{ task.owner }}
|
|
190
|
+
</span>
|
|
191
|
+
</ng-template>
|
|
192
|
+
<ng-template bfTableCell="status" [bfTableCellOf]="tasks" let-task>
|
|
193
|
+
<bf-badge [variant]="tone[task.status]">{{ task.status }}</bf-badge>
|
|
194
|
+
</ng-template>
|
|
195
|
+
<ng-template bfTableCell="open" [bfTableCellOf]="tasks" let-task>
|
|
196
|
+
<bf-button variant="text" size="sm" (clicked)="opened.set(task.title)">Open</bf-button>
|
|
197
|
+
</ng-template>
|
|
198
|
+
</bf-table>
|
|
199
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">
|
|
200
|
+
{{ opened() ? 'Opened ' + opened() + '.' : 'Open a task.' }}
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
`,
|
|
204
|
+
})
|
|
205
|
+
export class TableCustomCellsExampleComponent {
|
|
206
|
+
readonly tone = TONE;
|
|
207
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
208
|
+
{ key: 'title', label: 'Task' },
|
|
209
|
+
{ key: 'owner', label: 'Owner' },
|
|
210
|
+
{ key: 'status', label: 'Status' },
|
|
211
|
+
{ key: 'open', label: 'Actions', align: 'end' },
|
|
212
|
+
];
|
|
213
|
+
readonly tasks: readonly Task[] = [
|
|
214
|
+
{
|
|
215
|
+
id: 'brief',
|
|
216
|
+
title: 'Write the brief',
|
|
217
|
+
project: 'Atlas',
|
|
218
|
+
owner: 'Lena Meier',
|
|
219
|
+
initials: 'LM',
|
|
220
|
+
status: 'Review',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'deck',
|
|
224
|
+
title: 'Pitch deck',
|
|
225
|
+
project: 'Atlas',
|
|
226
|
+
owner: 'Jonas Keller',
|
|
227
|
+
initials: 'JK',
|
|
228
|
+
status: 'Open',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'copy',
|
|
232
|
+
title: 'Copy review',
|
|
233
|
+
project: 'Cedar',
|
|
234
|
+
owner: 'Lena Meier',
|
|
235
|
+
initials: 'LM',
|
|
236
|
+
status: 'Done',
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
readonly opened = signal('');
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Empty state
|
|
244
|
+
|
|
245
|
+
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.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { Component, signal } from '@angular/core';
|
|
249
|
+
import {
|
|
250
|
+
BfButtonComponent,
|
|
251
|
+
BfEmptyStateComponent,
|
|
252
|
+
BfTableComponent,
|
|
253
|
+
type BfTableColumn,
|
|
254
|
+
} from '@bitforgehq/ui';
|
|
255
|
+
import { BfIconComponent } from '@bitforgehq/ui/icons';
|
|
256
|
+
import { ListTodo } from 'lucide';
|
|
257
|
+
|
|
258
|
+
interface Task {
|
|
259
|
+
id: string;
|
|
260
|
+
title: string;
|
|
261
|
+
hours: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* An empty table with an empty state.
|
|
266
|
+
*
|
|
267
|
+
* While `rows` is empty the table shows one full-width row holding the `empty` slot, here a
|
|
268
|
+
* `bf-empty-state` whose action adds the first row.
|
|
269
|
+
*/
|
|
270
|
+
@Component({
|
|
271
|
+
selector: 'app-example-table-empty',
|
|
272
|
+
imports: [BfTableComponent, BfEmptyStateComponent, BfButtonComponent, BfIconComponent],
|
|
273
|
+
template: `
|
|
274
|
+
<bf-table label="Tasks" [rows]="tasks()" [columns]="columns">
|
|
275
|
+
<bf-empty-state
|
|
276
|
+
bfSlot="empty"
|
|
277
|
+
title="No tasks yet"
|
|
278
|
+
description="Add a task and it shows up here."
|
|
279
|
+
[headingLevel]="3"
|
|
280
|
+
>
|
|
281
|
+
<bf-icon bfSlot="icon" [icon]="listTodo" size="lg" />
|
|
282
|
+
<bf-button bfSlot="actions" (clicked)="add()">New task</bf-button>
|
|
283
|
+
</bf-empty-state>
|
|
284
|
+
</bf-table>
|
|
285
|
+
`,
|
|
286
|
+
})
|
|
287
|
+
export class TableEmptyExampleComponent {
|
|
288
|
+
readonly listTodo = ListTodo;
|
|
289
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
290
|
+
{ key: 'title', label: 'Task' },
|
|
291
|
+
{ key: 'hours', label: 'Hours', align: 'end' },
|
|
292
|
+
];
|
|
293
|
+
readonly tasks = signal<readonly Task[]>([]);
|
|
294
|
+
|
|
295
|
+
add(): void {
|
|
296
|
+
this.tasks.update((tasks) => [...tasks, { id: 'brief', title: 'Write the brief', hours: 3 }]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Paging
|
|
302
|
+
|
|
303
|
+
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.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { Component, computed, signal } from '@angular/core';
|
|
307
|
+
import {
|
|
308
|
+
BfPaginationComponent,
|
|
309
|
+
BfTableComponent,
|
|
310
|
+
type BfTableColumn,
|
|
311
|
+
type BfTableSort,
|
|
312
|
+
} from '@bitforgehq/ui';
|
|
313
|
+
|
|
314
|
+
interface Invoice {
|
|
315
|
+
id: string;
|
|
316
|
+
client: string;
|
|
317
|
+
amount: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const CLIENTS = ['Atlas', 'Bloom', 'Cedar', 'Delta', 'Ember'];
|
|
321
|
+
const INVOICES: readonly Invoice[] = Array.from({ length: 23 }, (_, i) => ({
|
|
322
|
+
id: `INV-${String(1001 + i)}`,
|
|
323
|
+
client: CLIENTS[i % CLIENTS.length] ?? 'Atlas',
|
|
324
|
+
amount: 120 + ((i * 37) % 90) * 10,
|
|
325
|
+
}));
|
|
326
|
+
const PAGE_SIZE = 5;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A paged table.
|
|
330
|
+
*
|
|
331
|
+
* Twenty-three invoices, five per page: the table shows the current page's rows, sliced in a
|
|
332
|
+
* `computed()` from the sorted rows, and a `bf-pagination` under it binds the page with `total`
|
|
333
|
+
* and `pageSize`. Sorting goes back to page 1.
|
|
334
|
+
*/
|
|
335
|
+
@Component({
|
|
336
|
+
selector: 'app-example-table-paged',
|
|
337
|
+
imports: [BfTableComponent, BfPaginationComponent],
|
|
338
|
+
template: `
|
|
339
|
+
<div class="flex flex-col gap-3">
|
|
340
|
+
<bf-table
|
|
341
|
+
label="Invoices"
|
|
342
|
+
[rows]="pageRows()"
|
|
343
|
+
[columns]="columns"
|
|
344
|
+
[sort]="sort()"
|
|
345
|
+
(sortChange)="sortBy($event)"
|
|
346
|
+
/>
|
|
347
|
+
<bf-pagination
|
|
348
|
+
class="self-end"
|
|
349
|
+
label="Invoice pages"
|
|
350
|
+
[(page)]="page"
|
|
351
|
+
[total]="invoices.length"
|
|
352
|
+
[pageSize]="pageSize"
|
|
353
|
+
/>
|
|
354
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
355
|
+
</div>
|
|
356
|
+
`,
|
|
357
|
+
})
|
|
358
|
+
export class TablePagedExampleComponent {
|
|
359
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
360
|
+
{ key: 'id', label: 'Invoice', sortable: true },
|
|
361
|
+
{ key: 'client', label: 'Client' },
|
|
362
|
+
{ key: 'amount', label: 'Amount', sortable: true, align: 'end' },
|
|
363
|
+
];
|
|
364
|
+
readonly invoices = INVOICES;
|
|
365
|
+
readonly pageSize = PAGE_SIZE;
|
|
366
|
+
readonly page = signal(1);
|
|
367
|
+
readonly sort = signal<BfTableSort | null>(null);
|
|
368
|
+
|
|
369
|
+
readonly sorted = computed(() => {
|
|
370
|
+
const sort = this.sort();
|
|
371
|
+
if (!sort) {
|
|
372
|
+
return this.invoices;
|
|
373
|
+
}
|
|
374
|
+
const factor = sort.direction === 'ascending' ? 1 : -1;
|
|
375
|
+
return [...this.invoices].sort((a, b) =>
|
|
376
|
+
sort.column === 'amount' ? (a.amount - b.amount) * factor : a.id.localeCompare(b.id) * factor,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/** The current page's rows: the table never sees the others. */
|
|
381
|
+
readonly pageRows = computed(() => {
|
|
382
|
+
const start = (this.page() - 1) * PAGE_SIZE;
|
|
383
|
+
return this.sorted().slice(start, start + PAGE_SIZE);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
readonly status = computed(() => {
|
|
387
|
+
const start = (this.page() - 1) * PAGE_SIZE + 1;
|
|
388
|
+
const end = Math.min(start + PAGE_SIZE - 1, this.invoices.length);
|
|
389
|
+
return `Invoices ${start}–${end} of ${this.invoices.length}.`;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/** A new sort starts at the top of the new order: back to page 1. */
|
|
393
|
+
sortBy(sort: BfTableSort | null): void {
|
|
394
|
+
this.sort.set(sort);
|
|
395
|
+
this.page.set(1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Density
|
|
401
|
+
|
|
402
|
+
`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.
|
|
403
|
+
|
|
404
|
+
## Stacked rows
|
|
405
|
+
|
|
406
|
+
`stack` lets the table itself fit a narrow space. With `stack="narrow"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack="coarse"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.
|
|
407
|
+
|
|
408
|
+
Stacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select of every sortable column, ascending and descending (`tableSortBy`, `tableSortAscending`, `tableSortDescending` labels), since the hidden header's buttons are not shown. Choosing an option sets `sort`, as a header click does.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import { Component, computed, signal } from '@angular/core';
|
|
412
|
+
import {
|
|
413
|
+
BfBadgeComponent,
|
|
414
|
+
BfSegmentedComponent,
|
|
415
|
+
BfTableCellDirective,
|
|
416
|
+
BfTableComponent,
|
|
417
|
+
type BfSegmentOption,
|
|
418
|
+
type BfTableColumn,
|
|
419
|
+
type BfTableSort,
|
|
420
|
+
} from '@bitforgehq/ui';
|
|
421
|
+
|
|
422
|
+
interface Task {
|
|
423
|
+
id: string;
|
|
424
|
+
title: string;
|
|
425
|
+
project: string;
|
|
426
|
+
status: 'Open' | 'Done';
|
|
427
|
+
hours: number;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const TASKS: readonly Task[] = [
|
|
431
|
+
{ id: 'brief', title: 'Write the brief', project: 'Atlas', status: 'Done', hours: 3 },
|
|
432
|
+
{ id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', status: 'Open', hours: 6 },
|
|
433
|
+
{ id: 'deck', title: 'Pitch deck', project: 'Atlas', status: 'Open', hours: 9 },
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
type Width = 'phone' | 'full';
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Stacked rows on a narrow table.
|
|
440
|
+
*
|
|
441
|
+
* `stack="narrow"` turns each row into a tile of label/value pairs while the table is narrower
|
|
442
|
+
* than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
|
|
443
|
+
* holds Select all and a Sort by select. Switch the box between phone width and full width.
|
|
444
|
+
*/
|
|
445
|
+
@Component({
|
|
446
|
+
selector: 'app-example-table-stacked',
|
|
447
|
+
imports: [BfTableComponent, BfTableCellDirective, BfBadgeComponent, BfSegmentedComponent],
|
|
448
|
+
template: `
|
|
449
|
+
<!-- A stacking table is a size container: its width comes from its parent, never its columns. -->
|
|
450
|
+
<div class="flex w-152 max-w-full flex-col gap-3">
|
|
451
|
+
<bf-segmented label="Box width" [options]="widths" [(value)]="width" />
|
|
452
|
+
<div [class]="width() === 'phone' ? 'w-full max-w-sm' : 'w-full'">
|
|
453
|
+
<bf-table
|
|
454
|
+
label="Stacked tasks"
|
|
455
|
+
stack="narrow"
|
|
456
|
+
selectable
|
|
457
|
+
[rows]="rows()"
|
|
458
|
+
[columns]="columns"
|
|
459
|
+
[(sort)]="sort"
|
|
460
|
+
[(selection)]="selection"
|
|
461
|
+
>
|
|
462
|
+
<ng-template bfTableCell="status" [bfTableCellOf]="rows()" let-task>
|
|
463
|
+
<bf-badge [variant]="task.status === 'Done' ? 'success' : 'neutral'">
|
|
464
|
+
{{ task.status }}
|
|
465
|
+
</bf-badge>
|
|
466
|
+
</ng-template>
|
|
467
|
+
</bf-table>
|
|
468
|
+
</div>
|
|
469
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
470
|
+
</div>
|
|
471
|
+
`,
|
|
472
|
+
})
|
|
473
|
+
export class TableStackedExampleComponent {
|
|
474
|
+
readonly widths: BfSegmentOption<Width>[] = [
|
|
475
|
+
{ value: 'phone', label: 'Phone' },
|
|
476
|
+
{ value: 'full', label: 'Full width' },
|
|
477
|
+
];
|
|
478
|
+
readonly width = signal<Width | null>('phone');
|
|
479
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
480
|
+
{ key: 'title', label: 'Task', sortable: true },
|
|
481
|
+
{ key: 'project', label: 'Project' },
|
|
482
|
+
{ key: 'status', label: 'Status' },
|
|
483
|
+
{ key: 'hours', label: 'Hours', sortable: true, align: 'end' },
|
|
484
|
+
];
|
|
485
|
+
readonly sort = signal<BfTableSort | null>(null);
|
|
486
|
+
readonly selection = signal<readonly string[]>([]);
|
|
487
|
+
|
|
488
|
+
/** The table never sorts: the page sorts its rows from `sort`. */
|
|
489
|
+
readonly rows = computed(() => {
|
|
490
|
+
const sort = this.sort();
|
|
491
|
+
if (!sort) {
|
|
492
|
+
return TASKS;
|
|
493
|
+
}
|
|
494
|
+
const factor = sort.direction === 'ascending' ? 1 : -1;
|
|
495
|
+
return [...TASKS].sort((a, b) =>
|
|
496
|
+
sort.column === 'hours'
|
|
497
|
+
? (a.hours - b.hours) * factor
|
|
498
|
+
: a.title.localeCompare(b.title) * factor,
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
readonly status = computed(() => {
|
|
503
|
+
const sort = this.sort();
|
|
504
|
+
const sorted = sort ? `Sorted by ${sort.column}, ${sort.direction}` : 'Unsorted';
|
|
505
|
+
return `${sorted} · ${this.selection().length} selected.`;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Narrow screens and touch
|
|
511
|
+
|
|
512
|
+
A table needs width. When stacked tiles are not what a phone should show (long records, a summary per row), render the same rows as a divided `bf-list` of `bf-list-item`s below the `md` breakpoint (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, or a table with `stack="coarse"`. The table's wrapper scrolls sideways when its columns cannot fit, but that is a last resort, not a layout for phones.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
import { Component } from '@angular/core';
|
|
516
|
+
import {
|
|
517
|
+
BfListComponent,
|
|
518
|
+
BfListItemComponent,
|
|
519
|
+
BfTableComponent,
|
|
520
|
+
type BfTableColumn,
|
|
521
|
+
} from '@bitforgehq/ui';
|
|
522
|
+
|
|
523
|
+
interface Task {
|
|
524
|
+
id: string;
|
|
525
|
+
title: string;
|
|
526
|
+
project: string;
|
|
527
|
+
hours: number;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* The same rows as a list on narrow screens.
|
|
532
|
+
*
|
|
533
|
+
* One `rows` array drives both: the table from the `md` breakpoint up (a `hidden md:block`
|
|
534
|
+
* wrapper), a divided `bf-list` of `bf-list-item` rows below it (`md:hidden`), so a phone never
|
|
535
|
+
* scrolls a table sideways.
|
|
536
|
+
*/
|
|
537
|
+
@Component({
|
|
538
|
+
selector: 'app-example-table-narrow-fallback',
|
|
539
|
+
imports: [BfTableComponent, BfListComponent, BfListItemComponent],
|
|
540
|
+
template: `
|
|
541
|
+
<div class="hidden md:block">
|
|
542
|
+
<bf-table label="Tasks" [rows]="tasks" [columns]="columns" />
|
|
543
|
+
</div>
|
|
544
|
+
<div class="md:hidden">
|
|
545
|
+
<bf-list divided>
|
|
546
|
+
@for (task of tasks; track task.id) {
|
|
547
|
+
<bf-list-item [title]="task.title" [subtitle]="task.project">
|
|
548
|
+
<span bfSlot="trailing" class="text-caption text-text-muted tabular-nums">
|
|
549
|
+
{{ task.hours }} h
|
|
550
|
+
</span>
|
|
551
|
+
</bf-list-item>
|
|
552
|
+
}
|
|
553
|
+
</bf-list>
|
|
554
|
+
</div>
|
|
555
|
+
`,
|
|
556
|
+
})
|
|
557
|
+
export class TableNarrowFallbackExampleComponent {
|
|
558
|
+
readonly columns: readonly BfTableColumn[] = [
|
|
559
|
+
{ key: 'title', label: 'Task' },
|
|
560
|
+
{ key: 'project', label: 'Project' },
|
|
561
|
+
{ key: 'hours', label: 'Hours', align: 'end' },
|
|
562
|
+
];
|
|
563
|
+
readonly tasks: readonly Task[] = [
|
|
564
|
+
{ id: 'brief', title: 'Write the brief', project: 'Atlas', hours: 3 },
|
|
565
|
+
{ id: 'moodboard', title: 'Bloom moodboard', project: 'Bloom', hours: 6 },
|
|
566
|
+
{ id: 'copy', title: 'Copy review', project: 'Cedar', hours: 2 },
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Desktop
|
|
572
|
+
|
|
573
|
+
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)).
|
|
574
|
+
|
|
575
|
+
## Theming
|
|
576
|
+
|
|
577
|
+
- Header: `text-label-xs`, bold, `text-muted`; a sortable header's hover `text`. A `border` line under the header row.
|
|
578
|
+
- Rows: `text` on the page's surface, divided by `border` lines; hover (selectable only) `surface-muted`; selected `primary-soft`, hover `primary-soft-hover`.
|
|
579
|
+
- Sort arrow: a 1.5 px CSS chevron in the header's text colour.
|
|
580
|
+
- Focus ring: `ring`. Header buttons: `rounded-field`.
|
|
581
|
+
- Stacked rows: tiles on `surface-sunken` with `rounded-tile`, labels `text-label-xs` bold in `text-muted`.
|
|
582
|
+
- Text: `text-body` (`text-label-sm` at `sm`), `tabular-nums` in `end` columns.
|
|
583
|
+
|
|
584
|
+
Put the table on a `bf-card` (or another surface) yourself; it has no background or border of its own.
|
|
585
|
+
|
|
586
|
+
## Accessibility
|
|
587
|
+
|
|
588
|
+
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.
|
|
589
|
+
|
|
590
|
+
A table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / ""`), so a screen reader reads the column header once, not the label as well.
|
|
591
|
+
|
|
592
|
+
### Keyboard
|
|
593
|
+
|
|
594
|
+
| Key | Action |
|
|
595
|
+
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
596
|
+
| <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves through the header buttons, checkboxes and the controls in custom cells in document order |
|
|
597
|
+
| <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 |
|
|
598
|
+
| <kbd>ArrowUp</kbd> | The same, upwards, up to the header row |
|
|
599
|
+
| <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |
|
|
600
|
+
| <kbd>Enter</kbd> | Sorts by the focused header |
|
|
601
|
+
|
|
602
|
+
A stacked table's Sort by select is a native select with its own keys.
|
|
603
|
+
|
|
604
|
+
Text fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.
|
|
605
|
+
|
|
606
|
+
## Inputs
|
|
607
|
+
|
|
608
|
+
| Name | Type | Default | Description |
|
|
609
|
+
| ---------------- | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
610
|
+
| `rows` | `readonly T[]` | required | The rows to show, already sorted and filtered by the consumer. |
|
|
611
|
+
| `columns` | `readonly BfTableColumn[]` | required | The columns, in order. |
|
|
612
|
+
| `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. |
|
|
613
|
+
| `label` | `string` | `''` | The table's name, rendered as a visually hidden `caption`. |
|
|
614
|
+
| `selectable` | `boolean` | `false` | Adds a first column of checkboxes that select rows. |
|
|
615
|
+
| `selectAllLabel` | `string` | `''` | Accessible name of the header checkbox; empty falls back to `provideBfLabels()`'s `tableSelectAll` ("Select all rows"). |
|
|
616
|
+
| `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). |
|
|
617
|
+
| `stack` | `BfTableStack` (`'never' \| 'narrow' \| 'coarse'`) | `'never'` | When the rows render as cards of label/value pairs: `never`, `narrow` (the table is narrower than 36rem) or `coarse` (under a coarse pointer, at any width). |
|
|
618
|
+
| `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)]`. |
|
|
619
|
+
| `selection` | `readonly string[]` | `[]` | The keys (`rowKey`) of the selected rows; two-way bindable. Two-way: `[(selection)]`. |
|
|
620
|
+
|
|
621
|
+
## Slots
|
|
622
|
+
|
|
623
|
+
- `empty`: Shown in one full-width row while `rows` is empty, such as a `bf-empty-state`.
|
|
624
|
+
|
|
625
|
+
## Methods
|
|
626
|
+
|
|
627
|
+
### `sortBy(key: string): void`
|
|
628
|
+
|
|
629
|
+
Sorts by the column with this key: ascending first, then descending and ascending again.
|
|
630
|
+
|
|
631
|
+
| Parameter | Type | Description |
|
|
632
|
+
| --------- | -------- | ------------------- |
|
|
633
|
+
| `key` | `string` | The column's `key`. |
|
|
634
|
+
|
|
635
|
+
### `selectAll(selected: boolean): void`
|
|
636
|
+
|
|
637
|
+
Selects or clears every row; keys of rows not shown stay as they are.
|
|
638
|
+
|
|
639
|
+
| Parameter | Type | Description |
|
|
640
|
+
| ---------- | --------- | ---------------------------------------------- |
|
|
641
|
+
| `selected` | `boolean` | True to select every row, false to clear them. |
|
|
642
|
+
|
|
643
|
+
### `selectRow(key: string, selected: boolean): void`
|
|
644
|
+
|
|
645
|
+
Selects or clears one row.
|
|
646
|
+
|
|
647
|
+
| Parameter | Type | Description |
|
|
648
|
+
| ---------- | --------- | ------------------------------------- |
|
|
649
|
+
| `key` | `string` | The row's key. |
|
|
650
|
+
| `selected` | `boolean` | True to select it, false to clear it. |
|
|
651
|
+
|
|
652
|
+
## Notes
|
|
653
|
+
|
|
654
|
+
### Do
|
|
655
|
+
|
|
656
|
+
- Give every table a `label`, and every sortable column a short header.
|
|
657
|
+
- Sort and filter the rows in a `computed()` from `sort` and your filters.
|
|
658
|
+
- Use `stack="narrow"` for a table that may sit in a narrow column, or `stack="coarse"` in the Capacitor shell; otherwise render the rows as `bf-list-item`s below `md`.
|
|
659
|
+
|
|
660
|
+
### Don't
|
|
661
|
+
|
|
662
|
+
- Don't put a table on a phone screen that scrolls sideways; stack it or use the list.
|
|
663
|
+
- Don't put a stacking table in a parent that sizes it from its content: the container query needs a width from outside.
|
|
664
|
+
- Don't make a whole row clickable; put a link or button in a custom cell.
|
|
665
|
+
|
|
666
|
+
## Migration notes
|
|
667
|
+
|
|
668
|
+
### 0.3.2
|
|
669
|
+
|
|
670
|
+
New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not
|
|
671
|
+
change, `'narrow'` or `'coarse'`): while the table is narrower than 36rem (a container query on
|
|
672
|
+
the host, which then gets `@container`) or, for `'coarse'`, under a coarse pointer at any width,
|
|
673
|
+
each body row renders as a sunken tile of label/value pairs. The markup stays a `table` with
|
|
674
|
+
explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles (some browsers drop a
|
|
675
|
+
table's semantics once its display changes), the header row is visually hidden but still read,
|
|
676
|
+
the selection checkbox and the first cell head each tile, and every other cell shows its column
|
|
677
|
+
label (`data-label`, a `::before` with empty alternative text, so it is not read twice). A bar
|
|
678
|
+
above the tiles holds a labelled Select all checkbox and a Sort by `bf-select`, since the hidden
|
|
679
|
+
header's controls are not shown. New label keys `tableSortBy` ("Sort by"), `tableSortAscending`
|
|
680
|
+
and `tableSortDescending` (`"Hours, ascending"`). `BfTableHarness` reads and drives a stacked
|
|
681
|
+
table the same way. New `table/stacked` example and a Stacked rows docs section.
|
|
682
|
+
|
|
683
|
+
### 0.3.1
|
|
684
|
+
|
|
685
|
+
New `bf-table` and `ng-template[bfTableCell]` (`BfTableCellDirective`) in `@bitforgehq/ui`, a
|
|
686
|
+
data table for desktop pages: `rows`, `columns` (`BfTableColumn`: `key`, `label`, `sortable`,
|
|
687
|
+
`align` of `BfTableAlign`), `rowKey` (`BfTableRowKey`, the row's `id` by default) and a
|
|
688
|
+
visually hidden caption from `label`. Sortable headers set the `sort` model (`BfTableSort`,
|
|
689
|
+
`BfTableSortDirection`) and `aria-sort`; the consumer sorts the rows. `selectable` adds a
|
|
690
|
+
checkbox column bound to the `selection` model of row keys, with a mixed header checkbox named
|
|
691
|
+
by the new `tableSelectAll` label ("Select all rows") or `selectAllLabel`. A `bfTableCell`
|
|
692
|
+
template replaces one column's cells (`BfTableCellContext`; `bfTableCellOf` types the row), the
|
|
693
|
+
`empty` slot shows while there are no rows, `size` follows `controlSize`, and ArrowUp / ArrowDown
|
|
694
|
+
move focus between the controls of a column. New `BfTableHarness` in `@bitforgehq/ui/testing`.
|
|
695
|
+
|
|
696
|
+
The docs gain a Paging section and a `table/paged` example: one page of rows with a
|
|
697
|
+
`bf-pagination` under the table, back to page 1 on a sort.
|