@bitforgehq/ui 0.3.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +9 -8
  3. package/ai/MANIFEST.index.json +18 -0
  4. package/ai/MANIFEST.json +368 -31
  5. package/ai/REFERENCE.index.json +63 -0
  6. package/ai/REFERENCE.json +247 -5
  7. package/ai/UI_GUIDE.md +59 -33
  8. package/ai/components/app-frame.md +328 -0
  9. package/ai/components/card.md +56 -2
  10. package/ai/components/empty-state.md +99 -6
  11. package/ai/components/header.md +5 -1
  12. package/ai/components/sidebar-toggle.md +287 -0
  13. package/ai/components/sidebar.md +188 -10
  14. package/ai/components/stat.md +74 -10
  15. package/ai/components/table.md +134 -5
  16. package/ai/components/tabs.md +5 -1
  17. package/ai/components/toolbar.md +69 -4
  18. package/ai/reference/bf-app-frame-breakpoint.md +27 -0
  19. package/ai/reference/bf-app-frame-content-width.md +29 -0
  20. package/ai/reference/bf-empty-state-heading-level.md +1 -1
  21. package/ai/reference/bf-empty-state-heading.md +33 -0
  22. package/ai/reference/bf-labels.md +6 -0
  23. package/ai/reference/bf-sidebar-state-service.md +21 -0
  24. package/ai/reference/bf-sidebar-toggle-harness.md +32 -0
  25. package/ai/reference/bf-size.md +1 -0
  26. package/ai/reference/bf-stat-appearance.md +33 -0
  27. package/ai/reference/bf-stat-variant.md +1 -1
  28. package/ai/reference/bf-table-harness.md +1 -1
  29. package/ai/reference/bf-table-stack.md +29 -0
  30. package/ai/reference/bf_sidebar_state_storage.md +20 -0
  31. package/ai/reference/provide-bf-labels.md +4 -3
  32. package/ai/reference/provide-bf-sidebar-state.md +20 -0
  33. package/bin/bf-ui.js +433 -3
  34. package/fesm2022/bitforgehq-ui-overlay.mjs +571 -446
  35. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  36. package/fesm2022/bitforgehq-ui-testing.mjs +18 -2
  37. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  38. package/fesm2022/bitforgehq-ui.mjs +759 -127
  39. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  40. package/llms-full.txt +1560 -43
  41. package/llms.txt +11 -0
  42. package/package.json +1 -1
  43. package/types/bitforgehq-ui-overlay.d.ts +51 -0
  44. package/types/bitforgehq-ui-testing.d.ts +35 -1
  45. package/types/bitforgehq-ui.d.ts +276 -20
@@ -61,7 +61,7 @@ Use `bf-stat` for a few headline figures the reader should take in at a glance,
61
61
 
62
62
  ## Cards
63
63
 
64
- `variant="card"` gives each stat its own outlined tile, the `flat` card surface. In a grid the cards stretch to the row's height, so a row of stats lines up even when one has a supporting line and another has none. Put an icon in the `icon` slot (`bfSlot="icon"`, a `bf-icon`) to show what the figure is about.
64
+ `variant="card"` gives each stat its own outlined tile, the `flat` card surface, for stats that sit on the page itself; inside a raised card use `tile` instead. In a grid the cards stretch to the row's height, so a row of stats lines up even when one has a supporting line and another has none. Put an icon in the `icon` slot (`bfSlot="icon"`, a `bf-icon`) to show what the figure is about.
65
65
 
66
66
  ```ts
67
67
  import { Component } from '@angular/core';
@@ -103,6 +103,60 @@ export class StatOverviewExampleComponent {
103
103
  }
104
104
  ```
105
105
 
106
+ ## Tiles
107
+
108
+ `variant="tile"` is a sunken, borderless tile (`surface-sunken`, the `card` radius and padding) for a row of stats inside a raised [bf-card](https://bitforge-ui.web.app/components/card): a raised card holds sunken tiles, never outlined ones. On a tile the icon tile turns `surface`, so it still shows. Keep one surface style per level of a dashboard: raised cards on the page, tiles in each card.
109
+
110
+ ```ts
111
+ import { Component } from '@angular/core';
112
+ import { BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
113
+
114
+ /**
115
+ * A dashboard with one surface per level.
116
+ *
117
+ * The page holds raised cards, and every card holds sunken stat tiles: each level has one surface
118
+ * style, so the eye reads the cards as the groups and the tiles as their figures.
119
+ */
120
+ @Component({
121
+ selector: 'app-example-stat-dashboard',
122
+ imports: [BfCardComponent, BfStatComponent],
123
+ template: `
124
+ <div class="grid gap-4 @lg:grid-cols-2">
125
+ <bf-card>
126
+ <h3 class="text-heading">Sales</h3>
127
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
128
+ <bf-stat label="Revenue" value="€48,200" variant="tile" delta="+12%" trend="up">
129
+ vs. last month
130
+ </bf-stat>
131
+ <bf-stat label="Orders" value="1,204" variant="tile" delta="+3%" trend="up">
132
+ vs. last month
133
+ </bf-stat>
134
+ </div>
135
+ </bf-card>
136
+ <bf-card>
137
+ <h3 class="text-heading">Support</h3>
138
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
139
+ <bf-stat
140
+ label="Open tickets"
141
+ value="18"
142
+ variant="tile"
143
+ delta="-4"
144
+ trend="down"
145
+ trendIntent="negative"
146
+ >
147
+ vs. last week
148
+ </bf-stat>
149
+ <bf-stat label="First reply" value="2 h" variant="tile" delta="0" trend="flat">
150
+ vs. last week
151
+ </bf-stat>
152
+ </div>
153
+ </bf-card>
154
+ </div>
155
+ `,
156
+ })
157
+ export class StatDashboardExampleComponent {}
158
+ ```
159
+
106
160
  ## Plain
107
161
 
108
162
  `variant="plain"` (the default) draws no surface, for stats that sit on a card or a page section of their own. The default slot is the supporting line under the value: a short trend ("+12% this week") or a `bf-badge`.
@@ -182,8 +236,9 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
182
236
  - Label (`dt`): `text-caption` in `text-muted`. Value (`dd`): `text-title` in `text`, with tabular figures (`tabular-nums`) so numbers line up across stats.
183
237
  - Supporting line: `text-caption` in `text-muted`.
184
238
  - Delta pill: the badge's label shape (`control` radius, `text-micro` bold, tabular figures); good changes `success-soft` with `success` text, bad ones `danger-soft` with `danger` text, flat or no trend `neutral-soft` with `neutral` text. The arrow mark is drawn with borders in `currentColor`, so it takes the pill's text colour.
185
- - Icon tile: `surface-sunken` background, `text-muted` icon, `tile` radius, 40 px.
239
+ - Icon tile: `surface-sunken` background (`surface` on a `tile` stat), `text-muted` icon, `tile` radius, 40 px.
186
240
  - `card`: `surface`, the `card` radius, `--spacing-card` padding (`p-card`) and a 1.5 px `border-strong` edge, as the `flat` `bf-card`.
241
+ - `tile`: `surface-sunken`, the `card` radius and `--spacing-card` padding, no border.
187
242
 
188
243
  ## Accessibility
189
244
 
@@ -191,14 +246,14 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
191
246
 
192
247
  ## Inputs
193
248
 
194
- | Name | Type | Default | Description |
195
- | ------------- | -------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------- |
196
- | `label` | `string` | required | What the figure measures, shown above it. |
197
- | `value` | `string` | required | The figure, already formatted (`45 min`, `1,204`, `98%`). |
198
- | `variant` | `BfStatVariant` (`'plain' \| 'card'`) | `'plain'` | Surface: plain (default) on its container, or card, an outlined tile of its own. |
199
- | `delta` | `string` | `''` | A change shown in a pill beside the value, already formatted (`+12%`); empty shows none. |
200
- | `trend` | `BfStatTrend \| undefined` (`'up' \| 'down' \| 'flat' \| undefined`) | `undefined` | The delta's direction: an arrow mark and a hidden word; left unset, the pill is neutral. |
201
- | `trendIntent` | `BfStatTrendIntent` (`'positive' \| 'negative'`) | `'positive'` | Whether up is good (positive, default) or down is good (negative, such as costs). |
249
+ | Name | Type | Default | Description |
250
+ | ------------- | -------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------- |
251
+ | `label` | `string` | required | What the figure measures, shown above it. |
252
+ | `value` | `string` | required | The figure, already formatted (`45 min`, `1,204`, `98%`). |
253
+ | `variant` | `BfStatAppearance` (`'plain' \| 'card' \| 'tile'`) | `'plain'` | Surface: plain (default) on its container, card (an outlined tile) or tile (sunken, no border). |
254
+ | `delta` | `string` | `''` | A change shown in a pill beside the value, already formatted (`+12%`); empty shows none. |
255
+ | `trend` | `BfStatTrend \| undefined` (`'up' \| 'down' \| 'flat' \| undefined`) | `undefined` | The delta's direction: an arrow mark and a hidden word; left unset, the pill is neutral. |
256
+ | `trendIntent` | `BfStatTrendIntent` (`'positive' \| 'negative'`) | `'positive'` | Whether up is good (positive, default) or down is good (negative, such as costs). |
202
257
 
203
258
  ## Slots
204
259
 
@@ -215,12 +270,21 @@ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size
215
270
 
216
271
  ### Don't
217
272
 
273
+ - Don't put `card` stats (outlined) inside a raised `bf-card`; use `tile`.
218
274
  - Don't use a stat for a value the user edits or acts on; give it its own control.
219
275
  - Don't put long text in `value`; it is set at the `title` size for a short figure.
220
276
  - Don't put the comparison period in `delta`; keep it to the change (`+12%`) and say "vs. last month" in the supporting line.
221
277
 
222
278
  ## Migration notes
223
279
 
280
+ ### 0.3.2
281
+
282
+ New `variant="tile"`: a sunken, borderless tile (`surface-sunken`, the `card` radius and
283
+ padding) for a row of stats inside a raised `bf-card`; on it the icon tile turns `surface`.
284
+ `variant` now takes the new named type `BfStatAppearance` (`BfStatVariant | 'tile'`); the
285
+ released `BfStatVariant` keeps its two members, so `Record`s over it keep compiling. New
286
+ `stat/dashboard` example with one surface per level.
287
+
224
288
  ### 0.3.1
225
289
 
226
290
  New: `bf-stat`, one labelled figure in its own `dl`, with the value at the `title` size in
@@ -103,13 +103,14 @@ export class TableOverviewExampleComponent {
103
103
 
104
104
  Data table for desktop: column headers that set a sort, optional row selection with
105
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`.
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.
107
108
 
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.
109
110
 
110
111
  ## When to use
111
112
 
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
+ 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.
113
114
 
114
115
  ## Columns and rows
115
116
 
@@ -400,9 +401,115 @@ export class TablePagedExampleComponent {
400
401
 
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.
402
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
+
403
510
  ## Narrow screens and touch
404
511
 
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.
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.
406
513
 
407
514
  ```ts
408
515
  import { Component } from '@angular/core';
@@ -471,6 +578,7 @@ A selectable table tints a row `surface-muted` while hovered (a selected row `pr
471
578
  - Rows: `text` on the page's surface, divided by `border` lines; hover (selectable only) `surface-muted`; selected `primary-soft`, hover `primary-soft-hover`.
472
579
  - Sort arrow: a 1.5 px CSS chevron in the header's text colour.
473
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`.
474
582
  - Text: `text-body` (`text-label-sm` at `sm`), `tabular-nums` in `end` columns.
475
583
 
476
584
  Put the table on a `bf-card` (or another surface) yourself; it has no background or border of its own.
@@ -479,6 +587,8 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
479
587
 
480
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.
481
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
+
482
592
  ### Keyboard
483
593
 
484
594
  | Key | Action |
@@ -489,6 +599,8 @@ A native `table` with a visually hidden `caption` from `label`, `th scope="col"`
489
599
  | <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |
490
600
  | <kbd>Enter</kbd> | Sorts by the focused header |
491
601
 
602
+ A stacked table's Sort by select is a native select with its own keys.
603
+
492
604
  Text fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.
493
605
 
494
606
  ## Inputs
@@ -502,6 +614,7 @@ Text fields, selects and composite widgets (radios, comboboxes, sliders) in a ce
502
614
  | `selectable` | `boolean` | `false` | Adds a first column of checkboxes that select rows. |
503
615
  | `selectAllLabel` | `string` | `''` | Accessible name of the header checkbox; empty falls back to `provideBfLabels()`'s `tableSelectAll` ("Select all rows"). |
504
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). |
505
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)]`. |
506
619
  | `selection` | `readonly string[]` | `[]` | The keys (`rowKey`) of the selected rows; two-way bindable. Two-way: `[(selection)]`. |
507
620
 
@@ -542,15 +655,31 @@ Selects or clears one row.
542
655
 
543
656
  - Give every table a `label`, and every sortable column a short header.
544
657
  - 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.
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`.
546
659
 
547
660
  ### Don't
548
661
 
549
- - Don't put a table on a phone screen that scrolls sideways; use the list.
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.
550
664
  - Don't make a whole row clickable; put a link or button in a custom cell.
551
665
 
552
666
  ## Migration notes
553
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
+
554
683
  ### 0.3.1
555
684
 
556
685
  New `bf-table` and `ng-template[bfTableCell]` (`BfTableCellDirective`) in `@bitforgehq/ui`, a
@@ -194,7 +194,7 @@ 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 [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.
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). Wrap the desktop app in a [bf-app-frame](https://bitforge-ui.web.app/components/app-frame): it holds the sidebar from `md` up and, below it, a header bar with that menu button. Inside the shell under a mouse, the tab links take the `bf-tab` hover and focus styles.
198
198
 
199
199
  ```ts
200
200
  import { Component } from '@angular/core';
@@ -365,6 +365,10 @@ content to the top.
365
365
 
366
366
  ## Migration notes
367
367
 
368
+ ### 0.3.2
369
+
370
+ The `## Desktop` docs point at `bf-app-frame` for the frame around the desktop navigation.
371
+
368
372
  ### 0.3.1
369
373
 
370
374
  The `## Desktop` docs name the desktop replacement: a `bf-top-nav` or `bf-sidebar` of
@@ -187,7 +187,7 @@ export class ToolbarStatusExampleComponent {
187
187
 
188
188
  ## Wrapping
189
189
 
190
- The row is a wrapping flex row with an 8 px gap between items and between rows. In a narrow column the filters wrap onto new rows, and the actions wrap as one group, kept together at the inline end. Give a search field a width (`class="w-full sm:w-56"`) so it fills a phone-wide row and stays compact on a wide one.
190
+ The row is a wrapping flex row with an 8 px gap between items and between rows. The status and the actions are one group that takes the rest of the last row: on a wide row the status follows the view switch and the actions sit at the inline end; when the group no longer fits beside the filters, it wraps onto a row of its own as a whole, with the status at the start and the actions at the end. So the actions never end up alone on a row, and they stay on the last filter row whenever the group fits there. Give a search field a width (`class="w-full sm:w-56"`) so it fills a phone-wide row and stays compact on a wide one.
191
191
 
192
192
  ```ts
193
193
  import { Component } from '@angular/core';
@@ -202,8 +202,8 @@ import {
202
202
  /**
203
203
  * A toolbar that wraps.
204
204
  *
205
- * In a narrow column the filters wrap onto new rows 8 px apart, and the actions move down
206
- * together, still at the inline end.
205
+ * In a narrow column the filters wrap onto new rows 8 px apart, and the status and the actions
206
+ * move down together: the count at the start of the last row, the actions at the inline end.
207
207
  */
208
208
  @Component({
209
209
  selector: 'app-example-toolbar-wrapping',
@@ -232,6 +232,63 @@ export class ToolbarWrappingExampleComponent {
232
232
  }
233
233
  ```
234
234
 
235
+ ## Phone width
236
+
237
+ At phone width (in a desktop web app's narrow window or a narrow column) the search takes a row, the filters share the next, and the status and actions share the last row. A toolbar without actions keeps the status as a plain item after the filters.
238
+
239
+ ```ts
240
+ import { Component, signal } from '@angular/core';
241
+ import {
242
+ BfButtonComponent,
243
+ BfInputComponent,
244
+ BfSelectComponent,
245
+ BfToolbarComponent,
246
+ type BfSelectOption,
247
+ } from '@bitforgehq/ui';
248
+
249
+ /**
250
+ * A toolbar at phone width.
251
+ *
252
+ * In a phone-wide column the search fills its own row, the two filters share the next one, and
253
+ * the status and the Refresh action share the last row: the count at the start, the action at the
254
+ * end, never alone on a row of its own.
255
+ */
256
+ @Component({
257
+ selector: 'app-example-toolbar-narrow',
258
+ imports: [BfButtonComponent, BfInputComponent, BfSelectComponent, BfToolbarComponent],
259
+ template: `
260
+ <div class="max-w-xs">
261
+ <bf-toolbar label="Order filters" [status]="count()">
262
+ <bf-input
263
+ type="search"
264
+ ariaLabel="Search orders"
265
+ placeholder="Search orders"
266
+ class="w-full"
267
+ />
268
+ <bf-select ariaLabel="Status" [options]="statuses" value="open" />
269
+ <bf-select ariaLabel="Period" [options]="periods" value="week" />
270
+ <bf-button bfSlot="actions" variant="outline" (clicked)="refresh()">Refresh</bf-button>
271
+ </bf-toolbar>
272
+ </div>
273
+ `,
274
+ })
275
+ export class ToolbarNarrowExampleComponent {
276
+ protected readonly count = signal('12 orders');
277
+ protected readonly statuses: BfSelectOption[] = [
278
+ { value: 'open', label: 'Open' },
279
+ { value: 'shipped', label: 'Shipped' },
280
+ ];
281
+ protected readonly periods: BfSelectOption[] = [
282
+ { value: 'week', label: 'This week' },
283
+ { value: 'month', label: 'This month' },
284
+ ];
285
+
286
+ protected refresh(): void {
287
+ this.count.set('12 orders, updated');
288
+ }
289
+ }
290
+ ```
291
+
235
292
  ## Desktop
236
293
 
237
294
  `bf-toolbar` is a desktop pattern. Every control and field projected into it resolves its unset `size` to `sm` (32 px tall), because its `providers` set `provideBfDefaults({ size: 'sm' })` for its content; a control that sets `size` itself keeps it, and nothing outside the toolbar changes ([density guide](https://bitforge-ui.web.app/density)). Hover, the pointer cursor and the `focus-visible` ring come from each control; the row adds none of its own. Under a fine pointer each control's visual box is its hit area; under a coarse pointer the `sm` touch extensions reach up to 7.5 px past the visual edge, which the 8 px gap keeps off the neighbouring row.
@@ -262,7 +319,7 @@ The `status` element is `role="status"` (an `aria-live="polite"` region), always
262
319
 
263
320
  - `default`: The search field and the filters (bf-input, bf-select, bf-chip-group), in order.
264
321
  - `view`: A view switch after the filters, such as a bf-segmented of list and grid.
265
- - `actions`: The trailing actions, kept together at the inline end of the row.
322
+ - `actions`: The trailing actions, kept together at the inline end of the row, beside the status.
266
323
 
267
324
  ## Notes
268
325
 
@@ -278,6 +335,14 @@ The `status` element is `role="status"` (an `aria-live="polite"` region), always
278
335
 
279
336
  ## Migration notes
280
337
 
338
+ ### 0.3.2
339
+
340
+ On a narrow row the actions no longer end up alone on a row of their own: the status and the
341
+ actions are now one group that takes the rest of the last row and wraps as a whole, with the
342
+ status at the start and the actions at the inline end. The wide layout is unchanged, and a
343
+ toolbar without actions keeps the status as a plain item of the row. New `toolbar/narrow`
344
+ example at phone width.
345
+
281
346
  ### 0.3.1
282
347
 
283
348
  New `bf-toolbar` in `@bitforgehq/ui`: a wrapping `role="group"` row for the search field and
@@ -0,0 +1,27 @@
1
+ # BfAppFrameBreakpoint
2
+
3
+ The breakpoint where an app frame switches from its header bar to the sidebar
4
+
5
+ Type from `@bitforgehq/ui` · since 0.3.2
6
+
7
+ The viewport width from which `bf-app-frame` shows its sidebar column instead of the narrow
8
+ header bar: Tailwind's `md` (768 px) or `lg` (1024 px).
9
+
10
+ ```ts
11
+ import { type BfAppFrameBreakpoint } from '@bitforgehq/ui';
12
+ ```
13
+
14
+ ## Signature
15
+
16
+ ```ts
17
+ type BfAppFrameBreakpoint = 'md' | 'lg'
18
+ ```
19
+
20
+ ## Values
21
+
22
+ - `md`
23
+ - `lg`
24
+
25
+ ## Used by
26
+
27
+ - [bf-app-frame](../components/app-frame.md)
@@ -0,0 +1,29 @@
1
+ # BfAppFrameContentWidth
2
+
3
+ The maximum width of an app frame's content column, from medium to full
4
+
5
+ Type from `@bitforgehq/ui` · since 0.3.2
6
+
7
+ The maximum width of `bf-app-frame`'s content column: `md` 48rem, `lg` 64rem, `xl` 80rem, or
8
+ `full` for no limit.
9
+
10
+ ```ts
11
+ import { type BfAppFrameContentWidth } from '@bitforgehq/ui';
12
+ ```
13
+
14
+ ## Signature
15
+
16
+ ```ts
17
+ type BfAppFrameContentWidth = 'md' | 'lg' | 'xl' | 'full'
18
+ ```
19
+
20
+ ## Values
21
+
22
+ - `md`
23
+ - `lg`
24
+ - `xl`
25
+ - `full`
26
+
27
+ ## Used by
28
+
29
+ - [bf-app-frame](../components/app-frame.md)
@@ -23,4 +23,4 @@ type BfEmptyStateHeadingLevel = 2 | 3
23
23
 
24
24
  ## Used by
25
25
 
26
- - [bf-empty-state](../components/empty-state.md)
26
+ - [BfEmptyStateHeading](bf-empty-state-heading.md)
@@ -0,0 +1,33 @@
1
+ # BfEmptyStateHeading
2
+
3
+ The heading level of an empty state title, one for a whole page
4
+
5
+ Type from `@bitforgehq/ui` · since 0.3.2
6
+
7
+ The heading level a `bf-empty-state` title takes: a `BfEmptyStateHeadingLevel`, or `1` when the
8
+ empty state is the page itself (a not-found or no-access page). A new type, so `Record`s over
9
+ the released `BfEmptyStateHeadingLevel` keep compiling.
10
+
11
+ ```ts
12
+ import { type BfEmptyStateHeading } from '@bitforgehq/ui';
13
+ ```
14
+
15
+ ## Signature
16
+
17
+ ```ts
18
+ type BfEmptyStateHeading = BfEmptyStateHeadingLevel | 1
19
+ ```
20
+
21
+ ## Values
22
+
23
+ - `2`
24
+ - `3`
25
+ - `1`
26
+
27
+ ## Uses
28
+
29
+ - [BfEmptyStateHeadingLevel](bf-empty-state-heading-level.md)
30
+
31
+ ## Used by
32
+
33
+ - [bf-empty-state](../components/empty-state.md)
@@ -32,6 +32,9 @@ import { type BfLabels } from '@bitforgehq/ui';
32
32
  | `dialog` | `string` | `'Dialog'` | A dialog's or side panel's name without an ariaLabel, a title or a heading. |
33
33
  | `notifications` | `string` | `'Notifications'` | The desktop toast stack's region name. |
34
34
  | `tableSelectAll` | `string` | `'Select all rows'` | bf-table's header checkbox, which selects or clears every row. |
35
+ | `tableSortBy` | `string` | `'Sort by'` | The label of a stacked bf-table's sort select. |
36
+ | `tableSortAscending` | `(column: string) => string` | `` (column) => `${column}, ascending` `` | A stacked bf-table's sort option for a column in ascending order. |
37
+ | `tableSortDescending` | `(column: string) => string` | `` (column) => `${column}, descending` `` | A stacked bf-table's sort option for a column in descending order. |
35
38
  | `theme` | `string` | `'Theme'` | bf-theme-switch's and bf-theme-menu-button's name without a label. |
36
39
  | `themeSystem` | `string` | `'System'` | The theme choice that follows the system's colour scheme. |
37
40
  | `themeLight` | `string` | `'Light'` | The theme choice that forces the light colour scheme. |
@@ -40,6 +43,9 @@ import { type BfLabels } from '@bitforgehq/ui';
40
43
  | `breadcrumbExpand` | `(count: number) => string` | ``(count) => (count === 1 ? 'Show 1 more level' : `Show ${count} more levels`)`` | bf-breadcrumb's ellipsis button, which shows the levels a `maxItems` trail hides. |
41
44
  | `navigation` | `string` | `'Main'` | bf-sidebar's and bf-top-nav's navigation name. |
42
45
  | `navBadge` | `(count: number) => string` | `` (count) => `${count} new` `` | a[bfNavItem]'s announcement of its count badge. |
46
+ | `sidebarCollapse` | `string` | `'Collapse sidebar'` | bf-sidebar-toggle's name and tooltip while the sidebar is expanded. |
47
+ | `sidebarExpand` | `string` | `'Expand sidebar'` | bf-sidebar-toggle's name and tooltip while the sidebar is an icon rail. |
48
+ | `skipToContent` | `string` | `'Skip to main content'` | bf-app-frame's skip link, the first stop of the tab order, which moves focus to the content. |
43
49
  | `pagination` | `string` | `'Pagination'` | bf-pagination's navigation name. |
44
50
  | `previousPage` | `string` | `'Previous page'` | bf-pagination's button to the previous page. |
45
51
  | `nextPage` | `string` | `'Next page'` | bf-pagination's button to the next page. |
@@ -0,0 +1,21 @@
1
+ # BfSidebarStateService
2
+
3
+ The sidebar's collapsed state, kept in storage between visits of the app
4
+
5
+ Service from `@bitforgehq/ui` · since 0.3.2
6
+
7
+ Whether the app's `bf-sidebar` is collapsed to an icon rail, remembered between visits: the app
8
+ binds the sidebar's `[(collapsed)]` to `sidebarState.collapsed`. With `provideBfSidebarState()`
9
+ every change is stored in `BF_SIDEBAR_STATE_STORAGE` under `bf-sidebar-collapsed`; without it
10
+ the signal still holds the state but nothing is written. Storage is read in the browser only, so a
11
+ server render starts expanded and writes nothing. The widgets never inject it.
12
+
13
+ ```ts
14
+ import { BfSidebarStateService } from '@bitforgehq/ui';
15
+ ```
16
+
17
+ ## Properties
18
+
19
+ | Name | Type | Default | Description |
20
+ | ----------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
21
+ | `collapsed` | `WritableSignal<boolean>` | | Whether the sidebar is collapsed: the stored state in the browser, else false. Bind it two-way to `bf-sidebar`'s `collapsed`. |
@@ -0,0 +1,32 @@
1
+ # BfSidebarToggleHarness
2
+
3
+ Test harness for a sidebar toggle: reads and flips the collapsed sidebar
4
+
5
+ Class from `@bitforgehq/ui/testing` · since 0.3.2
6
+
7
+ Harness for `bf-sidebar-toggle`, the button that collapses a `bf-sidebar` to an icon rail. Its
8
+ `label` filter matches the button's name ("Collapse sidebar" or "Expand sidebar").
9
+
10
+ ```ts
11
+ import { BfSidebarToggleHarness } from '@bitforgehq/ui/testing';
12
+ ```
13
+
14
+ ## Signature
15
+
16
+ ```ts
17
+ class BfSidebarToggleHarness extends ComponentHarness
18
+ ```
19
+
20
+ ## Methods
21
+
22
+ ### `getLabel(): Promise<string>`
23
+
24
+ The button's name, which says what the next press does.
25
+
26
+ ### `isExpanded(): Promise<boolean>`
27
+
28
+ Whether the sidebar is at full width (`aria-expanded`).
29
+
30
+ ### `toggle(): Promise<void>`
31
+
32
+ Flips the sidebar with a click.