@bitforgehq/ui 0.3.3 → 0.4.0

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 (39) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +4 -4
  3. package/ai/MANIFEST.index.json +36 -0
  4. package/ai/MANIFEST.json +414 -14
  5. package/ai/REFERENCE.index.json +35 -0
  6. package/ai/REFERENCE.json +184 -8
  7. package/ai/UI_GUIDE.md +25 -4
  8. package/ai/components/badge.md +39 -2
  9. package/ai/components/list-item.md +105 -9
  10. package/ai/components/step.md +112 -0
  11. package/ai/components/steps.md +124 -0
  12. package/ai/components/table.md +98 -2
  13. package/ai/components/timeline-event.md +144 -0
  14. package/ai/components/timeline.md +201 -0
  15. package/ai/components/toggle.md +11 -1
  16. package/ai/reference/bf-action-directive.md +3 -1
  17. package/ai/reference/bf-badge-emphasis.md +27 -0
  18. package/ai/reference/bf-labels.md +1 -0
  19. package/ai/reference/bf-step-heading-level.md +28 -0
  20. package/ai/reference/bf-table-align.md +1 -1
  21. package/ai/reference/bf-table-column-align.md +33 -0
  22. package/ai/reference/bf-table-column.md +7 -7
  23. package/ai/reference/bf-table-harness.md +12 -0
  24. package/ai/reference/bf-timeline-tone.md +31 -0
  25. package/ai/reference/bf-toggle-harness.md +55 -0
  26. package/ai/reference/provide-bf-labels.md +1 -1
  27. package/dark-ink.css +124 -0
  28. package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
  29. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  30. package/fesm2022/bitforgehq-ui-testing.mjs +51 -2
  31. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  32. package/fesm2022/bitforgehq-ui.mjs +682 -32
  33. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  34. package/llms-full.txt +1072 -15
  35. package/llms.txt +9 -0
  36. package/package.json +2 -1
  37. package/theme.css +24 -0
  38. package/types/bitforgehq-ui-testing.d.ts +76 -1
  39. package/types/bitforgehq-ui.d.ts +288 -8
package/llms-full.txt CHANGED
@@ -72,7 +72,7 @@ In `src/index.html`, give `<body>` the theme background and text colour, so the
72
72
 
73
73
  ### 7. Switch to dark mode
74
74
 
75
- Add the `dark` class to `<html>` (and remove it for light mode). Every token is redefined under `.dark`, and the theme sets `color-scheme`, so scrollbars and native controls follow too. Components need no `dark:` classes. To let users choose System, Light or Dark, add `provideBfColorScheme()` to the app providers: it sets the class, follows the OS and remembers the choice.
75
+ Add the `dark` class to `<html>` (and remove it for light mode). Every token is redefined under `.dark`, and the theme sets `color-scheme`, so scrollbars and native controls follow too. Components need no `dark:` classes. To let users choose System, Light or Dark, add `provideBfColorScheme()` to the app providers: it sets the class, follows the OS and remembers the choice. For a near-black dark mode, add `@import '@bitforgehq/ui/dark-ink.css';` right after the theme import: the Ink preset, which changes dark mode only.
76
76
 
77
77
  ```html
78
78
  <html lang="en" class="dark">
@@ -123,6 +123,10 @@ pressed, soft and strong steps, `on-primary`, `primary-text`, `selected`, `ring`
123
123
  and a `.dark` block with each contrast pair's ratio, and exits 1 naming the pairs below their
124
124
  minimum and a shade that passes; paste its output, never hand-pick the steps. Example (the
125
125
  Meadow Sage green): `bunx bf-ui theme '#238652' --dark '#5cc48a'`.
126
+ For a near-black dark mode, import the opt-in Ink preset right after the theme,
127
+ `@import '@bitforgehq/ui/dark-ink.css';`: it redefines every dark token but the brand set under
128
+ `.dark` (light mode and the default dark stay as they are), so a brand `.dark` block goes after
129
+ it. Never copy its values into `.dark` by hand, and never put `dark:` classes on widgets for it.
126
130
  `--radius-control` (`9999px`, pills) reshapes buttons, chips, badges, segmented, the tab bar, the
127
131
  action bar and toggles at once:
128
132
 
@@ -143,10 +147,10 @@ Render library components only in the browser, for example in a `@defer` block o
143
147
  route. Support will be decided before 1.0.
144
148
 
145
149
  The one server-side check is a render smoke, not a support test: the library repository's
146
- `tools/ssr-smoke.ts` renders 43 fixtures with `renderApplication()` (every root widget, `bf-icon`,
150
+ `tools/ssr-smoke.ts` renders 45 fixtures with `renderApplication()` (every root widget, `bf-icon`,
147
151
  `bf-number-input`, a `/charts` line chart, a `/mobile` page with `provideBfShell()`, `bf-page`,
148
152
  `bf-header` and `bf-content`, `bf-tabs` with `bf-stack-outlet` under the router, and an `/overlay`
149
- dialog), and all of them render without an error. 27 of them render identically twice. The other 16
153
+ dialog), and all of them render without an error. 29 of them render identically twice. The other 16
150
154
  number their ids differently on every render, from a module-level counter, so they could not
151
155
  hydrate: `bf-app-frame`, `bf-calendar`, `bf-checkbox`, `bf-input`, `bf-radio-group`, `bf-segmented`,
152
156
  `bf-select`, `bf-sidebar`, `bf-tab-group`, `bf-table`, `bf-textarea`, `bf-theme-switch`,
@@ -374,8 +378,14 @@ protected readonly colorScheme = inject(BfColorSchemeService);
374
378
  sorts the rows in a `computed()`, and a selection shows a `bf-action-bar`. `stack="narrow"`
375
379
  (below 36rem of its own width) or `stack="coarse"` turns each row into a label/value tile with
376
380
  a sort and select bar; in the Capacitor shell use `bf-list-item` rows instead.
381
+ A settings matrix puts a `bf-toggle` in each cell (`align: 'center'` columns, the row label
382
+ first, every toggle named `[ariaLabel]="column + ', ' + row"`).
377
383
  - **Details:** `bf-description-list` of `div[bfDescriptionItem]` label/value pairs, and `bf-stat`
378
384
  figure tiles in a grid; both work in both shells.
385
+ - **History:** `<bf-timeline label="…">` of `<bf-timeline-event actor="…" [time]="iso" tone="…">`
386
+ (text with inline links in the content, a small action in `bfSlot="end"`) for a record's
387
+ history or an audit trail, in the order the app gives; times follow `LOCALE_ID` (`timeFormat`,
388
+ `timeText` for a relative time). Not for navigation or steps.
379
389
  - **Cards in a grid:** the grid stretches each `bf-card` and its `bfSlot="footer"` sits on the
380
390
  bottom edge, so footers line up; put a footer's parts in one element.
381
391
  - **Rows on a card:** `<bf-list [inset]="false" divided class="-mx-card">`, flush to the card's
@@ -408,6 +418,13 @@ protected readonly colorScheme = inject(BfColorSchemeService);
408
418
  - **Empty pages:** a 404, "no access" or "nothing here yet" page is a `bf-empty-state` with
409
419
  `headingLevel` 1, centred in the content area, with the main action and an optional link.
410
420
  - **Toolbar on a phone:** `bf-toolbar` keeps its status and actions together on the last row.
421
+ - **Severity:** soft `bf-badge` tones for most states; `emphasis="strong"` (a solid fill, also on
422
+ a count pill) only for the one that must stand out, such as `variant="danger"` "Critical".
423
+ - **Inbox and notifications:** `bf-list-item [unread]="!item.read"` (dot, bolder title, "Unread"
424
+ read before the title, also in its `bfAction`'s name); clear it when the row is opened.
425
+ - **Instructions in order:** `<bf-steps>` of `<bf-step title="Install the app">…details…</bf-step>`
426
+ (`start` to continue a list) for setup steps in both shells; not a wizard (a multi-page flow
427
+ with state is a set of pages).
411
428
 
412
429
  ## Overlays (`@bitforgehq/ui/overlay`)
413
430
 
@@ -976,7 +993,7 @@ describe('bf-button in a consumer app (Vitest)', () => {
976
993
 
977
994
  ### 6. Drive a form widget through its harness
978
995
 
979
- The entry ships CDK harnesses for the widgets whose markup is not obvious to drive: `BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfMenuButtonHarness`, `BfDesktopToastHarness`, `BfTableHarness`, `BfPaginationHarness` `BfTabGroupHarness`, `BfCalendarHarness` and `BfChartHarness`. `with({ label })` finds a widget by its label. Load an in-page widget with `TestbedHarnessEnvironment.loader(fixture)`.
996
+ The entry ships CDK harnesses for the widgets whose markup is not obvious to drive: `BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfMenuButtonHarness`, `BfDesktopToastHarness`, `BfTableHarness`, `BfToggleHarness`, `BfPaginationHarness` `BfTabGroupHarness`, `BfCalendarHarness` and `BfChartHarness`. `with({ label })` finds a widget by its label. Load an in-page widget with `TestbedHarnessEnvironment.loader(fixture)`.
980
997
 
981
998
  `select-harness.spec.ts`:
982
999
 
@@ -1239,7 +1256,11 @@ notes.
1239
1256
  | `bf-progress` | `@bitforgehq/ui` | Progress bar in two sizes and four tones for completion, quotas and unknown waits |
1240
1257
  | `bf-sparkline` | `@bitforgehq/ui/charts` | Tiny trend line with no axes or legend, for stats and table cells |
1241
1258
  | `bf-stat` | `@bitforgehq/ui` | One labelled figure with an optional icon tile and a supporting line |
1259
+ | `bf-step` | `@bitforgehq/ui` | One numbered step of an instruction list, with a title and its details |
1260
+ | `bf-steps` | `@bitforgehq/ui` | Numbered instruction steps a user follows in order, with joined markers |
1242
1261
  | `bf-table` | `@bitforgehq/ui` | Desktop data table with sortable headers, row selection, custom cells and an empty slot |
1262
+ | `bf-timeline` | `@bitforgehq/ui` | Dated list of events with who did what and when, on a vertical line |
1263
+ | `bf-timeline-event` | `@bitforgehq/ui` | One timeline event with a coloured marker, its text, actor and time |
1243
1264
 
1244
1265
  ### Feedback
1245
1266
 
@@ -3830,6 +3851,31 @@ import { BfBadgeComponent } from '@bitforgehq/ui';
3830
3851
  export class BadgeStatusExampleComponent {}
3831
3852
  ```
3832
3853
 
3854
+ ## Emphasis
3855
+
3856
+ `emphasis="strong"` turns the soft tint into a solid fill with contrasting text, for the one state that must stand out from the rest: a "Critical" finding among "High" and "Medium" ones. It takes every variant (`primary` uses the brand fill) and the count pill too: a strong count takes its fill from `variant`, so `variant="danger" emphasis="strong" [count]="3"` is a red pill, while a soft count stays the primary pill. `emphasis` takes a `BfBadgeEmphasis` (`soft`, the default, or `strong`).
3857
+
3858
+ ```ts
3859
+ import { Component } from '@angular/core';
3860
+ import { BfBadgeComponent } from '@bitforgehq/ui';
3861
+
3862
+ /** A strong badge marks the one critical finding among soft status badges. */
3863
+ @Component({
3864
+ selector: 'app-example-badge-strong',
3865
+ imports: [BfBadgeComponent],
3866
+ template: `
3867
+ <div class="flex flex-wrap items-center gap-2">
3868
+ <bf-badge variant="danger" emphasis="strong">Critical</bf-badge>
3869
+ <bf-badge variant="danger">High</bf-badge>
3870
+ <bf-badge variant="warning">Medium</bf-badge>
3871
+ <bf-badge>Low</bf-badge>
3872
+ <bf-badge variant="danger" emphasis="strong" [count]="3" countLabel="critical findings" />
3873
+ </div>
3874
+ `,
3875
+ })
3876
+ export class BadgeStrongExampleComponent {}
3877
+ ```
3878
+
3833
3879
  ## Count pill
3834
3880
 
3835
3881
  Setting `count` switches to the compact primary pill. Values above 99 render as "99+"; zero renders as "0".
@@ -3892,17 +3938,19 @@ Consumes tokens by variant, background then text:
3892
3938
  - `danger`: `danger-soft` / `danger`
3893
3939
  - `info`: `info-soft` / `info` (≥ 4.5:1 in both modes; it is not derived from the primary, so a custom brand colour leaves it unchanged)
3894
3940
  - Count pill (`count` set): `primary` / `on-primary`, ignoring `variant`
3941
+ - Strong (`emphasis="strong"`, label and count pill): `<variant>-strong` / `on-<variant>-strong` (`neutral-strong`, `success-strong`, `warning-strong`, `danger-strong`, `info-strong`, each with its `on-` text token; ≥ 4.5:1 in both modes and the fill ≥ 3:1 on `bg`, `surface` and `surface-sunken`); `primary` uses `primary` / `on-primary`. An app can retune `--color-danger-strong` and `--color-on-danger-strong` (and the others) in `@theme` and `.dark` like any status token
3895
3942
  - Shape (label and count pill): `rounded-control` (`--radius-control`, `9999px` by default).
3896
3943
 
3897
3944
  ## Accessibility
3898
3945
 
3899
- A badge is a plain `<span>` with no role or ARIA attributes; its text is read as part of the surrounding content, so pair it with a label or context that says what it means rather than relying on color alone. A count pill with `countLabel` carries that context itself: a visually hidden (`sr-only`) span after the number, read as one phrase with it ("99+ unread messages").
3946
+ A badge is a plain `<span>` with no role or ARIA attributes; its text is read as part of the surrounding content, so pair it with a label or context that says what it means rather than relying on color alone. A count pill with `countLabel` carries that context itself: a visually hidden (`sr-only`) span after the number, read as one phrase with it ("99+ unread messages"). A strong badge is louder, not more meaningful: its text ("Critical", "Kritisch") still says the state. In forced colours (Windows High Contrast) the fills give way to the system colours, so a strong badge draws a `CanvasText` outline and still reads as a filled pill next to a soft one.
3900
3947
 
3901
3948
  ## Inputs
3902
3949
 
3903
3950
  | Name | Type | Default | Description |
3904
3951
  | --- | --- | --- | --- |
3905
- | `variant` | `BfBadgeAppearance` (`'neutral' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'`) | `'neutral'` | Tint for the label style: neutral, primary, success, warning, danger or info. Ignored when `count` is set. |
3952
+ | `variant` | `BfBadgeAppearance` (`'neutral' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'`) | `'neutral'` | Tint for the label style: neutral, primary, success, warning, danger or info. Ignored by a soft count pill; a strong one takes its fill from it. |
3953
+ | `emphasis` | `BfBadgeEmphasis` (`'soft' \| 'strong'`) | `'soft'` | `soft` (a pale tint, the default) or `strong` (a solid fill with contrasting text, also for the count pill). |
3906
3954
  | `count` | `number \| undefined` | `undefined` | Switches to the count pill; values above 99 render as "99+". |
3907
3955
  | `countLabel` | `string` | `''` | What the count counts ("unread messages"), read by assistive tech after the number ("99+ unread messages"); visually hidden. Only used with `count`. |
3908
3956
 
@@ -3916,15 +3964,25 @@ A badge is a plain `<span>` with no role or ARIA attributes; its text is read as
3916
3964
 
3917
3965
  - Keep labels to one or two words; badges are scanned, not read.
3918
3966
  - Pair a count pill with a label or icon that says what is being counted, and set `countLabel` so the number is not read alone.
3967
+ - Keep `emphasis="strong"` for the one state that must stand out on the screen, such as critical findings.
3919
3968
 
3920
3969
  ### Don't
3921
3970
 
3922
3971
  - Don't use a badge as a button; it has no interactive states.
3923
3972
  - Don't invent new colors on it; pick the variant that matches the meaning.
3924
3973
  - Don't use `primary` or `success` for a category; use `info`, so the status tones keep their meaning.
3974
+ - Don't make every badge strong: when all of them shout, none stands out.
3925
3975
 
3926
3976
  ## Migration notes
3927
3977
 
3978
+ ### 0.4.0
3979
+
3980
+ New `emphasis` input (`BfBadgeEmphasis`: `soft`, the default, or `strong`). `strong` is a solid
3981
+ fill with contrasting text in every variant, for the one state that must stand out ("Critical");
3982
+ `primary` uses the brand fill. A strong count pill takes its fill from `variant` (a soft count pill
3983
+ stays primary, as before). In forced colours a strong badge keeps a `CanvasText` outline. Opt-in:
3984
+ without `emphasis` nothing changes.
3985
+
3928
3986
  ### 0.3.1
3929
3987
 
3930
3988
  New `info` tone for informational labels such as a category, on the new `info` / `info-soft`
@@ -13342,6 +13400,7 @@ export class ListItemOverviewExampleComponent {
13342
13400
  One row of a list: leading slot (avatar, icon tile), title and subtitle, trailing slot
13343
13401
  (time, toggle, badge) and an optional chevron for rows that navigate. A navigation row puts a
13344
13402
  `bfAction` link in the `title` slot, so the row stays a list item and the link covers it.
13403
+ `unread` marks a row not yet opened with a dot and a bolder title.
13345
13404
 
13346
13405
  A row of a list: a leading slot (an avatar, an icon), the title and subtitle, a trailing slot (a time, a toggle, a chip) and an optional chevron. Place rows in a [`bf-list`](https://bitforge-ui.web.app/components/list), which gives them the list semantics and the inset container; a row can also stand alone. A row is static text, a navigation link or an action button, never more than one of them.
13347
13406
 
@@ -13472,6 +13531,87 @@ export class ListItemActionsExampleComponent {
13472
13531
  }
13473
13532
  ```
13474
13533
 
13534
+ ## Unread rows
13535
+
13536
+ `unread` marks a row the user has not opened yet, such as a new message or notification: a small `primary` dot at the row's inline start, on the title line, and the title in a bolder weight. It is not colour alone: the row also reads a visually hidden state before the title, from the `listItemUnread` label ("Unread"; set it with `provideBfLabels({ listItemUnread: 'Ungelesen' })` in a German app), so a screen reader says "Unread, Scan finished". The dot sits in the row's own inline-start padding (or the gap after a leading avatar or icon), so read and unread rows keep their text aligned and nothing moves when a row is marked read; without `unread` nothing changes. Clear `unread` when the row is opened.
13537
+
13538
+ ```ts
13539
+ import { Component, computed, signal } from '@angular/core';
13540
+ import { BfActionDirective, BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
13541
+
13542
+ interface Notice {
13543
+ readonly id: string;
13544
+ readonly title: string;
13545
+ readonly subtitle: string;
13546
+ readonly time: string;
13547
+ readonly unread: boolean;
13548
+ }
13549
+
13550
+ /**
13551
+ * An inbox with unread rows.
13552
+ *
13553
+ * Each title is a stretched `<button bfAction>`; opening a row marks it read.
13554
+ */
13555
+ @Component({
13556
+ selector: 'app-example-list-item-unread',
13557
+ imports: [BfActionDirective, BfListComponent, BfListItemComponent],
13558
+ template: `
13559
+ <bf-list>
13560
+ @for (notice of notices(); track notice.id) {
13561
+ <bf-list-item [unread]="notice.unread" [subtitle]="notice.subtitle">
13562
+ <button bfSlot="title" bfAction type="button" class="text-start" (click)="open(notice)">
13563
+ {{ notice.title }}
13564
+ </button>
13565
+ <span bfSlot="trailing" class="text-caption text-text-muted">{{ notice.time }}</span>
13566
+ </bf-list-item>
13567
+ }
13568
+ </bf-list>
13569
+ <p data-status aria-live="polite" class="text-caption text-text-muted mt-3">{{ status() }}</p>
13570
+ `,
13571
+ })
13572
+ export class ListItemUnreadExampleComponent {
13573
+ protected readonly notices = signal<readonly Notice[]>([
13574
+ {
13575
+ id: 'scan',
13576
+ title: 'Scan finished',
13577
+ subtitle: '3 new findings on api.example.com',
13578
+ time: '9:41',
13579
+ unread: true,
13580
+ },
13581
+ {
13582
+ id: 'cert',
13583
+ title: 'Certificate expires soon',
13584
+ subtitle: 'shop.example.com, in 12 days',
13585
+ time: '8:15',
13586
+ unread: true,
13587
+ },
13588
+ {
13589
+ id: 'report',
13590
+ title: 'Weekly report',
13591
+ subtitle: 'Ready to download',
13592
+ time: 'Mon',
13593
+ unread: false,
13594
+ },
13595
+ ]);
13596
+ private readonly opened = signal('');
13597
+
13598
+ protected readonly status = computed(() => {
13599
+ const unread = this.notices().filter((notice) => notice.unread).length;
13600
+ const opened = this.opened();
13601
+ return opened ? `${unread} unread · Opened: ${opened}` : `${unread} unread`;
13602
+ });
13603
+
13604
+ protected open(notice: Notice): void {
13605
+ this.opened.set(notice.title);
13606
+ this.notices.update((list) =>
13607
+ list.map((item) => (item.id === notice.id ? { ...item, unread: false } : item)),
13608
+ );
13609
+ }
13610
+ }
13611
+ ```
13612
+
13613
+ With a `bfAction` link or button in the `title` slot, the state becomes part of that element's accessible name: the directive sets `aria-labelledby` to the state text and the element itself (it gives the element an id if it has none, and keeps an `aria-labelledby` of its own after the state), so the link reads "Unread, Scan finished" when tabbed to or listed, and the state text is hidden from the tree so it is read once. An `interactive` row holds the state text inside its button, so its name starts with it too.
13614
+
13475
13615
  ## Desktop
13476
13616
 
13477
13617
  An `interactive` row and a row holding a `bfAction` link take a `surface-muted` fill and the pointer cursor while hovered; a static row has none. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The ring is drawn around the row on keyboard focus only. A navigation row keeps the browser's link behaviour: Ctrl/Cmd-click and a middle click open it in a new tab. The trailing slot sits above the stretched link, so a toggle or button there takes its own click. Rows have one height; they do not follow `provideBfDefaults()`.
@@ -13482,10 +13622,11 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13482
13622
  - Text: the title, the title slot and the subtitle wrap (`wrap-anywhere`); with `truncate` each keeps one line (`truncate`).
13483
13623
  - `interactive` rows and rows holding a `bfAction` link add a pointer, a `surface-muted` hover, a `surface-pressed` fill while pressed (the row's action, for a `bfAction` row) and a `ring` focus ring; `interactive` rows dim to `text-disabled` when `disabled` and have no pressed state then.
13484
13624
  - The `chevron` mark uses `text-subtle`.
13625
+ - `unread`: the dot is `primary` (`CanvasText` in forced colours), `rounded-full` and 8 px (`size-2`); the title turns `font-extrabold`.
13485
13626
 
13486
13627
  ## Accessibility
13487
13628
 
13488
- `bf-list-item` sets `role="listitem"` on its host only inside a `bf-list`; a standalone row has no role, so it never claims a list parent that is not there. A non-interactive row is a plain `<div>`, while `[interactive]="true"` renders it as a native `<button>` so it gets keyboard activation and the button role for free. A navigation row's `bfAction` link in the `title` slot is a real `<a>` inside the list item ([WAI-ARIA APG link pattern](https://www.w3.org/WAI/ARIA/apg/patterns/link/)): exposed as `link` with the title as its name, reached with Tab (in Safari and iOS WKWebView, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise); VoiceOver and Full Keyboard Access are unaffected), followed with Enter (not Space), opened in a new tab with Ctrl/Cmd-click; under forced colours a system-coloured outline around the whole row marks the focused link. Making the host itself a link would take the row out of the list's `listitem` structure, so the link lives inside it. `chevron` is a purely visual CSS mark (an `::after` pseudo-element), so it adds nothing to the accessibility tree.
13629
+ `bf-list-item` sets `role="listitem"` on its host only inside a `bf-list`; a standalone row has no role, so it never claims a list parent that is not there. A non-interactive row is a plain `<div>`, while `[interactive]="true"` renders it as a native `<button>` so it gets keyboard activation and the button role for free. A navigation row's `bfAction` link in the `title` slot is a real `<a>` inside the list item ([WAI-ARIA APG link pattern](https://www.w3.org/WAI/ARIA/apg/patterns/link/)): exposed as `link` with the title as its name, reached with Tab (in Safari and iOS WKWebView, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise); VoiceOver and Full Keyboard Access are unaffected), followed with Enter (not Space), opened in a new tab with Ctrl/Cmd-click; under forced colours a system-coloured outline around the whole row marks the focused link. Making the host itself a link would take the row out of the list's `listitem` structure, so the link lives inside it. `chevron` is a purely visual CSS mark (an `::after` pseudo-element), so it adds nothing to the accessibility tree. An `unread` row's dot carries a visually hidden (`sr-only`) state text from the `listItemUnread` label, read before the title ("Unread, Scan finished"); on a row with a `bfAction` the text is `aria-hidden` and the action names itself with `aria-labelledby` from it and its own content, so the state is part of the link's or button's name and is read once. The row stays a `listitem`.
13489
13630
 
13490
13631
  ### Keyboard
13491
13632
 
@@ -13505,6 +13646,7 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13505
13646
  | `chevron` | `boolean` | `false` | Show a trailing chevron (navigation rows). |
13506
13647
  | `disabled` | `boolean` | `false` | Disable an interactive row. |
13507
13648
  | `truncate` | `boolean` | `false` | Keep the title, the title slot and the subtitle on one line each, cut with an ellipsis. By default they wrap, so nothing is lost at large text sizes. |
13649
+ | `unread` | `boolean` | `false` | Mark the row as not yet opened: a dot at the inline start, a bolder title and a visually hidden state (the `listItemUnread` label) read before the title. |
13508
13650
 
13509
13651
  ## Outputs
13510
13652
 
@@ -13525,14 +13667,26 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13525
13667
 
13526
13668
  - Keep titles short; a long one wraps. Add `truncate` only where one line per row is the pattern (a mail list).
13527
13669
  - Make a row that navigates a `bfAction` link in the `title` slot, and a row that acts on the page `interactive`; put secondary actions in the trailing slot.
13670
+ - Set `unread` on rows not yet opened (messages, notifications) and clear it when the row is opened.
13528
13671
 
13529
13672
  ### Don't
13530
13673
 
13531
13674
  - Don't nest interactive controls inside an `interactive` row (it is one button); use a navigation row with a trailing control instead.
13532
13675
  - Don't wrap a `bf-list-item` in an `<a>`, and don't navigate from an `interactive` row's `(clicked)`.
13676
+ - Don't mark unread rows with a coloured title or a badge of your own; `unread` gives the dot, the weight and the screen-reader state together.
13533
13677
 
13534
13678
  ## Migration notes
13535
13679
 
13680
+ ### 0.4.0
13681
+
13682
+ New `unread` input (boolean, default false) for a row not yet opened: a `primary` dot at the
13683
+ row's inline start on the title line, a bolder title (`font-extrabold`) and a visually hidden
13684
+ state read before the title, from the new label key `listItemUnread` ("Unread"). A row's
13685
+ `bfAction` link or button then names itself `aria-labelledby="<state> <itself>"` ("Unread,
13686
+ Report"; an id is added if it has none, and its own `aria-labelledby` is kept after the state).
13687
+ The dot sits in the row's own inline-start padding, so read and unread rows stay aligned. Opt-in: without `unread` nothing
13688
+ changes.
13689
+
13536
13690
  ### 0.3.0
13537
13691
 
13538
13692
  **Behaviour:** logical properties: text aligns to the inline start and the chevron sits at the
@@ -22974,6 +23128,248 @@ hidden word (new labels `statTrendUp`, `statTrendDown`, `statTrendFlat`). `trend
22974
23128
 
22975
23129
  ---
22976
23130
 
23131
+ # bf-step
23132
+
23133
+ One numbered step of an instruction list, with a title and its details
23134
+
23135
+ `BfStepComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
23136
+
23137
+ ## Example
23138
+
23139
+ ```ts
23140
+ import { Component } from '@angular/core';
23141
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
23142
+
23143
+ /** Steps with a short instruction as the title and the details in the content. */
23144
+ @Component({
23145
+ selector: 'app-example-step-overview',
23146
+ imports: [BfStepComponent, BfStepsComponent],
23147
+ template: `
23148
+ <bf-steps>
23149
+ <bf-step title="Export your contacts"
23150
+ >In your old app, choose Export and save the file.</bf-step
23151
+ >
23152
+ <bf-step title="Import the file">
23153
+ Pick the file you saved. Contacts that are already in your address book are merged, not
23154
+ added twice.
23155
+ </bf-step>
23156
+ <bf-step title="Check the result" />
23157
+ </bf-steps>
23158
+ `,
23159
+ })
23160
+ export class StepOverviewExampleComponent {}
23161
+ ```
23162
+
23163
+ ## Overview
23164
+
23165
+ One step of a `bf-steps` list: a numbered marker, a short instruction as its title and the
23166
+ step's details in the content.
23167
+
23168
+ One step of a [`bf-steps`](https://bitforge-ui.web.app/components/steps) list: `title` is the step's short instruction and the content holds its details (text, a code to type, an image or a button). The list numbers the steps by position; the step draws the numbered circle and the line to the next one. It works the same in both shells.
23169
+
23170
+ ## When to use
23171
+
23172
+ Use `bf-step` for each instruction inside a `bf-steps` list. Use [bf-list-item](https://bitforge-ui.web.app/components/list-item) instead for a row the user opens or acts on, and [bf-description-list](https://bitforge-ui.web.app/components/description-list) for facts with no order.
23173
+
23174
+ ## Headings
23175
+
23176
+ The title is text by default, which suits short steps. For a long guide whose steps hold screenshots or code, set `headingLevel` (`2`, `3` or `4`, a `BfStepHeadingLevel`) to the level that fits the page outline, so a screen-reader user can move from step to step by heading. The look does not change.
23177
+
23178
+ ```ts
23179
+ import { Component } from '@angular/core';
23180
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
23181
+
23182
+ /** Long steps with level 3 headings, so a screen-reader user can move between them. */
23183
+ @Component({
23184
+ selector: 'app-example-step-headings',
23185
+ imports: [BfStepComponent, BfStepsComponent],
23186
+ template: `
23187
+ <bf-steps>
23188
+ <bf-step title="Create an API key" [headingLevel]="3">
23189
+ In the console, open Settings, then API keys, and choose New key. Give it a name that says
23190
+ where it is used, such as "CI pipeline", and copy it: it is shown only once.
23191
+ </bf-step>
23192
+ <bf-step title="Store the key as a secret" [headingLevel]="3">
23193
+ Add it to your CI provider's secrets as BITFORGE_API_KEY. Never commit it to the repository.
23194
+ </bf-step>
23195
+ </bf-steps>
23196
+ `,
23197
+ })
23198
+ export class StepHeadingsExampleComponent {}
23199
+ ```
23200
+
23201
+ ## Desktop
23202
+
23203
+ A step is not interactive: no hover, no pointer cursor, no focus stop; only a link or button in its content takes focus. It renders the same inside `provideBfShell()`.
23204
+
23205
+ ## Theming
23206
+
23207
+ - Marker: `bg-primary-soft` / `text-primary-strong`, `text-label-sm` bold, a 24 px `rounded-full` circle; the line under it is `border-border`, hidden on the last step.
23208
+ - Title: `text-label` bold in `text`, wrapping anywhere; content: `text-body` in `text-secondary`.
23209
+
23210
+ ## Accessibility
23211
+
23212
+ Inside `bf-steps` the host is a `role="listitem"`, so its position is announced ("2 of 3"); the visible number is `aria-hidden`. The title is a `p`, or an `h2`, `h3` or `h4` with `headingLevel`. Outside `bf-steps` the host claims no role.
23213
+
23214
+ ## Inputs
23215
+
23216
+ | Name | Type | Default | Description |
23217
+ | --- | --- | --- | --- |
23218
+ | `title` | `string` | `''` | The step's short instruction ("Install the app"), shown next to the number. |
23219
+ | `headingLevel` | `BfStepHeadingLevel \| undefined` (`2 \| 3 \| 4 \| undefined`) | `undefined` | Renders the title as a heading of this level instead of text; leave it off for short steps. |
23220
+
23221
+ ## Slots
23222
+
23223
+ - `default`: The step's details: text, a code snippet, an image or a button.
23224
+
23225
+ ## Notes
23226
+
23227
+ ### Do
23228
+
23229
+ - Keep the title to one instruction and put everything else in the content.
23230
+
23231
+ ### Don't
23232
+
23233
+ - Don't use `bf-step` outside a `bf-steps` list: it then has no number to show but 1.
23234
+
23235
+ ## Migration notes
23236
+
23237
+ ### 0.4.0
23238
+
23239
+ New `bf-step`: one step of a `bf-steps` list, with `title` (the short instruction, text by
23240
+ default; `headingLevel` 2, 3 or 4, a `BfStepHeadingLevel`, makes it a heading) and the details
23241
+ in the default slot. Its number is drawn in a `primary-soft` circle and hidden from assistive
23242
+ technology, which reads the list position instead.
23243
+
23244
+ ---
23245
+
23246
+ # bf-steps
23247
+
23248
+ Numbered instruction steps a user follows in order, with joined markers
23249
+
23250
+ `BfStepsComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
23251
+
23252
+ ## Example
23253
+
23254
+ ```ts
23255
+ import { Component } from '@angular/core';
23256
+ import { RouterLink } from '@angular/router';
23257
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
23258
+
23259
+ /** Setting up the app on a phone, in four numbered steps with a code to type and a link. */
23260
+ @Component({
23261
+ selector: 'app-example-steps-overview',
23262
+ imports: [RouterLink, BfStepComponent, BfStepsComponent],
23263
+ template: `
23264
+ <bf-steps>
23265
+ <bf-step title="Install the app">
23266
+ Download Bitforge Guard from the App Store or Google Play.
23267
+ </bf-step>
23268
+ <bf-step title="Sign in">Use your work email address and the password you chose.</bf-step>
23269
+ <bf-step title="Pair your phone">
23270
+ Open Settings, then Devices, and enter this code:
23271
+ <code
23272
+ class="rounded-tile bg-surface-sunken text-label-sm text-text mt-2 block w-fit px-3 py-1.5 font-mono"
23273
+ >K7F-29Q</code
23274
+ >
23275
+ </bf-step>
23276
+ <bf-step title="Turn on notifications">
23277
+ Allow them when the app asks, or later in
23278
+ <a routerLink="/components/steps" class="text-primary-text underline"
23279
+ >notification settings</a
23280
+ >.
23281
+ </bf-step>
23282
+ </bf-steps>
23283
+ `,
23284
+ })
23285
+ export class StepsOverviewExampleComponent {}
23286
+ ```
23287
+
23288
+ ## Overview
23289
+
23290
+ Numbered instructions a user follows in order, such as setting up an app on a phone: an
23291
+ ordered list of `bf-step` items, each with a numbered marker joined to the next by a line.
23292
+
23293
+ Numbered instructions a user follows in order: a `bf-steps` list of `bf-step` items, each with a numbered circle, a short instruction as its title and the details under it, the circles joined by a thin line. It works the same in a desktop web app and in the Capacitor shell.
23294
+
23295
+ ## When to use
23296
+
23297
+ Use `bf-steps` for instructions the user carries out in order, outside the app or across it: "Set up the app on your phone", "Connect your domain". It is static content, not a wizard: a multi-page stepper that holds state and moves the user from step to step is a flow of pages (in the Capacitor shell, pages pushed in [bf-tabs](https://bitforge-ui.web.app/components/tabs) stacks), and a progress figure for such a flow is a [bf-progress](https://bitforge-ui.web.app/components/progress). Use [bf-list](https://bitforge-ui.web.app/components/list) instead for items that have no order.
23298
+
23299
+ ## Continued lists
23300
+
23301
+ `start` sets the number of the first step, for a list continued after a paragraph or on another part of the page: `start="3"` numbers the steps 3, 4, 5. It takes a number or a plain attribute.
23302
+
23303
+ ```ts
23304
+ import { Component } from '@angular/core';
23305
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
23306
+
23307
+ /** A list continued after a paragraph: the second list starts at 3. */
23308
+ @Component({
23309
+ selector: 'app-example-steps-continued',
23310
+ imports: [BfStepComponent, BfStepsComponent],
23311
+ template: `
23312
+ <bf-steps>
23313
+ <bf-step title="Open the admin console">Sign in with an administrator account.</bf-step>
23314
+ <bf-step title="Add your domain">Enter it without https:// in front.</bf-step>
23315
+ </bf-steps>
23316
+ <p class="text-body text-text-muted my-4">The console now shows a TXT record for the domain.</p>
23317
+ <bf-steps start="3">
23318
+ <bf-step title="Add the TXT record">Paste it at your DNS provider.</bf-step>
23319
+ <bf-step title="Verify the domain">Changes can take up to an hour to arrive.</bf-step>
23320
+ </bf-steps>
23321
+ `,
23322
+ })
23323
+ export class StepsContinuedExampleComponent {}
23324
+ ```
23325
+
23326
+ ## Desktop
23327
+
23328
+ Steps are not interactive: no hover, no pointer cursor and no focus stop of their own; only a link or button in a step's content takes focus, with its own focus ring. They render the same at every width: the marker column keeps its 24 px and the content wraps (a long word or URL breaks anywhere), so nothing scrolls sideways at 320 px. Put a code snippet that must not wrap in its own `overflow-x-auto` block.
23329
+
23330
+ ## Theming
23331
+
23332
+ - Marker: `bg-primary-soft` / `text-primary-strong` (the tonal pair, ≥ 4.5:1 in both modes), `text-label-sm` bold in a 24 px circle (`size-6`, `rounded-full`); a `CanvasText` outline in forced colours.
23333
+ - Line: `border-border`, 1 px, hidden after the last step.
23334
+ - Title: `text-label` bold in `text`; content: `text-body` in `text-secondary`; `pb-6` between steps.
23335
+
23336
+ ## Accessibility
23337
+
23338
+ `bf-steps` renders a `role="list"` container and each `bf-step` host is a `role="listitem"` directly inside it, so a screen reader announces the list and each step's position ("1 of 3"). The visible number is `aria-hidden`, so it is not read twice. A list with a `start` other than 1 also sets `aria-posinset` and `aria-setsize` on each step (the size counts the earlier steps too: steps 3 to 5 read "3 of 5"), so the announcement matches the number on screen. The title is text by default; give long steps `headingLevel` on `bf-step` so users can move between them by heading. A lone `bf-step` outside `bf-steps` claims no role.
23339
+
23340
+ ## Inputs
23341
+
23342
+ | Name | Type | Default | Description |
23343
+ | --- | --- | --- | --- |
23344
+ | `start` | `number` | `1` | The number of the first step, for a list continued from an earlier one. |
23345
+
23346
+ ## Slots
23347
+
23348
+ - `default`: The steps, each a `bf-step`.
23349
+
23350
+ ## Notes
23351
+
23352
+ ### Do
23353
+
23354
+ - Start each title with a verb and keep it to one instruction ("Install the app").
23355
+ - Put the details, a code to type or a link to open, in the step's content.
23356
+
23357
+ ### Don't
23358
+
23359
+ - Don't use steps for a multi-page flow that tracks where the user is; that is a set of pages.
23360
+ - Don't number steps yourself in the title; the marker shows the number.
23361
+
23362
+ ## Migration notes
23363
+
23364
+ ### 0.4.0
23365
+
23366
+ New `bf-steps`: numbered instructions a user follows in order ("Set up the app on your phone"),
23367
+ a `role="list"` of `bf-step` items whose markers are joined by a line. `start` (a number, 1 by
23368
+ default) continues a list; a continued list sets `aria-posinset` and `aria-setsize` on its steps
23369
+ so the announced position matches the visible number.
23370
+
23371
+ ---
23372
+
22977
23373
  # bf-tab
22978
23374
 
22979
23375
  One tab bar destination with an icon, a label and an optional count badge
@@ -24213,7 +24609,7 @@ Use `bf-table` on a desktop page for records the user scans and compares column
24213
24609
 
24214
24610
  ## Columns and rows
24215
24611
 
24216
- `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.
24612
+ `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; `align: 'center'` centres the header and each cell's control, for a column of toggles or checkboxes (see Toggle cells). `BfTableColumn.align` is a `BfTableColumnAlign`, the released `BfTableAlign` (`start`, `end`) plus `center`.
24217
24613
 
24218
24614
  `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.
24219
24615
 
@@ -24339,6 +24735,90 @@ export class TableCustomCellsExampleComponent {
24339
24735
  }
24340
24736
  ```
24341
24737
 
24738
+ ## Toggle cells
24739
+
24740
+ A settings matrix is a table of switches: rows are what the setting applies to (alert kinds), columns are the options (channels: Email, Push, Slack), and each cell is a `bf-toggle` from a `bfTableCell` template. Keep the row label as the first column, and give the toggle columns `align: 'center'`, which centres the header and the switch in each cell. A toggle in a cell shows no label of its own, so name each one after its column and row with `ariaLabel` (`[ariaLabel]="channel.label + ', ' + alert.label"`, "Push, New CVE"); a screen reader also announces the column header as it moves across. Write the change back from `(changed)` and save it right away, as for any toggle. With `stack="narrow"` (or `coarse` in the Capacitor shell) a phone shows each alert as a tile with the channel labels at the start and the toggles at the end, centred on their label line. In a test, `BfTableHarness.getCellHarness(row, 'Push', BfToggleHarness)` returns a cell's toggle.
24741
+
24742
+ ```ts
24743
+ import { Component, signal } from '@angular/core';
24744
+ import {
24745
+ BfTableCellDirective,
24746
+ BfTableComponent,
24747
+ BfToggleComponent,
24748
+ type BfTableColumn,
24749
+ } from '@bitforgehq/ui';
24750
+
24751
+ type Channel = 'email' | 'push' | 'slack';
24752
+
24753
+ interface AlertSetting {
24754
+ id: string;
24755
+ label: string;
24756
+ email: boolean;
24757
+ push: boolean;
24758
+ slack: boolean;
24759
+ }
24760
+
24761
+ const CHANNELS: readonly { key: Channel; label: string }[] = [
24762
+ { key: 'email', label: 'Email' },
24763
+ { key: 'push', label: 'Push' },
24764
+ { key: 'slack', label: 'Slack' },
24765
+ ];
24766
+
24767
+ /**
24768
+ * A settings matrix of toggle cells.
24769
+ *
24770
+ * Rows are alert kinds and columns are channels, each cell a `bf-toggle` named after its column
24771
+ * and row ("Push, New CVE") with `ariaLabel`, in centred columns (`align: 'center'`). The row label
24772
+ * stays the first column, and `stack="narrow"` turns each row into a tile with the toggles at its
24773
+ * end in a narrow panel or on a phone.
24774
+ */
24775
+ @Component({
24776
+ selector: 'app-example-table-toggle-matrix',
24777
+ imports: [BfTableComponent, BfTableCellDirective, BfToggleComponent],
24778
+ template: `
24779
+ <div class="flex w-152 max-w-full flex-col gap-3">
24780
+ <bf-table label="Alert channels" stack="narrow" [rows]="alerts()" [columns]="columns">
24781
+ @for (channel of channels; track channel.key) {
24782
+ <ng-template [bfTableCell]="channel.key" [bfTableCellOf]="alerts()" let-alert>
24783
+ <bf-toggle
24784
+ [ariaLabel]="channel.label + ', ' + alert.label"
24785
+ [checked]="alert[channel.key]"
24786
+ (changed)="set(alert, channel.key, $event)"
24787
+ />
24788
+ </ng-template>
24789
+ }
24790
+ </bf-table>
24791
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
24792
+ </div>
24793
+ `,
24794
+ })
24795
+ export class TableToggleMatrixExampleComponent {
24796
+ readonly channels = CHANNELS;
24797
+ readonly columns: readonly BfTableColumn[] = [
24798
+ { key: 'label', label: 'Alert' },
24799
+ ...CHANNELS.map((channel): BfTableColumn => ({ ...channel, align: 'center' })),
24800
+ ];
24801
+ readonly alerts = signal<readonly AlertSetting[]>([
24802
+ { id: 'critical', label: 'Critical finding', email: true, push: true, slack: true },
24803
+ { id: 'cve', label: 'New CVE', email: true, push: false, slack: true },
24804
+ { id: 'ci', label: 'Failed CI run', email: false, push: false, slack: true },
24805
+ { id: 'digest', label: 'Weekly digest', email: true, push: false, slack: false },
24806
+ ]);
24807
+ readonly status = signal('No change yet.');
24808
+
24809
+ /** Writes one cell back; a real app saves the setting right away. */
24810
+ set(alert: AlertSetting, channel: Channel, on: boolean): void {
24811
+ this.alerts.update((rows) =>
24812
+ rows.map((row) => (row.id === alert.id ? { ...row, [channel]: on } : row)),
24813
+ );
24814
+ const name = CHANNELS.find((c) => c.key === channel)?.label ?? channel;
24815
+ this.status.set(`${name} for ${alert.label} turned ${on ? 'on' : 'off'}.`);
24816
+ }
24817
+ }
24818
+ ```
24819
+
24820
+ Under a coarse pointer every toggle is a 44 px target inside its own cell (a row grows to fit it), and a cell's horizontal padding keeps neighbouring toggles apart, so no two hit areas overlap. A `sm` toggle (40 px wide) takes a 44 px width under touch as well. ArrowUp and ArrowDown move between the toggles of one column.
24821
+
24342
24822
  ## Empty state
24343
24823
 
24344
24824
  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.
@@ -24685,7 +25165,7 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
24685
25165
 
24686
25166
  ## Accessibility
24687
25167
 
24688
- 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.
25168
+ 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. A control in a custom cell brings its own name: give a label-less one (a toggle in a settings matrix) `ariaLabel` from its column and row, such as "Email, Critical finding", so it is named the same in the table and in a stacked tile. The empty row's cell spans every column.
24689
25169
 
24690
25170
  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) / ""`, the `content-data-label` utility of `theme.css`), so a screen reader reads the column header once, not the label as well.
24691
25171
 
@@ -24754,6 +25234,7 @@ Selects or clears one row.
24754
25234
  ### Do
24755
25235
 
24756
25236
  - Give every table a `label`, and every sortable column a short header.
25237
+ - In a settings matrix, keep the row label as the first column, centre the toggle columns and name every toggle after its column and row.
24757
25238
  - Sort and filter the rows in a `computed()` from `sort` and your filters.
24758
25239
  - 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`.
24759
25240
 
@@ -24765,6 +25246,17 @@ Selects or clears one row.
24765
25246
 
24766
25247
  ## Migration notes
24767
25248
 
25249
+ ### 0.4.0
25250
+
25251
+ New: toggle cells for a settings matrix (rows of alert kinds, columns of channels, a `bf-toggle`
25252
+ in each cell), documented with the `table/toggle-matrix` example. `BfTableColumn.align` takes
25253
+ `'center'`: the header and each cell's control are centred, and while stacked the control sits at
25254
+ the tile's end, centred on its label line. New type `BfTableColumnAlign` (`BfTableAlign` plus
25255
+ `'center'`; `BfTableAlign` keeps its two members). A stacked cell of a start or end column keeps
25256
+ its baseline alignment. `@bitforgehq/ui/testing`: new `BfTableHarness.getCellHarness(row, column,
25257
+ query)` returns the harness of a control in one cell, and `selectRow()`, `getSelectedCount()`
25258
+ read only the selection column's checkboxes, never a checkbox or toggle in a custom cell.
25259
+
24768
25260
  ### 0.3.3
24769
25261
 
24770
25262
  Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
@@ -26245,6 +26737,357 @@ New label keys `timeInputHour`, `timeInputMinute`, `timeInputSecond`, `timeInput
26245
26737
 
26246
26738
  ---
26247
26739
 
26740
+ # bf-timeline
26741
+
26742
+ Dated list of events with who did what and when, on a vertical line
26743
+
26744
+ `BfTimelineComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
26745
+
26746
+ ## Example
26747
+
26748
+ ```ts
26749
+ import { Component } from '@angular/core';
26750
+ import { RouterLink } from '@angular/router';
26751
+ import { BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
26752
+
26753
+ /**
26754
+ * The bf-timeline overview.
26755
+ *
26756
+ * Live demo of `bf-timeline`: the history of a security finding, newest first. A system event has
26757
+ * no actor, the pull request is a link in the event text, the failed CI run is `danger` and the
26758
+ * fix `success`.
26759
+ */
26760
+ @Component({
26761
+ selector: 'app-example-timeline-overview',
26762
+ imports: [RouterLink, BfTimelineComponent, BfTimelineEventComponent],
26763
+ template: `
26764
+ <bf-timeline label="Finding history" class="max-w-xl">
26765
+ <bf-timeline-event actor="Lena Meier" time="2026-10-28T15:40:00Z" tone="success">
26766
+ Marked the finding as fixed
26767
+ </bf-timeline-event>
26768
+ <bf-timeline-event actor="CI" time="2026-10-28T13:05:00Z" tone="danger">
26769
+ CI failed on the security scan
26770
+ </bf-timeline-event>
26771
+ <bf-timeline-event actor="Jonas Keller" time="2026-10-28T11:20:00Z" tone="primary">
26772
+ Linked
26773
+ <a routerLink="." fragment="links-in-events">PR #123</a>
26774
+ to the finding
26775
+ </bf-timeline-event>
26776
+ <bf-timeline-event actor="Jonas Keller" time="2026-10-27T16:02:00Z">
26777
+ Assigned the finding to Lena Meier
26778
+ </bf-timeline-event>
26779
+ <bf-timeline-event time="2026-10-27T08:14:00Z">
26780
+ Created by the nightly dependency scan
26781
+ </bf-timeline-event>
26782
+ </bf-timeline>
26783
+ `,
26784
+ })
26785
+ export class TimelineOverviewExampleComponent {}
26786
+ ```
26787
+
26788
+ ## Overview
26789
+
26790
+ A dated history of events, newest first or oldest first as the consumer orders them: who did
26791
+ what and when, each event a `bf-timeline-event` with a marker on a vertical line.
26792
+
26793
+ A timeline shows the history of one thing as dated events: who did what, and when. Each event is a [`bf-timeline-event`](https://bitforge-ui.web.app/components/timeline-event) with a coloured marker on a vertical line, its text (which may link the objects it names), then the actor and the time. The timeline shows the events in the order you give them, newest first or oldest first; it never sorts. It works the same in a desktop web app, a narrow side panel and the Capacitor shell.
26794
+
26795
+ ## When to use
26796
+
26797
+ Use `bf-timeline` for a record's history or an audit trail: a finding that was created, assigned, linked to a pull request and fixed; the changes in a CVE side panel; who changed a setting. Use [bf-list](https://bitforge-ui.web.app/components/list) instead for items the user opens or acts on, and for navigation; a timeline is not a stepper through a process either, nor a feed the user scrolls for new items. For the details of one record, use a [bf-description-list](https://bitforge-ui.web.app/components/description-list).
26798
+
26799
+ ## Events and tones
26800
+
26801
+ `actor` names who did it (leave it empty for a system event), `time` says when (a `Date` or an ISO 8601 string), and the content is the event text. `tone` colours the marker: `neutral` (the default) for routine events, `primary` for the current or most important one, `success` (fixed, merged), `warning`, `danger` (CI failed, a regression) and `info`. The colour only supports the text, so say what happened in words ("CI failed"), never by the colour alone. The line runs from each marker to the next and ends at the last event.
26802
+
26803
+ ## Links in events
26804
+
26805
+ The event text is the default slot, so the objects it names can be links: a pull request, a repository, a CVE. Put a plain `<a href>` (or `<a routerLink>`) inline in the text; the event styles every link in its text in `primary-text`, underlined, with a thicker underline on hover and the focus ring on keyboard focus. Keep the link text the object's name ("PR #123"), not "here".
26806
+
26807
+ ## Time format
26808
+
26809
+ The time is formatted with `Intl.DateTimeFormat` in the app's `LOCALE_ID`: a medium date and a short time by default (`28 Oct 2026, 14:30` in `en-GB`, `28.10.2026, 14:30` in `de-CH`), in the browser's time zone. `timeFormat` on the timeline takes any `Intl.DateTimeFormatOptions` for all its events (`{ dateStyle: 'short' }`, or a `timeZone`), and `locale` overrides the language for one timeline. `timeText` on an event shows your own text in place of the formatted time, such as a relative "vor 2 Std."; the `time` element's `datetime` keeps the exact value. An empty or invalid `time` shows no time, and a locale or options the browser refuses fall back to the defaults, so a bad value never throws.
26810
+
26811
+ ## Audit trail
26812
+
26813
+ An audit trail is actor-heavy: every event names a person or a bot. The `end` slot holds a small action for one event, such as a Show diff button: it sits at the inline end while the timeline is wide, and wraps under the text in a narrow side panel or on a phone (once the text would get narrower than 16rem). It takes no space while empty.
26814
+
26815
+ ```ts
26816
+ import { Component, signal } from '@angular/core';
26817
+ import { BfButtonComponent, BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
26818
+
26819
+ interface AuditEntry {
26820
+ id: string;
26821
+ actor: string;
26822
+ action: string;
26823
+ time: string;
26824
+ ago: string;
26825
+ diff: boolean;
26826
+ }
26827
+
26828
+ const ENTRIES: readonly AuditEntry[] = [
26829
+ {
26830
+ id: 'severity',
26831
+ actor: 'Lena Meier',
26832
+ action: 'Raised the severity from Medium to High',
26833
+ time: '2026-10-28T14:10:00Z',
26834
+ ago: '2 hours ago',
26835
+ diff: true,
26836
+ },
26837
+ {
26838
+ id: 'policy',
26839
+ actor: 'Policy bot',
26840
+ action: 'Applied the "Critical CVEs block releases" policy',
26841
+ time: '2026-10-28T09:30:00Z',
26842
+ ago: '7 hours ago',
26843
+ diff: true,
26844
+ },
26845
+ {
26846
+ id: 'owner',
26847
+ actor: 'Jonas Keller',
26848
+ action: 'Set the owner to the Platform team',
26849
+ time: '2026-10-27T16:45:00Z',
26850
+ ago: 'Yesterday',
26851
+ diff: false,
26852
+ },
26853
+ ];
26854
+
26855
+ /**
26856
+ * An audit trail with relative times and an action per event.
26857
+ *
26858
+ * Every event names its actor, `timeText` shows a relative time while `datetime` keeps the exact
26859
+ * one, and the `end` slot holds a Show diff button: at the inline end while the timeline is wide,
26860
+ * under the text in a narrow panel.
26861
+ */
26862
+ @Component({
26863
+ selector: 'app-example-timeline-audit',
26864
+ imports: [BfButtonComponent, BfTimelineComponent, BfTimelineEventComponent],
26865
+ template: `
26866
+ <div class="flex max-w-xl flex-col gap-3">
26867
+ <bf-timeline label="Audit trail">
26868
+ @for (entry of entries; track entry.id) {
26869
+ <bf-timeline-event [actor]="entry.actor" [time]="entry.time" [timeText]="entry.ago">
26870
+ {{ entry.action }}
26871
+ @if (entry.diff) {
26872
+ <bf-button bfSlot="end" variant="outline" size="sm" (clicked)="showDiff(entry)">
26873
+ Show diff
26874
+ </bf-button>
26875
+ }
26876
+ </bf-timeline-event>
26877
+ }
26878
+ </bf-timeline>
26879
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
26880
+ </div>
26881
+ `,
26882
+ })
26883
+ export class TimelineAuditExampleComponent {
26884
+ readonly entries = ENTRIES;
26885
+ readonly status = signal('No diff open.');
26886
+
26887
+ /** A real app would open the change in a side panel. */
26888
+ showDiff(entry: AuditEntry): void {
26889
+ this.status.set(`Diff of "${entry.action}" by ${entry.actor}.`);
26890
+ }
26891
+ }
26892
+ ```
26893
+
26894
+ ## Desktop
26895
+
26896
+ The timeline itself is not interactive: no hover, no pointer cursor, no focus stop. Only the links in the event text and the controls in an `end` slot take focus, with their own hover and focus ring. Its spacing does not follow `provideBfDefaults()`: the events keep one rhythm at every density, and it has no `size`. Inside `provideBfShell()` it renders and behaves the same; the `end` slot's buttons bring their own 44 px touch height.
26897
+
26898
+ ## Theming
26899
+
26900
+ - Marker: a 10 px dot (`rounded-control`) in `text-subtle` (neutral), `primary`, `success`, `warning`, `danger` or `info`; `CanvasText` under forced colours.
26901
+ - Line: a 1 px `border` line from marker to marker.
26902
+ - Text: `text-body` in `text`; links `primary-text`, underlined, `ring` on keyboard focus. Meta line: `text-caption` in `text-muted`.
26903
+ - Spacing: 12 px between the marker and the text, 20 px between events.
26904
+
26905
+ ## Accessibility
26906
+
26907
+ The timeline is a list (`role="list"`, named by `label` through `aria-label`) and each event inside it a `listitem`, so a screen reader announces the number of events and the position of each. An event outside a `bf-timeline` claims no role. The marker and the line are `aria-hidden`; the tone is decoration, so the text must say what happened. The time is a `time` element whose `datetime` is the ISO value, and the separator between actor and time is hidden from assistive technology. Links and `end` slot controls are native elements in reading order.
26908
+
26909
+ ## Inputs
26910
+
26911
+ | Name | Type | Default | Description |
26912
+ | --- | --- | --- | --- |
26913
+ | `label` | `string` | `''` | The list's accessible name, such as "History"; empty leaves the list unnamed (a visible heading above it already names the section). |
26914
+ | `timeFormat` | `Intl.DateTimeFormatOptions` | `TIMELINE_TIME_FORMAT` | `Intl.DateTimeFormat` options of every event's time; by default a medium date and a short time (`dateStyle: 'medium'`, `timeStyle: 'short'`). |
26915
+ | `locale` | `string` | `''` | BCP 47 locale of the times, such as `de-CH`; empty follows the app's `LOCALE_ID`. |
26916
+
26917
+ ## Slots
26918
+
26919
+ - `default`: The events, each a `bf-timeline-event`, in the order to show them.
26920
+
26921
+ ## Notes
26922
+
26923
+ ### Do
26924
+
26925
+ - Give the timeline a `label` when no visible heading names it, and keep one order (newest first) across the app.
26926
+ - Name the actor and the object in every event; link the object.
26927
+
26928
+ ### Don't
26929
+
26930
+ - Don't use a timeline for navigation or for steps the user still has to take.
26931
+ - Don't carry meaning in the tone alone; say "CI failed", not just a red dot.
26932
+
26933
+ ## Migration notes
26934
+
26935
+ ### 0.4.0
26936
+
26937
+ New: `bf-timeline`, a dated history of events (who did what, and when) for a record's history
26938
+ or an audit trail, in both shells. A `role="list"` named by `label`, in the order the consumer
26939
+ gives (it never sorts); each event's time is formatted with `Intl.DateTimeFormat` in the app's
26940
+ `LOCALE_ID` (`timeFormat`, default a medium date and a short time; `locale` overrides it).
26941
+
26942
+ ---
26943
+
26944
+ # bf-timeline-event
26945
+
26946
+ One timeline event with a coloured marker, its text, actor and time
26947
+
26948
+ `BfTimelineEventComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
26949
+
26950
+ ## Example
26951
+
26952
+ ```ts
26953
+ import { Component } from '@angular/core';
26954
+ import { RouterLink } from '@angular/router';
26955
+ import { BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
26956
+
26957
+ /**
26958
+ * The bf-timeline-event overview.
26959
+ *
26960
+ * Live demo of `bf-timeline-event`: one event per tone in a `bf-timeline`, with and without an
26961
+ * actor, one with a link in its text and one with a relative `timeText`.
26962
+ */
26963
+ @Component({
26964
+ selector: 'app-example-timeline-event-overview',
26965
+ imports: [RouterLink, BfTimelineComponent, BfTimelineEventComponent],
26966
+ template: `
26967
+ <bf-timeline label="Event tones" class="max-w-xl">
26968
+ <bf-timeline-event actor="Lena Meier" time="2026-10-28T15:40:00Z" tone="primary">
26969
+ Opened
26970
+ <a routerLink="/components/timeline" fragment="links-in-events">CVE-2026-1234</a>
26971
+ </bf-timeline-event>
26972
+ <bf-timeline-event actor="CI" time="2026-10-28T15:10:00Z" tone="success">
26973
+ All checks passed
26974
+ </bf-timeline-event>
26975
+ <bf-timeline-event actor="Dependency bot" time="2026-10-28T14:00:00Z" tone="warning">
26976
+ Found an outdated base image
26977
+ </bf-timeline-event>
26978
+ <bf-timeline-event actor="CI" time="2026-10-28T13:05:00Z" tone="danger">
26979
+ CI failed on the security scan
26980
+ </bf-timeline-event>
26981
+ <bf-timeline-event time="2026-10-28T12:00:00Z" timeText="4 hours ago" tone="info">
26982
+ A new advisory was published
26983
+ </bf-timeline-event>
26984
+ <bf-timeline-event actor="Jonas Keller" time="2026-10-27T16:02:00Z">
26985
+ Assigned the finding to Lena Meier
26986
+ </bf-timeline-event>
26987
+ </bf-timeline>
26988
+ `,
26989
+ })
26990
+ export class TimelineEventOverviewExampleComponent {}
26991
+ ```
26992
+
26993
+ ## Overview
26994
+
26995
+ One event of a `bf-timeline`: a marker coloured by `tone`, the event text (the default slot,
26996
+ which may hold links), then who did it (`actor`) and when (`time`, in a `time` element).
26997
+
26998
+ One event of a [`bf-timeline`](https://bitforge-ui.web.app/components/timeline): a marker coloured by `tone`, the event text from the default slot, then a meta line with `actor` and `time`. Place events in a `bf-timeline`, which gives them the list semantics and the time format; the line between the markers ends at the last event.
26999
+
27000
+ ## When to use
27001
+
27002
+ Use `bf-timeline-event` for each dated entry of a `bf-timeline`: a change, a comment, a CI run. Use [bf-list-item](https://bitforge-ui.web.app/components/list-item) instead for a row the user opens or acts on.
27003
+
27004
+ ## Basic usage
27005
+
27006
+ `actor`, `time` (a `Date` or an ISO 8601 string) and `tone` are inputs; the text is the content and may hold links, which take the link style. `timeText` replaces the formatted time with your own text while the `datetime` keeps the exact value. `bfSlot="end"` holds a small action, at the inline end on a wide timeline and below the text on a narrow one.
27007
+
27008
+ ```ts
27009
+ import { Component } from '@angular/core';
27010
+ import { BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
27011
+
27012
+ /**
27013
+ * An event's actor, time and tone.
27014
+ *
27015
+ * Two events of a CVE side panel: one by a person with an exact time, one by a bot with a relative
27016
+ * `timeText` whose `datetime` keeps the exact value.
27017
+ */
27018
+ @Component({
27019
+ selector: 'app-example-timeline-event-basic',
27020
+ imports: [BfTimelineComponent, BfTimelineEventComponent],
27021
+ template: `
27022
+ <bf-timeline label="CVE history" class="max-w-sm">
27023
+ <bf-timeline-event actor="Lena Meier" time="2026-10-28T15:40:00Z" tone="warning">
27024
+ Accepted the risk until the next release
27025
+ </bf-timeline-event>
27026
+ <bf-timeline-event
27027
+ actor="Advisory bot"
27028
+ time="2026-10-28T10:00:00Z"
27029
+ timeText="6 hours ago"
27030
+ tone="info"
27031
+ >
27032
+ Imported the advisory
27033
+ </bf-timeline-event>
27034
+ </bf-timeline>
27035
+ `,
27036
+ })
27037
+ export class TimelineEventBasicExampleComponent {}
27038
+ ```
27039
+
27040
+ ## Desktop
27041
+
27042
+ An event has no hover and no focus stop of its own; only the links in its text and the controls in its `end` slot are interactive. It renders the same inside `provideBfShell()`.
27043
+
27044
+ ## Theming
27045
+
27046
+ - Marker: `text-subtle`, `primary`, `success`, `warning`, `danger` or `info`; `CanvasText` under forced colours. Line: `border`.
27047
+ - Text `text-body` in `text`, links `primary-text`; meta line `text-caption` in `text-muted`.
27048
+
27049
+ ## Accessibility
27050
+
27051
+ Inside a `bf-timeline` the event is a `listitem`; on its own it has no role. The marker is `aria-hidden`; the time is a `time` element with an ISO `datetime`, and the separator between actor and time is hidden from assistive technology.
27052
+
27053
+ ## Inputs
27054
+
27055
+ | Name | Type | Default | Description |
27056
+ | --- | --- | --- | --- |
27057
+ | `actor` | `string` | `''` | Who did it, such as a person or a bot; empty for a system event. |
27058
+ | `time` | `Date \| string \| null` | `null` | When it happened: a Date or an ISO 8601 string; empty or invalid shows no time. |
27059
+ | `timeText` | `string` | `''` | Shown in place of the formatted time, such as "vor 2 Std."; the `time` element's `datetime` keeps the exact value. |
27060
+ | `tone` | `BfTimelineTone` (`'neutral' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'`) | `'neutral'` | The marker's colour. |
27061
+
27062
+ ## Slots
27063
+
27064
+ - `default`: The event text; links in it take the link style.
27065
+ - `end`: Content at the inline end on a wide timeline and below the text on a narrow one, such as a button.
27066
+
27067
+ ## Notes
27068
+
27069
+ ### Do
27070
+
27071
+ - Write the text as what happened ("Assigned to Lena Meier"), and link the object it names.
27072
+
27073
+ ### Don't
27074
+
27075
+ - Don't put more than one small action in the `end` slot; open the details elsewhere.
27076
+
27077
+ ## Migration notes
27078
+
27079
+ ### 0.4.0
27080
+
27081
+ New: `bf-timeline-event`, one event of a `bf-timeline`: a 10 px marker coloured by `tone`
27082
+ (`neutral`, `primary`, `success`, `warning`, `danger`, `info`; new type `BfTimelineTone`) on a
27083
+ `border` line that ends at the last event, the event text in the default slot (links in it take
27084
+ the link style), then `actor` and `time` (a `Date` or an ISO 8601 string, in a `time` element;
27085
+ `timeText` shows a relative text while `datetime` keeps the exact value; an invalid time shows
27086
+ nothing). A `bfSlot="end"` action sits at the inline end, or under the text once the event is
27087
+ narrow.
27088
+
27089
+ ---
27090
+
26248
27091
  # bf-toast
26249
27092
 
26250
27093
  Passive status pill with an optional action that never steals focus
@@ -26902,9 +27745,11 @@ export class ToggleListRowExampleComponent {
26902
27745
  }
26903
27746
  ```
26904
27747
 
27748
+ In a table of switches (a settings matrix: alert kinds by channel), each cell's toggle is named after its column and row with `ariaLabel`; see [bf-table](https://bitforge-ui.web.app/components/table)'s Toggle cells.
27749
+
26905
27750
  ## Sizes
26906
27751
 
26907
- `sm` (32 px row, 24×40 track), `md` (default, 40 px row, 28×48 track), `lg` (48 px row, 32×56 track). Unset, `size` follows `provideBfDefaults()`'s `controlSize`, `md` by default (`provideBfDefaults({ size: 'lg' })` enlarges every control of a Capacitor app; `{ size: 'sm' }` in a component's `providers` makes a dense sub-tree). Under a fine pointer the row is the hit area (32 px and up, above the 24 px of WCAG 2.5.8); under a coarse pointer (touch) `sm` and `md` rows grow to 44 px. The knob travels to the inline end, so it moves left under `dir="rtl"`.
27752
+ `sm` (32 px row, 24×40 track), `md` (default, 40 px row, 28×48 track), `lg` (48 px row, 32×56 track). Unset, `size` follows `provideBfDefaults()`'s `controlSize`, `md` by default (`provideBfDefaults({ size: 'lg' })` enlarges every control of a Capacitor app; `{ size: 'sm' }` in a component's `providers` makes a dense sub-tree). Under a fine pointer the row is the hit area (32 px and up, above the 24 px of WCAG 2.5.8); under a coarse pointer (touch) `sm` and `md` rows grow to 44 px, and a label-less `sm` switch (40 px wide) also takes a 44 px width, its track centred. The knob travels to the inline end, so it moves left under `dir="rtl"`.
26908
27753
 
26909
27754
  ```ts
26910
27755
  import { Component, signal } from '@angular/core';
@@ -27097,6 +27942,14 @@ Focuses the native switch; Signal Forms calls it from `focusBoundControl()`.
27097
27942
 
27098
27943
  ## Migration notes
27099
27944
 
27945
+ ### 0.4.0
27946
+
27947
+ A label-less `sm` toggle (a table cell) takes a 44 px min-width under a coarse pointer, its track
27948
+ centred, so its touch target is 44 × 44 px (it was 40 px wide). Nothing changes under a fine
27949
+ pointer or for `md` and `lg`. `@bitforgehq/ui/testing`: new `BfToggleHarness` (`with({ label })`
27950
+ by the switch's computed name, `getLabel()`, `isChecked()`, `isDisabled()`, `isReadonly()`,
27951
+ `toggle()`, `check()`, `uncheck()`, `getErrorText()`).
27952
+
27100
27953
  ### 0.3.0
27101
27954
 
27102
27955
  New `ariaLabelledby` input: the id of the element that names the switch when no visible `label`
@@ -28140,7 +28993,9 @@ exposed: the heading stays a heading and the accessible name is the title alone.
28140
28993
  with the nearest card or row, which then draws the pointer, the hover and, while the action has
28141
28994
  keyboard focus, the focus ring; a card is never lit by the actions of rows nested in it. Give
28142
28995
  secondary controls inside the surface `class="relative z-1"` so they stay clickable above the
28143
- stretched area.
28996
+ stretched area. When the surface marks a state that belongs in the name (an `unread` list
28997
+ row), the directive names the element `aria-labelledby="<state id> <own id>"`, so the link reads
28998
+ "Unread, Account", and restores the element's own `aria-labelledby` when the state goes.
28144
28999
 
28145
29000
  ```ts
28146
29001
  import { BfActionDirective } from '@bitforgehq/ui';
@@ -28376,6 +29231,36 @@ type BfBadgeAppearance = BfBadgeVariant | 'info'
28376
29231
 
28377
29232
  ---
28378
29233
 
29234
+ # BfBadgeEmphasis
29235
+
29236
+ The two badge emphases, the soft tint or the solid strong fill
29237
+
29238
+ Type from `@bitforgehq/ui` · since 0.4.0
29239
+
29240
+ How loud a `bf-badge` is: `soft`, a pale tint with tinted text (the default), or `strong`, a
29241
+ solid fill with contrasting text for the one state that must stand out, such as "Critical".
29242
+
29243
+ ```ts
29244
+ import { type BfBadgeEmphasis } from '@bitforgehq/ui';
29245
+ ```
29246
+
29247
+ ## Signature
29248
+
29249
+ ```ts
29250
+ type BfBadgeEmphasis = 'soft' | 'strong'
29251
+ ```
29252
+
29253
+ ## Values
29254
+
29255
+ - `soft`
29256
+ - `strong`
29257
+
29258
+ ## Used by
29259
+
29260
+ - [bf-badge](../components/badge.md)
29261
+
29262
+ ---
29263
+
28379
29264
  # BfBadgeVariant
28380
29265
 
28381
29266
  The five tones of a badge, from neutral counts to danger states
@@ -29421,6 +30306,7 @@ import { type BfLabels } from '@bitforgehq/ui';
29421
30306
  | `chartValue` | `string` | `'Value'` | The value column header of a bf-donut-chart's data table. |
29422
30307
  | `chartEmpty` | `string` | `'No data to show'` | bf-chart-card's text when it has no data and no `empty` slot. |
29423
30308
  | `chartError` | `string` | `'The data could not be loaded'` | bf-chart-card's text when its data failed to load and it has no `error` slot. |
30309
+ | `listItemUnread` | `string` | `'Unread'` | bf-list-item's visually hidden state text before the title of an `unread` row. |
29424
30310
  | `validation` | `BfValidationMessages` | required | Default error text per validator kind (see `BfValidationMessages`). |
29425
30311
 
29426
30312
  ## Uses
@@ -29982,6 +30868,37 @@ type BfStatVariant = 'plain' | 'card'
29982
30868
 
29983
30869
  ---
29984
30870
 
30871
+ # BfStepHeadingLevel
30872
+
30873
+ The heading level a step title can take, two, three or four
30874
+
30875
+ Type from `@bitforgehq/ui` · since 0.4.0
30876
+
30877
+ The heading level of a `bf-step` title, for a guide whose steps hold enough content (screenshots,
30878
+ code) that a screen-reader user moves between them by heading.
30879
+
30880
+ ```ts
30881
+ import { type BfStepHeadingLevel } from '@bitforgehq/ui';
30882
+ ```
30883
+
30884
+ ## Signature
30885
+
30886
+ ```ts
30887
+ type BfStepHeadingLevel = 2 | 3 | 4
30888
+ ```
30889
+
30890
+ ## Values
30891
+
30892
+ - `2`
30893
+ - `3`
30894
+ - `4`
30895
+
30896
+ ## Used by
30897
+
30898
+ - [bf-step](../components/step.md)
30899
+
30900
+ ---
30901
+
29985
30902
  # BfTabBarController
29986
30903
 
29987
30904
  The state a parent that drives a projected tab bar gives it to derive from
@@ -30116,7 +31033,7 @@ type BfTableAlign = 'start' | 'end'
30116
31033
 
30117
31034
  ## Used by
30118
31035
 
30119
- - [BfTableColumn](bf-table-column.md)
31036
+ - [BfTableColumnAlign](bf-table-column-align.md)
30120
31037
 
30121
31038
  ---
30122
31039
 
@@ -30191,11 +31108,11 @@ import { type BfTableColumn } from '@bitforgehq/ui';
30191
31108
  | `key` | `string` | required | The row property the default cell shows; also the key a `bfTableCell` template names. |
30192
31109
  | `label` | `string` | required | The header text. |
30193
31110
  | `sortable` | `boolean` | `false` | Whether the header is a button that sets the table's `sort`. |
30194
- | `align` | `BfTableAlign` (`'start' \| 'end'`) | `'start'` | The inline alignment of the header and cells; `end` also sets tabular figures. |
31111
+ | `align` | `BfTableColumnAlign` (`'start' \| 'end' \| 'center'`) | `'start'` | The inline alignment of the header and cells; `end` also sets tabular figures, `center` centres a cell's control (a toggle, a checkbox) in the column. |
30195
31112
 
30196
31113
  ## Uses
30197
31114
 
30198
- - [BfTableAlign](bf-table-align.md)
31115
+ - [BfTableColumnAlign](bf-table-column-align.md)
30199
31116
 
30200
31117
  ## Used by
30201
31118
 
@@ -30203,6 +31120,42 @@ import { type BfTableColumn } from '@bitforgehq/ui';
30203
31120
 
30204
31121
  ---
30205
31122
 
31123
+ # BfTableColumnAlign
31124
+
31125
+ The inline alignment of a table column: start, end, or centred for controls
31126
+
31127
+ Type from `@bitforgehq/ui` · since 0.4.0
31128
+
31129
+ Where a `bf-table` column lines up its header and cells: a `BfTableAlign`, or `center` for a
31130
+ column of controls such as the toggles of a settings matrix. A new type, so `Record`s over the
31131
+ released `BfTableAlign` keep compiling.
31132
+
31133
+ ```ts
31134
+ import { type BfTableColumnAlign } from '@bitforgehq/ui';
31135
+ ```
31136
+
31137
+ ## Signature
31138
+
31139
+ ```ts
31140
+ type BfTableColumnAlign = BfTableAlign | 'center'
31141
+ ```
31142
+
31143
+ ## Values
31144
+
31145
+ - `start`
31146
+ - `end`
31147
+ - `center`
31148
+
31149
+ ## Uses
31150
+
31151
+ - [BfTableAlign](bf-table-align.md)
31152
+
31153
+ ## Used by
31154
+
31155
+ - [BfTableColumn](bf-table-column.md)
31156
+
31157
+ ---
31158
+
30206
31159
  # BfTableRowKey
30207
31160
 
30208
31161
  Function that returns the unique string key of one table row
@@ -30358,6 +31311,40 @@ with signals.
30358
31311
 
30359
31312
  ---
30360
31313
 
31314
+ # BfTimelineTone
31315
+
31316
+ The marker colour of a timeline event, neutral, primary or a status
31317
+
31318
+ Type from `@bitforgehq/ui` · since 0.4.0
31319
+
31320
+ The colour of a `bf-timeline-event`'s marker: `neutral` (the default), `primary`, or a status
31321
+ (`success`, `warning`, `danger`, `info`), such as danger for a failed CI run.
31322
+
31323
+ ```ts
31324
+ import { type BfTimelineTone } from '@bitforgehq/ui';
31325
+ ```
31326
+
31327
+ ## Signature
31328
+
31329
+ ```ts
31330
+ type BfTimelineTone = 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
31331
+ ```
31332
+
31333
+ ## Values
31334
+
31335
+ - `neutral`
31336
+ - `primary`
31337
+ - `success`
31338
+ - `warning`
31339
+ - `danger`
31340
+ - `info`
31341
+
31342
+ ## Used by
31343
+
31344
+ - [bf-timeline-event](../components/timeline-event.md)
31345
+
31346
+ ---
31347
+
30361
31348
  # BfToggleLabelPosition
30362
31349
 
30363
31350
  Whether a toggle's label sits after or before its switch
@@ -30535,7 +31522,7 @@ every string. In a component's `providers` it covers that sub-tree's widgets and
30535
31522
  `dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,
30536
31523
  `timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,
30537
31524
  `chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,
30538
- `chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,
31525
+ `chartValue`, `chartEmpty`, `chartError`, `listItemUnread`, `validation`); the overlay services (`ok`, `cancel`,
30539
31526
  `actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,
30540
31527
  `notifications`) are root singletons and pick it up only when the caller passes its injector:
30541
31528
  `open(…, { injector: inject(Injector) })`.
@@ -37477,6 +38464,18 @@ The text of one cell.
37477
38464
  | `row` | `number` | The row's position, from 0. |
37478
38465
  | `column` | `string` | The column's header text. |
37479
38466
 
38467
+ ### `getCellHarness<T extends ComponentHarness>(row: number, column: string, query: HarnessQuery<T>): Promise<T>`
38468
+
38469
+ A harness for a control in one cell, such as a `bf-toggle` of a settings matrix or a
38470
+ `bf-select` in a custom cell. Throws for an unknown row or column, or when the cell holds no
38471
+ matching control.
38472
+
38473
+ | Parameter | Type | Description |
38474
+ | --- | --- | --- |
38475
+ | `row` | `number` | The row's position, from 0. |
38476
+ | `column` | `string` | The column's header text. |
38477
+ | `query` | `HarnessQuery<T>` | The harness type, or a predicate from its `with()`. |
38478
+
37480
38479
  ### `sortBy(column: string): Promise<void>`
37481
38480
 
37482
38481
  Clicks a sortable column's header, as a user would: ascending first, then descending.
@@ -37620,6 +38619,64 @@ time, which closes it. Throws for a time the list does not hold (off the `step`)
37620
38619
 
37621
38620
  ---
37622
38621
 
38622
+ # BfToggleHarness
38623
+
38624
+ Test harness for the toggle: reads its name and state and switches it
38625
+
38626
+ Class from `@bitforgehq/ui/testing` · since 0.4.0
38627
+
38628
+ Harness for `bf-toggle`. Its `label` filter matches the switch's name: the text of the elements
38629
+ `ariaLabelledby` names, else `ariaLabel`, else the visible label, so a label-less switch in a
38630
+ table cell or a list row is found the same way.
38631
+
38632
+ ```ts
38633
+ import { BfToggleHarness } from '@bitforgehq/ui/testing';
38634
+ ```
38635
+
38636
+ ## Signature
38637
+
38638
+ ```ts
38639
+ class BfToggleHarness extends ComponentHarness
38640
+ ```
38641
+
38642
+ ## Methods
38643
+
38644
+ ### `getLabel(): Promise<string>`
38645
+
38646
+ The switch's name, as the browser computes it: the text of the elements `ariaLabelledby`
38647
+ names, else `ariaLabel`, else the visible label (the `label` text and the default slot).
38648
+
38649
+ ### `isChecked(): Promise<boolean>`
38650
+
38651
+ Whether the switch is on.
38652
+
38653
+ ### `isDisabled(): Promise<boolean>`
38654
+
38655
+ Whether the switch is disabled.
38656
+
38657
+ ### `isReadonly(): Promise<boolean>`
38658
+
38659
+ Whether the switch is read-only (`aria-readonly`): it shows its state but a click does nothing.
38660
+
38661
+ ### `toggle(): Promise<void>`
38662
+
38663
+ Clicks the switch, as a user would, so it turns on or off. Throws while it is disabled (a
38664
+ user's click would do nothing); a read-only switch takes the click and stays as it is.
38665
+
38666
+ ### `check(): Promise<void>`
38667
+
38668
+ Turns the switch on with a click; an on switch stays on. Throws while it is disabled.
38669
+
38670
+ ### `uncheck(): Promise<void>`
38671
+
38672
+ Turns the switch off with a click; an off switch stays off. Throws while it is disabled.
38673
+
38674
+ ### `getErrorText(): Promise<string>`
38675
+
38676
+ The error text under the switch.
38677
+
38678
+ ---
38679
+
37623
38680
  # BfTooltipHarness
37624
38681
 
37625
38682
  Test harness for a tooltip: shows it, reads its text and whether it shows