@bitforgehq/ui 0.3.2 → 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 (42) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +4 -4
  3. package/ai/MANIFEST.index.json +36 -0
  4. package/ai/MANIFEST.json +436 -24
  5. package/ai/REFERENCE.index.json +35 -0
  6. package/ai/REFERENCE.json +202 -10
  7. package/ai/UI_GUIDE.md +25 -4
  8. package/ai/components/app-frame.md +14 -7
  9. package/ai/components/badge.md +39 -2
  10. package/ai/components/header.md +9 -0
  11. package/ai/components/list-item.md +105 -9
  12. package/ai/components/step.md +112 -0
  13. package/ai/components/steps.md +124 -0
  14. package/ai/components/table.md +119 -5
  15. package/ai/components/timeline-event.md +144 -0
  16. package/ai/components/timeline.md +201 -0
  17. package/ai/components/toggle.md +11 -1
  18. package/ai/reference/bf-action-directive.md +3 -1
  19. package/ai/reference/bf-badge-emphasis.md +27 -0
  20. package/ai/reference/bf-labels.md +2 -0
  21. package/ai/reference/bf-select-harness.md +4 -0
  22. package/ai/reference/bf-step-heading-level.md +28 -0
  23. package/ai/reference/bf-table-align.md +1 -1
  24. package/ai/reference/bf-table-column-align.md +33 -0
  25. package/ai/reference/bf-table-column.md +7 -7
  26. package/ai/reference/bf-table-harness.md +18 -0
  27. package/ai/reference/bf-timeline-tone.md +31 -0
  28. package/ai/reference/bf-toggle-harness.md +55 -0
  29. package/ai/reference/provide-bf-labels.md +2 -2
  30. package/dark-ink.css +124 -0
  31. package/fesm2022/bitforgehq-ui-mobile.mjs +13 -3
  32. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  33. package/fesm2022/bitforgehq-ui-testing.mjs +55 -2
  34. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  35. package/fesm2022/bitforgehq-ui.mjs +714 -46
  36. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  37. package/llms-full.txt +1128 -26
  38. package/llms.txt +9 -0
  39. package/package.json +2 -1
  40. package/theme.css +37 -0
  41. package/types/bitforgehq-ui-testing.d.ts +90 -1
  42. package/types/bitforgehq-ui.d.ts +306 -18
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
 
@@ -2982,8 +3003,8 @@ interface Destination {
2982
3003
  * The bf-app-frame overview.
2983
3004
  *
2984
3005
  * Live demo of `bf-app-frame` in a box of fixed height: one destinations array drives the
2985
- * `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`; the header also holds the
2986
- * brand and a notifications action. Tab into the frame to reveal its skip link; Enter moves focus
3006
+ * `bf-sidebar` (from `md` up) and the narrow header's `bf-menu-button`, at the header's inline
3007
+ * start; the brand follows it, and a notifications action sits at the end. Tab into the frame to reveal its skip link; Enter moves focus
2987
3008
  * to the content. The result line reports it.
2988
3009
  */
2989
3010
  @Component({
@@ -3023,11 +3044,11 @@ interface Destination {
3023
3044
  }
3024
3045
  </bf-sidebar>
3025
3046
 
3026
- <span bfSlot="brand" class="text-heading">Atlas</span>
3027
3047
  <bf-menu-button bfSlot="menu" variant="text" [menu]="navMenu">
3028
3048
  <bf-icon bfSlot="icon-start" [icon]="menu" />
3029
3049
  Sections
3030
3050
  </bf-menu-button>
3051
+ <span bfSlot="brand" class="text-heading">Atlas</span>
3031
3052
  <bf-icon-button
3032
3053
  bfSlot="actions"
3033
3054
  variant="text"
@@ -3094,7 +3115,7 @@ export class AppFrameOverviewExampleComponent {
3094
3115
  ## Overview
3095
3116
 
3096
3117
  The frame of a desktop web app: a skip link, a sticky full-height sidebar column from the
3097
- breakpoint up, a header bar with the brand, the narrow-screen menu and actions below it, and
3118
+ breakpoint up, a header bar with the narrow-screen menu, the brand and actions below it, and
3098
3119
  the `main` landmark with a content column of a set maximum width.
3099
3120
 
3100
3121
  The frame around a desktop web app's pages: a skip link, a [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar) column that sticks to the viewport at its full height from a breakpoint up, a header bar below the breakpoint with the brand, the narrow-screen [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) and end actions, and the `main` landmark with a content column of a set maximum width. It replaces the `hidden md:flex` sidebar, the `md:hidden` menu button and the padded `main` an app would otherwise build by hand. The frame imports no overlay and never reads the router: the menu is your own `bf-menu-button`, projected into a slot.
@@ -3105,7 +3126,7 @@ Use `bf-app-frame` once, at the root of a desktop web app whose top-level destin
3105
3126
 
3106
3127
  ## Slots and layout
3107
3128
 
3108
- Put the `bf-sidebar` in `bfSlot="sidebar"`, the app's name or logo in `bfSlot="brand"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot="menu"` and end actions (the theme menu, the account) in `bfSlot="actions"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the brand at the start, then the menu, and the actions at the end. Keep the brand in the sidebar's own `header` slot too, for the wide layout.
3129
+ Put the `bf-sidebar` in `bfSlot="sidebar"`, the app's name or logo in `bfSlot="brand"`, the narrow-screen `bf-menu-button` of `a[bfMenuItem]` router links in `bfSlot="menu"` and end actions (the theme menu, the account) in `bfSlot="actions"`; everything else is the page. From the breakpoint up the sidebar column sits at the inline start, `sticky top-0` and `h-dvh` tall, and the `bf-sidebar` inside fills it and scrolls its own destinations; the header bar is hidden. Below it the sidebar column is hidden and the header bar (`surface`, a bottom border, sticky at the top) shows the menu at the inline start, then the brand, and the actions at the end: the top app bar convention, and the menu comes first in the tab order too. The order is fixed, whatever order the slots have in your template. Keep the brand in the sidebar's own `header` slot too, for the wide layout.
3109
3130
 
3110
3131
  ## Breakpoint and content width
3111
3132
 
@@ -3242,8 +3263,8 @@ The skip link is a native `<a>` with an href to the main landmark and is the fir
3242
3263
  ## Slots
3243
3264
 
3244
3265
  - `sidebar`: The `bf-sidebar`, shown from the breakpoint up; it fills the column's height.
3245
- - `brand`: The app's name or logo in the narrow header bar.
3246
- - `menu`: The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links.
3266
+ - `menu`: The narrow-screen navigation: a `bf-menu-button` of `a[bfMenuItem]` links, at the header bar's inline start.
3267
+ - `brand`: The app's name or logo in the narrow header bar, after the menu.
3247
3268
  - `actions`: End actions of the narrow header bar, such as the theme menu.
3248
3269
  - `default`: The page: what the router shows, inside the `main` landmark.
3249
3270
 
@@ -3262,6 +3283,13 @@ The skip link is a native `<a>` with an href to the main landmark and is the fir
3262
3283
 
3263
3284
  ## Migration notes
3264
3285
 
3286
+ ### 0.3.3
3287
+
3288
+ Fix: the narrow header bar puts the `menu` slot at the inline start, before the `brand` (0.3.2 put
3289
+ it after the brand), with the actions at the end as before: the top app bar convention. The menu
3290
+ also comes first in the tab order. No API change; an app needs no change unless it relied on the
3291
+ brand coming first.
3292
+
3265
3293
  ### 0.3.2
3266
3294
 
3267
3295
  New `bf-app-frame` in `@bitforgehq/ui`: the frame of a desktop web app. Slots `sidebar` (a
@@ -3823,6 +3851,31 @@ import { BfBadgeComponent } from '@bitforgehq/ui';
3823
3851
  export class BadgeStatusExampleComponent {}
3824
3852
  ```
3825
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
+
3826
3879
  ## Count pill
3827
3880
 
3828
3881
  Setting `count` switches to the compact primary pill. Values above 99 render as "99+"; zero renders as "0".
@@ -3885,17 +3938,19 @@ Consumes tokens by variant, background then text:
3885
3938
  - `danger`: `danger-soft` / `danger`
3886
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)
3887
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
3888
3942
  - Shape (label and count pill): `rounded-control` (`--radius-control`, `9999px` by default).
3889
3943
 
3890
3944
  ## Accessibility
3891
3945
 
3892
- 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.
3893
3947
 
3894
3948
  ## Inputs
3895
3949
 
3896
3950
  | Name | Type | Default | Description |
3897
3951
  | --- | --- | --- | --- |
3898
- | `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). |
3899
3954
  | `count` | `number \| undefined` | `undefined` | Switches to the count pill; values above 99 render as "99+". |
3900
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`. |
3901
3956
 
@@ -3909,15 +3964,25 @@ A badge is a plain `<span>` with no role or ARIA attributes; its text is read as
3909
3964
 
3910
3965
  - Keep labels to one or two words; badges are scanned, not read.
3911
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.
3912
3968
 
3913
3969
  ### Don't
3914
3970
 
3915
3971
  - Don't use a badge as a button; it has no interactive states.
3916
3972
  - Don't invent new colors on it; pick the variant that matches the meaning.
3917
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.
3918
3975
 
3919
3976
  ## Migration notes
3920
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
+
3921
3986
  ### 0.3.1
3922
3987
 
3923
3988
  New `info` tone for informational labels such as a category, on the new `info` / `info-soft`
@@ -11636,6 +11701,15 @@ The title is not in the tab order; after a push, `bf-stack-outlet` focuses it wi
11636
11701
 
11637
11702
  ## Migration notes
11638
11703
 
11704
+ ### 0.3.3
11705
+
11706
+ Fix: `bf-header` measures its title and slots in the next animation frame after a resize, not
11707
+ inside the `ResizeObserver` callback. Its measurement writes the title inset and height, which
11708
+ resize the observed elements, and WebKit then reported "ResizeObserver loop completed with
11709
+ undelivered notifications": an error event that an app's global error listener
11710
+ (`provideBrowserGlobalErrorListeners()`) logs as an error. Several resizes in one frame now
11711
+ measure once.
11712
+
11639
11713
  ### 0.3.2
11640
11714
 
11641
11715
  The `## Desktop` docs say `bf-app-frame` supplies the app-level header bar below its breakpoint.
@@ -13326,6 +13400,7 @@ export class ListItemOverviewExampleComponent {
13326
13400
  One row of a list: leading slot (avatar, icon tile), title and subtitle, trailing slot
13327
13401
  (time, toggle, badge) and an optional chevron for rows that navigate. A navigation row puts a
13328
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.
13329
13404
 
13330
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.
13331
13406
 
@@ -13456,6 +13531,87 @@ export class ListItemActionsExampleComponent {
13456
13531
  }
13457
13532
  ```
13458
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
+
13459
13615
  ## Desktop
13460
13616
 
13461
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()`.
@@ -13466,10 +13622,11 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13466
13622
  - Text: the title, the title slot and the subtitle wrap (`wrap-anywhere`); with `truncate` each keeps one line (`truncate`).
13467
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.
13468
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`.
13469
13626
 
13470
13627
  ## Accessibility
13471
13628
 
13472
- `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`.
13473
13630
 
13474
13631
  ### Keyboard
13475
13632
 
@@ -13489,6 +13646,7 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13489
13646
  | `chevron` | `boolean` | `false` | Show a trailing chevron (navigation rows). |
13490
13647
  | `disabled` | `boolean` | `false` | Disable an interactive row. |
13491
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. |
13492
13650
 
13493
13651
  ## Outputs
13494
13652
 
@@ -13509,14 +13667,26 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
13509
13667
 
13510
13668
  - Keep titles short; a long one wraps. Add `truncate` only where one line per row is the pattern (a mail list).
13511
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.
13512
13671
 
13513
13672
  ### Don't
13514
13673
 
13515
13674
  - Don't nest interactive controls inside an `interactive` row (it is one button); use a navigation row with a trailing control instead.
13516
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.
13517
13677
 
13518
13678
  ## Migration notes
13519
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
+
13520
13690
  ### 0.3.0
13521
13691
 
13522
13692
  **Behaviour:** logical properties: text aligns to the inline start and the chevron sits at the
@@ -22958,6 +23128,248 @@ hidden word (new labels `statTrendUp`, `statTrendDown`, `statTrendFlat`). `trend
22958
23128
 
22959
23129
  ---
22960
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
+
22961
23373
  # bf-tab
22962
23374
 
22963
23375
  One tab bar destination with an icon, a label and an optional count badge
@@ -24197,7 +24609,7 @@ Use `bf-table` on a desktop page for records the user scans and compares column
24197
24609
 
24198
24610
  ## Columns and rows
24199
24611
 
24200
- `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`.
24201
24613
 
24202
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.
24203
24615
 
@@ -24323,6 +24735,90 @@ export class TableCustomCellsExampleComponent {
24323
24735
  }
24324
24736
  ```
24325
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
+
24326
24822
  ## Empty state
24327
24823
 
24328
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.
@@ -24488,7 +24984,7 @@ export class TablePagedExampleComponent {
24488
24984
 
24489
24985
  `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.
24490
24986
 
24491
- 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.
24987
+ 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, since the hidden header's buttons are not shown. Its first option, Not sorted, shows while `sort` is null and sets it back to null; then every sortable column, ascending and descending (`tableSortBy`, `tableSortNone`, `tableSortAscending`, `tableSortDescending` labels). Choosing a column option sets `sort`, as a header click does. In a test, `BfTableHarness.getSortSelect()` returns that select's `BfSelectHarness`.
24492
24988
 
24493
24989
  ```ts
24494
24990
  import { Component, computed, signal } from '@angular/core';
@@ -24523,7 +25019,8 @@ type Width = 'phone' | 'full';
24523
25019
  *
24524
25020
  * `stack="narrow"` turns each row into a tile of label/value pairs while the table is narrower
24525
25021
  * than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
24526
- * holds Select all and a Sort by select. Switch the box between phone width and full width.
25022
+ * holds Select all and a Sort by select, whose first option, Not sorted, shows while `sort` is
25023
+ * null and clears it. Switch the box between phone width and full width.
24527
25024
  */
24528
25025
  @Component({
24529
25026
  selector: 'app-example-table-stacked',
@@ -24668,9 +25165,9 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
24668
25165
 
24669
25166
  ## Accessibility
24670
25167
 
24671
- 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.
24672
25169
 
24673
- 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.
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.
24674
25171
 
24675
25172
  ### Keyboard
24676
25173
 
@@ -24737,6 +25234,7 @@ Selects or clears one row.
24737
25234
  ### Do
24738
25235
 
24739
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.
24740
25238
  - Sort and filter the rows in a `computed()` from `sort` and your filters.
24741
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`.
24742
25240
 
@@ -24748,6 +25246,34 @@ Selects or clears one row.
24748
25246
 
24749
25247
  ## Migration notes
24750
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
+
25260
+ ### 0.3.3
25261
+
25262
+ Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
25263
+ package is installed from npm. 0.3.2 wrote the label as `content-[attr(data-label)_/_""]`; the
25264
+ library build escaped its quotes in the FESM (`\"\"`), so Tailwind built the rule for a class no
25265
+ cell carries and every label `::before` computed to `none`. The cells now carry
25266
+ `before:content-data-label`, a utility `theme.css` defines (below). Apps that added the
25267
+ workaround `@source inline('@max-xl:before:content-[attr(data-label)_/_""] pointer-coarse:before:content-[attr(data-label)_/_""]')`
25268
+ can remove it. consumer-smoke now opens a stacked table built from the packed package in
25269
+ Chromium, under a fine and a coarse pointer, and fails if any FESM class string holds a
25270
+ backslash escape.
25271
+
25272
+ The stacked Sort by select's first option is Not sorted (new label key `tableSortNone`, "Not
25273
+ sorted"): it shows while `sort` is null, so the select is no longer an empty box, and choosing it
25274
+ sets `sort` back to null. New `BfTableHarness.getSortSelect()` returns that select's
25275
+ `BfSelectHarness` (`null` when the table cannot stack or has no sortable column).
25276
+
24751
25277
  ### 0.3.2
24752
25278
 
24753
25279
  New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not
@@ -26211,6 +26737,357 @@ New label keys `timeInputHour`, `timeInputMinute`, `timeInputSecond`, `timeInput
26211
26737
 
26212
26738
  ---
26213
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
+
26214
27091
  # bf-toast
26215
27092
 
26216
27093
  Passive status pill with an optional action that never steals focus
@@ -26868,9 +27745,11 @@ export class ToggleListRowExampleComponent {
26868
27745
  }
26869
27746
  ```
26870
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
+
26871
27750
  ## Sizes
26872
27751
 
26873
- `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"`.
26874
27753
 
26875
27754
  ```ts
26876
27755
  import { Component, signal } from '@angular/core';
@@ -27063,6 +27942,14 @@ Focuses the native switch; Signal Forms calls it from `focusBoundControl()`.
27063
27942
 
27064
27943
  ## Migration notes
27065
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
+
27066
27953
  ### 0.3.0
27067
27954
 
27068
27955
  New `ariaLabelledby` input: the id of the element that names the switch when no visible `label`
@@ -28106,7 +28993,9 @@ exposed: the heading stays a heading and the accessible name is the title alone.
28106
28993
  with the nearest card or row, which then draws the pointer, the hover and, while the action has
28107
28994
  keyboard focus, the focus ring; a card is never lit by the actions of rows nested in it. Give
28108
28995
  secondary controls inside the surface `class="relative z-1"` so they stay clickable above the
28109
- 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.
28110
28999
 
28111
29000
  ```ts
28112
29001
  import { BfActionDirective } from '@bitforgehq/ui';
@@ -28342,6 +29231,36 @@ type BfBadgeAppearance = BfBadgeVariant | 'info'
28342
29231
 
28343
29232
  ---
28344
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
+
28345
29264
  # BfBadgeVariant
28346
29265
 
28347
29266
  The five tones of a badge, from neutral counts to danger states
@@ -29335,6 +30254,7 @@ import { type BfLabels } from '@bitforgehq/ui';
29335
30254
  | `notifications` | `string` | `'Notifications'` | The desktop toast stack's region name. |
29336
30255
  | `tableSelectAll` | `string` | `'Select all rows'` | bf-table's header checkbox, which selects or clears every row. |
29337
30256
  | `tableSortBy` | `string` | `'Sort by'` | The label of a stacked bf-table's sort select. |
30257
+ | `tableSortNone` | `string` | `'Not sorted'` | A stacked bf-table's first sort option: rows in the order given, no column sorted. |
29338
30258
  | `tableSortAscending` | `(column: string) => string` | `` (column) => `${column}, ascending` `` | A stacked bf-table's sort option for a column in ascending order. |
29339
30259
  | `tableSortDescending` | `(column: string) => string` | `` (column) => `${column}, descending` `` | A stacked bf-table's sort option for a column in descending order. |
29340
30260
  | `theme` | `string` | `'Theme'` | bf-theme-switch's and bf-theme-menu-button's name without a label. |
@@ -29386,6 +30306,7 @@ import { type BfLabels } from '@bitforgehq/ui';
29386
30306
  | `chartValue` | `string` | `'Value'` | The value column header of a bf-donut-chart's data table. |
29387
30307
  | `chartEmpty` | `string` | `'No data to show'` | bf-chart-card's text when it has no data and no `empty` slot. |
29388
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. |
29389
30310
  | `validation` | `BfValidationMessages` | required | Default error text per validator kind (see `BfValidationMessages`). |
29390
30311
 
29391
30312
  ## Uses
@@ -29947,6 +30868,37 @@ type BfStatVariant = 'plain' | 'card'
29947
30868
 
29948
30869
  ---
29949
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
+
29950
30902
  # BfTabBarController
29951
30903
 
29952
30904
  The state a parent that drives a projected tab bar gives it to derive from
@@ -30081,7 +31033,7 @@ type BfTableAlign = 'start' | 'end'
30081
31033
 
30082
31034
  ## Used by
30083
31035
 
30084
- - [BfTableColumn](bf-table-column.md)
31036
+ - [BfTableColumnAlign](bf-table-column-align.md)
30085
31037
 
30086
31038
  ---
30087
31039
 
@@ -30156,11 +31108,11 @@ import { type BfTableColumn } from '@bitforgehq/ui';
30156
31108
  | `key` | `string` | required | The row property the default cell shows; also the key a `bfTableCell` template names. |
30157
31109
  | `label` | `string` | required | The header text. |
30158
31110
  | `sortable` | `boolean` | `false` | Whether the header is a button that sets the table's `sort`. |
30159
- | `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. |
30160
31112
 
30161
31113
  ## Uses
30162
31114
 
30163
- - [BfTableAlign](bf-table-align.md)
31115
+ - [BfTableColumnAlign](bf-table-column-align.md)
30164
31116
 
30165
31117
  ## Used by
30166
31118
 
@@ -30168,6 +31120,42 @@ import { type BfTableColumn } from '@bitforgehq/ui';
30168
31120
 
30169
31121
  ---
30170
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
+
30171
31159
  # BfTableRowKey
30172
31160
 
30173
31161
  Function that returns the unique string key of one table row
@@ -30323,6 +31311,40 @@ with signals.
30323
31311
 
30324
31312
  ---
30325
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
+
30326
31348
  # BfToggleLabelPosition
30327
31349
 
30328
31350
  Whether a toggle's label sits after or before its switch
@@ -30491,7 +31513,7 @@ Replaces built-in strings, merged over the parent's (validation merged key by ke
30491
31513
  out or set to `undefined` keeps the parent's string. In `ApplicationConfig.providers` it covers
30492
31514
  every string. In a component's `providers` it covers that sub-tree's widgets and form controls
30493
31515
  (`loading`, `dismiss`, `tabBar`, `tabBadge`, `back`, `refreshing`, `tableSelectAll`, `tableSortBy`,
30494
- `tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,
31516
+ `tableSortNone`, `tableSortAscending`, `tableSortDescending`, `theme`, `themeSystem`, `themeLight`, `themeDark`,
30495
31517
  `breadcrumb`, `breadcrumbExpand`, `navigation`, `navBadge`, `sidebarCollapse`, `sidebarExpand`,
30496
31518
  `skipToContent`, `pagination`, `previousPage`, `nextPage`, `pageOf`, `goToPage`, `tabGroup`,
30497
31519
  `calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`,
@@ -30500,7 +31522,7 @@ every string. In a component's `providers` it covers that sub-tree's widgets and
30500
31522
  `dateInputUnavailable`, `timeInputHour`, `timeInputMinute`, `timeInputSecond`,
30501
31523
  `timeInputDayPeriod`, `timeInputOpenList`, `timeInputInvalid`, `timeInputMin`, `timeInputMax`,
30502
31524
  `chart`, `chartLegend`, `chartValues`, `chartShowData`, `chartHideData`, `chartCategory`,
30503
- `chartValue`, `chartEmpty`, `chartError`, `validation`); the overlay services (`ok`, `cancel`,
31525
+ `chartValue`, `chartEmpty`, `chartError`, `listItemUnread`, `validation`); the overlay services (`ok`, `cancel`,
30504
31526
  `actions`, `sheet`, `modal`, `close`, `expand`, `collapse`, `sheetPosition`, `dialog`,
30505
31527
  `notifications`) are root singletons and pick it up only when the caller passes its injector:
30506
31528
  `open(…, { injector: inject(Injector) })`.
@@ -37274,6 +38296,10 @@ Focuses the native select.
37274
38296
 
37275
38297
  Blurs the native select, which marks a bound form field touched.
37276
38298
 
38299
+ ## Used by
38300
+
38301
+ - [BfTableHarness](bf-table-harness.md)
38302
+
37277
38303
  ---
37278
38304
 
37279
38305
  # BfSidebarToggleHarness
@@ -37438,6 +38464,18 @@ The text of one cell.
37438
38464
  | `row` | `number` | The row's position, from 0. |
37439
38465
  | `column` | `string` | The column's header text. |
37440
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
+
37441
38479
  ### `sortBy(column: string): Promise<void>`
37442
38480
 
37443
38481
  Clicks a sortable column's header, as a user would: ascending first, then descending.
@@ -37451,6 +38489,11 @@ Throws for an unknown or unsortable column.
37451
38489
 
37452
38490
  The sorted column (its `key`) and direction, from `aria-sort`.
37453
38491
 
38492
+ ### `getSortSelect(): Promise<BfSelectHarness | null>`
38493
+
38494
+ The Sort by select in a stacked table's bar: its first option, Not sorted (`tableSortNone`),
38495
+ sets `sort` to null and shows while it is; then each sortable column ascending and descending.
38496
+
37454
38497
  ### `selectRow(index: number): Promise<void>`
37455
38498
 
37456
38499
  Checks one row's checkbox, as a user would; a selected row stays selected. Throws when the
@@ -37471,6 +38514,7 @@ The number of selected rows (checked row checkboxes).
37471
38514
 
37472
38515
  ## Uses
37473
38516
 
38517
+ - [BfSelectHarness](bf-select-harness.md)
37474
38518
  - [BfTableSort](bf-table-sort.md)
37475
38519
 
37476
38520
  ---
@@ -37575,6 +38619,64 @@ time, which closes it. Throws for a time the list does not hold (off the `step`)
37575
38619
 
37576
38620
  ---
37577
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
+
37578
38680
  # BfTooltipHarness
37579
38681
 
37580
38682
  Test harness for a tooltip: shows it, reads its text and whether it shows