@bitforgehq/ui 0.3.3 → 0.5.0-next.3

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 (99) hide show
  1. package/CHANGELOG.md +394 -0
  2. package/README.md +14 -14
  3. package/ai/MANIFEST.index.json +101 -2
  4. package/ai/MANIFEST.json +2630 -746
  5. package/ai/REFERENCE.index.json +196 -0
  6. package/ai/REFERENCE.json +1625 -140
  7. package/ai/UI_GUIDE.md +83 -21
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/badge.md +39 -2
  10. package/ai/components/chat-composer.md +357 -0
  11. package/ai/components/chat-message.md +251 -0
  12. package/ai/components/chat-thread.md +521 -0
  13. package/ai/components/checkbox.md +7 -0
  14. package/ai/components/combobox.md +12 -0
  15. package/ai/components/date-input.md +9 -0
  16. package/ai/components/datetime-input.md +2 -0
  17. package/ai/components/field.md +10 -0
  18. package/ai/components/file-input.md +498 -0
  19. package/ai/components/header.md +1 -1
  20. package/ai/components/image-input.md +368 -0
  21. package/ai/components/image-viewer.md +309 -0
  22. package/ai/components/list-item.md +105 -9
  23. package/ai/components/media-card.md +478 -0
  24. package/ai/components/menu-button.md +2 -0
  25. package/ai/components/menu.md +10 -0
  26. package/ai/components/multi-combobox.md +7 -0
  27. package/ai/components/nav-group.md +10 -3
  28. package/ai/components/nav-item.md +11 -2
  29. package/ai/components/number-input.md +2 -7
  30. package/ai/components/popover.md +13 -0
  31. package/ai/components/radio-group.md +6 -0
  32. package/ai/components/segmented.md +14 -2
  33. package/ai/components/select.md +100 -42
  34. package/ai/components/sidebar-toggle.md +16 -9
  35. package/ai/components/sidebar.md +41 -13
  36. package/ai/components/step.md +112 -0
  37. package/ai/components/steps.md +124 -0
  38. package/ai/components/tab-group.md +18 -2
  39. package/ai/components/table.md +115 -4
  40. package/ai/components/theme-menu-button.md +1 -0
  41. package/ai/components/time-input.md +2 -0
  42. package/ai/components/timeline-event.md +144 -0
  43. package/ai/components/timeline.md +201 -0
  44. package/ai/components/toggle.md +11 -1
  45. package/ai/components/toolbar.md +7 -18
  46. package/ai/reference/bf-action-directive.md +3 -1
  47. package/ai/reference/bf-badge-emphasis.md +27 -0
  48. package/ai/reference/bf-chat-composer-harness.md +64 -0
  49. package/ai/reference/bf-chat-message-role.md +27 -0
  50. package/ai/reference/bf-chat-message-status.md +28 -0
  51. package/ai/reference/bf-fade-in-directive.md +18 -0
  52. package/ai/reference/bf-file-input-harness.md +67 -0
  53. package/ai/reference/bf-file-input-variant.md +27 -0
  54. package/ai/reference/bf-image-input-harness.md +67 -0
  55. package/ai/reference/bf-image-input-shape.md +27 -0
  56. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  57. package/ai/reference/bf-image-viewer-harness.md +87 -0
  58. package/ai/reference/bf-image-viewer-image.md +25 -0
  59. package/ai/reference/bf-image-viewer-options.md +33 -0
  60. package/ai/reference/bf-image-viewer-origin.md +23 -0
  61. package/ai/reference/bf-image-viewer-ref.md +55 -0
  62. package/ai/reference/bf-image-viewer-service.md +32 -0
  63. package/ai/reference/bf-labels.md +105 -74
  64. package/ai/reference/bf-media-card-aspect.md +28 -0
  65. package/ai/reference/bf-media-card-harness.md +66 -0
  66. package/ai/reference/bf-motion-list-directive.md +22 -0
  67. package/ai/reference/bf-prose-directive.md +16 -0
  68. package/ai/reference/bf-rejected-file-reason.md +28 -0
  69. package/ai/reference/bf-rejected-file.md +28 -0
  70. package/ai/reference/bf-select-harness.md +14 -5
  71. package/ai/reference/bf-select-option.md +2 -0
  72. package/ai/reference/bf-size.md +3 -0
  73. package/ai/reference/bf-step-heading-level.md +28 -0
  74. package/ai/reference/bf-table-align.md +1 -1
  75. package/ai/reference/bf-table-column-align.md +33 -0
  76. package/ai/reference/bf-table-column.md +7 -7
  77. package/ai/reference/bf-table-harness.md +12 -0
  78. package/ai/reference/bf-timeline-tone.md +31 -0
  79. package/ai/reference/bf-toggle-harness.md +55 -0
  80. package/ai/reference/bf_table_sort_select.md +23 -0
  81. package/ai/reference/provide-bf-labels.md +10 -3
  82. package/ai/reference/provide-bf-select.md +20 -0
  83. package/bin/bf-ui.js +1 -0
  84. package/dark-ink.css +124 -0
  85. package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
  86. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  87. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  88. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  89. package/fesm2022/bitforgehq-ui-testing.mjs +406 -11
  90. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  91. package/fesm2022/bitforgehq-ui.mjs +11149 -6957
  92. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  93. package/llms-full.txt +5219 -141
  94. package/llms.txt +40 -1
  95. package/package.json +2 -1
  96. package/theme.css +84 -0
  97. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  98. package/types/bitforgehq-ui-testing.d.ts +510 -7
  99. package/types/bitforgehq-ui.d.ts +1757 -186
@@ -0,0 +1,112 @@
1
+ # bf-step
2
+
3
+ One numbered step of an instruction list, with a title and its details
4
+
5
+ `BfStepComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
12
+
13
+ /** Steps with a short instruction as the title and the details in the content. */
14
+ @Component({
15
+ selector: 'app-example-step-overview',
16
+ imports: [BfStepComponent, BfStepsComponent],
17
+ template: `
18
+ <bf-steps>
19
+ <bf-step title="Export your contacts"
20
+ >In your old app, choose Export and save the file.</bf-step
21
+ >
22
+ <bf-step title="Import the file">
23
+ Pick the file you saved. Contacts that are already in your address book are merged, not
24
+ added twice.
25
+ </bf-step>
26
+ <bf-step title="Check the result" />
27
+ </bf-steps>
28
+ `,
29
+ })
30
+ export class StepOverviewExampleComponent {}
31
+ ```
32
+
33
+ ## Overview
34
+
35
+ One step of a `bf-steps` list: a numbered marker, a short instruction as its title and the
36
+ step's details in the content.
37
+
38
+ 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.
39
+
40
+ ## When to use
41
+
42
+ 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.
43
+
44
+ ## Headings
45
+
46
+ 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.
47
+
48
+ ```ts
49
+ import { Component } from '@angular/core';
50
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
51
+
52
+ /** Long steps with level 3 headings, so a screen-reader user can move between them. */
53
+ @Component({
54
+ selector: 'app-example-step-headings',
55
+ imports: [BfStepComponent, BfStepsComponent],
56
+ template: `
57
+ <bf-steps>
58
+ <bf-step title="Create an API key" [headingLevel]="3">
59
+ In the console, open Settings, then API keys, and choose New key. Give it a name that says
60
+ where it is used, such as "CI pipeline", and copy it: it is shown only once.
61
+ </bf-step>
62
+ <bf-step title="Store the key as a secret" [headingLevel]="3">
63
+ Add it to your CI provider's secrets as BITFORGE_API_KEY. Never commit it to the repository.
64
+ </bf-step>
65
+ </bf-steps>
66
+ `,
67
+ })
68
+ export class StepHeadingsExampleComponent {}
69
+ ```
70
+
71
+ ## Desktop
72
+
73
+ 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()`.
74
+
75
+ ## Theming
76
+
77
+ - 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.
78
+ - Title: `text-label` bold in `text`, wrapping anywhere; content: `text-body` in `text-secondary`.
79
+
80
+ ## Accessibility
81
+
82
+ 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.
83
+
84
+ ## Inputs
85
+
86
+ | Name | Type | Default | Description |
87
+ | -------------- | -------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
88
+ | `title` | `string` | `''` | The step's short instruction ("Install the app"), shown next to the number. |
89
+ | `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. |
90
+
91
+ ## Slots
92
+
93
+ - `default`: The step's details: text, a code snippet, an image or a button.
94
+
95
+ ## Notes
96
+
97
+ ### Do
98
+
99
+ - Keep the title to one instruction and put everything else in the content.
100
+
101
+ ### Don't
102
+
103
+ - Don't use `bf-step` outside a `bf-steps` list: it then has no number to show but 1.
104
+
105
+ ## Migration notes
106
+
107
+ ### 0.4.0
108
+
109
+ New `bf-step`: one step of a `bf-steps` list, with `title` (the short instruction, text by
110
+ default; `headingLevel` 2, 3 or 4, a `BfStepHeadingLevel`, makes it a heading) and the details
111
+ in the default slot. Its number is drawn in a `primary-soft` circle and hidden from assistive
112
+ technology, which reads the list position instead.
@@ -0,0 +1,124 @@
1
+ # bf-steps
2
+
3
+ Numbered instruction steps a user follows in order, with joined markers
4
+
5
+ `BfStepsComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { RouterLink } from '@angular/router';
12
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
13
+
14
+ /** Setting up the app on a phone, in four numbered steps with a code to type and a link. */
15
+ @Component({
16
+ selector: 'app-example-steps-overview',
17
+ imports: [RouterLink, BfStepComponent, BfStepsComponent],
18
+ template: `
19
+ <bf-steps>
20
+ <bf-step title="Install the app">
21
+ Download Bitforge Guard from the App Store or Google Play.
22
+ </bf-step>
23
+ <bf-step title="Sign in">Use your work email address and the password you chose.</bf-step>
24
+ <bf-step title="Pair your phone">
25
+ Open Settings, then Devices, and enter this code:
26
+ <code
27
+ class="rounded-tile bg-surface-sunken text-label-sm text-text mt-2 block w-fit px-3 py-1.5 font-mono"
28
+ >K7F-29Q</code
29
+ >
30
+ </bf-step>
31
+ <bf-step title="Turn on notifications">
32
+ Allow them when the app asks, or later in
33
+ <a routerLink="/components/steps" class="text-primary-text underline"
34
+ >notification settings</a
35
+ >.
36
+ </bf-step>
37
+ </bf-steps>
38
+ `,
39
+ })
40
+ export class StepsOverviewExampleComponent {}
41
+ ```
42
+
43
+ ## Overview
44
+
45
+ Numbered instructions a user follows in order, such as setting up an app on a phone: an
46
+ ordered list of `bf-step` items, each with a numbered marker joined to the next by a line.
47
+
48
+ 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.
49
+
50
+ ## When to use
51
+
52
+ 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.
53
+
54
+ ## Continued lists
55
+
56
+ `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.
57
+
58
+ ```ts
59
+ import { Component } from '@angular/core';
60
+ import { BfStepComponent, BfStepsComponent } from '@bitforgehq/ui';
61
+
62
+ /** A list continued after a paragraph: the second list starts at 3. */
63
+ @Component({
64
+ selector: 'app-example-steps-continued',
65
+ imports: [BfStepComponent, BfStepsComponent],
66
+ template: `
67
+ <bf-steps>
68
+ <bf-step title="Open the admin console">Sign in with an administrator account.</bf-step>
69
+ <bf-step title="Add your domain">Enter it without https:// in front.</bf-step>
70
+ </bf-steps>
71
+ <p class="text-body text-text-muted my-4">The console now shows a TXT record for the domain.</p>
72
+ <bf-steps start="3">
73
+ <bf-step title="Add the TXT record">Paste it at your DNS provider.</bf-step>
74
+ <bf-step title="Verify the domain">Changes can take up to an hour to arrive.</bf-step>
75
+ </bf-steps>
76
+ `,
77
+ })
78
+ export class StepsContinuedExampleComponent {}
79
+ ```
80
+
81
+ ## Desktop
82
+
83
+ 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.
84
+
85
+ ## Theming
86
+
87
+ - 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.
88
+ - Line: `border-border`, 1 px, hidden after the last step.
89
+ - Title: `text-label` bold in `text`; content: `text-body` in `text-secondary`; `pb-6` between steps.
90
+
91
+ ## Accessibility
92
+
93
+ `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.
94
+
95
+ ## Inputs
96
+
97
+ | Name | Type | Default | Description |
98
+ | ------- | -------- | ------- | ----------------------------------------------------------------------- |
99
+ | `start` | `number` | `1` | The number of the first step, for a list continued from an earlier one. |
100
+
101
+ ## Slots
102
+
103
+ - `default`: The steps, each a `bf-step`.
104
+
105
+ ## Notes
106
+
107
+ ### Do
108
+
109
+ - Start each title with a verb and keep it to one instruction ("Install the app").
110
+ - Put the details, a code to type or a link to open, in the step's content.
111
+
112
+ ### Don't
113
+
114
+ - Don't use steps for a multi-page flow that tracks where the user is; that is a set of pages.
115
+ - Don't number steps yourself in the title; the marker shows the number.
116
+
117
+ ## Migration notes
118
+
119
+ ### 0.4.0
120
+
121
+ New `bf-steps`: numbered instructions a user follows in order ("Set up the app on your phone"),
122
+ a `role="list"` of `bf-step` items whose markers are joined by a line. `start` (a number, 1 by
123
+ default) continues a list; a continued list sets `aria-posinset` and `aria-setsize` on its steps
124
+ so the announced position matches the visible number.
@@ -148,6 +148,8 @@ export class TabGroupLazyExampleComponent {
148
148
 
149
149
  `[(selected)]` holds the shown panel's `value`; `(changed)` emits the new value after a user change, never for a write of `selected`. While `selected` matches no panel (unset, or a value that went away), the first enabled panel shows; `selected` itself keeps its value until the user picks a tab.
150
150
 
151
+ The underline slides to the newly selected tab, changing width as it goes (200 ms, `--bf-duration-layout`); it lands in place at once on the first render and after a resize, and always under `prefers-reduced-motion`. The panel a user switches to fades in over 200 ms (`animate-panel-in`); the first panel shown, and every panel under reduced motion, appears at once.
152
+
151
153
  ## Sizes and width
152
154
 
153
155
  `size` sets the tabs' height: `sm` 32 px, `md` 40 px, `lg` 48 px. Unset, it follows `provideBfDefaults()`'s `controlSize`, `md` by default. `fitted` stretches the tabs to share the group's width (two or three tabs on a phone). When the tabs do not fit, the row scrolls sideways; focusing a tab with the keyboard scrolls it into view.
@@ -159,7 +161,7 @@ Under a mouse an unselected tab takes a `surface-muted` fill and `text` on hover
159
161
  ## Theming
160
162
 
161
163
  - Tabs: `text-muted` at rest, hover `surface-muted` with `text`, pressed `surface-pressed`; top corners `rounded-tile`.
162
- - Selected tab: `primary-text` with a 2 px `primary` underline on its bottom edge, so the selection never rests on colour alone (`CanvasText` under forced colours; `text-disabled` on a disabled selected tab).
164
+ - Selected tab: `primary-text` with a 2 px `primary` underline on its bottom edge, so the selection never rests on colour alone (`CanvasText` under forced colours; `text-disabled` on a disabled selected tab). It slides over `--bf-duration-layout` with the `ease-md` curve.
163
165
  - Disabled tab: `text-disabled` (never opacity).
164
166
  - The row: a 1 px `border` line under the tabs, across the group's width.
165
167
  - Badge: `primary` with `on-primary` text, `rounded-control`, `text-micro`.
@@ -168,7 +170,7 @@ Under a mouse an unselected tab takes a `surface-muted` fill and `text` on hover
168
170
 
169
171
  ## Accessibility
170
172
 
171
- The row is a `role="tablist"` named by `label`, else `provideBfLabels()`'s `tabGroup` ("Tabs"); give each group on a page its own `label`. Each tab is a native `<button role="tab">` with `aria-selected` and `aria-controls` naming its panel; each `bf-tab-panel` is a `role="tabpanel"` with `aria-labelledby` naming its tab and `tabindex="0"`, and an unselected panel is `hidden`. Only the selected tab is in the tab order (a roving tab stop), so <kbd>Tab</kbd> moves from it into the panel. Selection follows focus (automatic activation): the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> move to a tab and show its panel at once, so keep a panel's first render quick. A disabled tab is native-disabled and skipped. A badge's pill is `aria-hidden`; its count is read after the label from visually hidden text (`tabBadge(n)`, "3 new").
173
+ The row is a `role="tablist"` named by `label`, else `provideBfLabels()`'s `tabGroup` ("Tabs"); give each group on a page its own `label`. Each tab is a native `<button role="tab">` with `aria-selected` and `aria-controls` naming its panel; each `bf-tab-panel` is a `role="tabpanel"` with `aria-labelledby` naming its tab and `tabindex="0"`, and an unselected panel is `hidden`. Only the selected tab is in the tab order (a roving tab stop), so <kbd>Tab</kbd> moves from it into the panel. Selection follows focus (automatic activation): the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> move to a tab and show its panel at once, so keep a panel's first render quick. A disabled tab is native-disabled and skipped. The sliding underline is one decorative `aria-hidden` element of the row. A badge's pill is `aria-hidden`; its count is read after the label from visually hidden text (`tabBadge(n)`, "3 new").
172
174
 
173
175
  ### Keyboard
174
176
 
@@ -213,6 +215,20 @@ The row is a `role="tablist"` named by `label`, else `provideBfLabels()`'s `tabG
213
215
 
214
216
  ## Migration notes
215
217
 
218
+ ### 0.5.0
219
+
220
+ The selected tab's underline is now one indicator element of the row that slides to the newly
221
+ selected tab, changing width as it goes (`--bf-duration-layout`, `ease-md`). The first placement
222
+ and a resize land at once, and so does everything under `prefers-reduced-motion`. It is measured
223
+ from the selected tab's inline-start offset, so it mirrors under `dir="rtl"`, and is re-measured
224
+ on resize and when labels, badges, size or `fitted` change. Until it has been measured (jsdom,
225
+ server render, a hidden group) the tab keeps drawing its own underline, so unit tests and the
226
+ first paint are unchanged. The indicator is `aria-hidden`; roles, keyboard and focus are
227
+ unchanged.
228
+
229
+ The panel a user switches to fades in (200 ms of opacity, new `--animate-panel-in`, behind
230
+ `motion-safe:`); the panel shown on the first render never does.
231
+
216
232
  ### 0.3.1
217
233
 
218
234
  New `bf-tab-group`: in-page tabs over the content they switch, in both shells. A `tablist` of
@@ -114,7 +114,7 @@ Use `bf-table` on a desktop page for records the user scans and compares column
114
114
 
115
115
  ## Columns and rows
116
116
 
117
- `columns` is an array of `BfTableColumn`: the row property `key`, the header `label`, `sortable` and `align`. A default cell shows the row's `key` property as text when it is a string, a number or a boolean, and is empty otherwise (`null`, `undefined`, an object): render a date or an object with a custom cell. `align: 'end'` lines the header and cells up at the inline end and sets tabular figures, for numbers and amounts.
117
+ `columns` is an array of `BfTableColumn`: the row property `key`, the header `label`, `sortable` and `align`. A default cell shows the row's `key` property as text when it is a string, a number or a boolean, and is empty otherwise (`null`, `undefined`, an object): render a date or an object with a custom cell. `align: 'end'` lines the header and cells up at the inline end and sets tabular figures, for numbers and amounts; `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`.
118
118
 
119
119
  `rowKey` returns the string that identifies a row in `selection` and keeps each row's DOM when the rows change. By default it reads the row's `id` (a string or a number), and falls back to the row's index when a row has none; pass `[rowKey]="byEmail"` when the key is another property.
120
120
 
@@ -240,6 +240,90 @@ export class TableCustomCellsExampleComponent {
240
240
  }
241
241
  ```
242
242
 
243
+ ## Toggle cells
244
+
245
+ 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.
246
+
247
+ ```ts
248
+ import { Component, signal } from '@angular/core';
249
+ import {
250
+ BfTableCellDirective,
251
+ BfTableComponent,
252
+ BfToggleComponent,
253
+ type BfTableColumn,
254
+ } from '@bitforgehq/ui';
255
+
256
+ type Channel = 'email' | 'push' | 'slack';
257
+
258
+ interface AlertSetting {
259
+ id: string;
260
+ label: string;
261
+ email: boolean;
262
+ push: boolean;
263
+ slack: boolean;
264
+ }
265
+
266
+ const CHANNELS: readonly { key: Channel; label: string }[] = [
267
+ { key: 'email', label: 'Email' },
268
+ { key: 'push', label: 'Push' },
269
+ { key: 'slack', label: 'Slack' },
270
+ ];
271
+
272
+ /**
273
+ * A settings matrix of toggle cells.
274
+ *
275
+ * Rows are alert kinds and columns are channels, each cell a `bf-toggle` named after its column
276
+ * and row ("Push, New CVE") with `ariaLabel`, in centred columns (`align: 'center'`). The row label
277
+ * stays the first column, and `stack="narrow"` turns each row into a tile with the toggles at its
278
+ * end in a narrow panel or on a phone.
279
+ */
280
+ @Component({
281
+ selector: 'app-example-table-toggle-matrix',
282
+ imports: [BfTableComponent, BfTableCellDirective, BfToggleComponent],
283
+ template: `
284
+ <div class="flex w-152 max-w-full flex-col gap-3">
285
+ <bf-table label="Alert channels" stack="narrow" [rows]="alerts()" [columns]="columns">
286
+ @for (channel of channels; track channel.key) {
287
+ <ng-template [bfTableCell]="channel.key" [bfTableCellOf]="alerts()" let-alert>
288
+ <bf-toggle
289
+ [ariaLabel]="channel.label + ', ' + alert.label"
290
+ [checked]="alert[channel.key]"
291
+ (changed)="set(alert, channel.key, $event)"
292
+ />
293
+ </ng-template>
294
+ }
295
+ </bf-table>
296
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
297
+ </div>
298
+ `,
299
+ })
300
+ export class TableToggleMatrixExampleComponent {
301
+ readonly channels = CHANNELS;
302
+ readonly columns: readonly BfTableColumn[] = [
303
+ { key: 'label', label: 'Alert' },
304
+ ...CHANNELS.map((channel): BfTableColumn => ({ ...channel, align: 'center' })),
305
+ ];
306
+ readonly alerts = signal<readonly AlertSetting[]>([
307
+ { id: 'critical', label: 'Critical finding', email: true, push: true, slack: true },
308
+ { id: 'cve', label: 'New CVE', email: true, push: false, slack: true },
309
+ { id: 'ci', label: 'Failed CI run', email: false, push: false, slack: true },
310
+ { id: 'digest', label: 'Weekly digest', email: true, push: false, slack: false },
311
+ ]);
312
+ readonly status = signal('No change yet.');
313
+
314
+ /** Writes one cell back; a real app saves the setting right away. */
315
+ set(alert: AlertSetting, channel: Channel, on: boolean): void {
316
+ this.alerts.update((rows) =>
317
+ rows.map((row) => (row.id === alert.id ? { ...row, [channel]: on } : row)),
318
+ );
319
+ const name = CHANNELS.find((c) => c.key === channel)?.label ?? channel;
320
+ this.status.set(`${name} for ${alert.label} turned ${on ? 'on' : 'off'}.`);
321
+ }
322
+ }
323
+ ```
324
+
325
+ 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.
326
+
243
327
  ## Empty state
244
328
 
245
329
  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.
@@ -405,11 +489,12 @@ export class TablePagedExampleComponent {
405
489
 
406
490
  `stack` lets the table itself fit a narrow space. With `stack="narrow"` it stacks while the table is narrower than 36rem (576 px at the default root size, Tailwind's `xl` container size): a container query on the table's own width, so a table in a narrow column or a side panel stacks even on a wide screen. The host is then a size container (`container-type: inline-size`), so give the table its width from its parent (a block or `w-full` / `flex-1`), not from its columns. With `stack="coarse"` it stacks under a coarse pointer at any width (phones, tablets, the Capacitor shell). The default `never` keeps the table as it is.
407
491
 
408
- Stacked, each row is a sunken tile (`surface-sunken`, `rounded-tile`, the tile a raised card holds; a selected row keeps `primary-soft`). Its checkbox and first cell head the tile in bold; every other cell shows its column label at the start and its value at the end. The header row is visually hidden, and a bar above the tiles holds a labelled Select all checkbox (with `selectable`) and a Sort by select, 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`.
492
+ 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. The Sort by select is a native `<select>` unless the app adds `provideBfSelect()` from `@bitforgehq/ui/overlay` to its providers: then it is the styled [bf-select](https://bitforge-ui.web.app/components/select) (still native under touch), through the `BF_TABLE_SORT_SELECT` token, so `@bitforgehq/ui` itself never loads the CDK. In a test, `BfTableHarness.getSortSelect()` returns that select's `BfSelectHarness`, in either form.
409
493
 
410
494
  ```ts
411
495
  import { Component, computed, signal } from '@angular/core';
412
496
  import {
497
+ BF_TABLE_SORT_SELECT,
413
498
  BfBadgeComponent,
414
499
  BfSegmentedComponent,
415
500
  BfTableCellDirective,
@@ -418,6 +503,7 @@ import {
418
503
  type BfTableColumn,
419
504
  type BfTableSort,
420
505
  } from '@bitforgehq/ui';
506
+ import { BfSelectComponent } from '@bitforgehq/ui/overlay';
421
507
 
422
508
  interface Task {
423
509
  id: string;
@@ -442,10 +528,16 @@ type Width = 'phone' | 'full';
442
528
  * than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
443
529
  * holds Select all and a Sort by select, whose first option, Not sorted, shows while `sort` is
444
530
  * null and clears it. Switch the box between phone width and full width.
531
+ *
532
+ * The Sort by select is the styled `bf-select`. In an app, add `provideBfSelect()` from
533
+ * `@bitforgehq/ui/overlay` to `app.config.ts` once; this page provides `BF_TABLE_SORT_SELECT`
534
+ * on the example instead, so the docs site loads the select only with this example.
445
535
  */
446
536
  @Component({
447
537
  selector: 'app-example-table-stacked',
448
538
  imports: [BfTableComponent, BfTableCellDirective, BfBadgeComponent, BfSegmentedComponent],
539
+ // In an app: provideBfSelect() in app.config.ts (see the comment above).
540
+ providers: [{ provide: BF_TABLE_SORT_SELECT, useValue: BfSelectComponent }],
449
541
  template: `
450
542
  <!-- A stacking table is a size container: its width comes from its parent, never its columns. -->
451
543
  <div class="flex w-152 max-w-full flex-col gap-3">
@@ -586,7 +678,7 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
586
678
 
587
679
  ## Accessibility
588
680
 
589
- 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.
681
+ 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.
590
682
 
591
683
  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.
592
684
 
@@ -600,7 +692,7 @@ A table that can stack (`stack` other than `never`) sets explicit `table`, `rowg
600
692
  | <kbd>Space</kbd> | Toggles the focused checkbox, or sorts by the focused header |
601
693
  | <kbd>Enter</kbd> | Sorts by the focused header |
602
694
 
603
- A stacked table's Sort by select is a native select with its own keys.
695
+ A stacked table's Sort by select has its own keys: the native select's, or with `provideBfSelect()` those of [bf-select](https://bitforge-ui.web.app/components/select#keyboard).
604
696
 
605
697
  Text fields, selects and composite widgets (radios, comboboxes, sliders) in a cell keep their own arrow keys.
606
698
 
@@ -655,6 +747,7 @@ Selects or clears one row.
655
747
  ### Do
656
748
 
657
749
  - Give every table a `label`, and every sortable column a short header.
750
+ - 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.
658
751
  - Sort and filter the rows in a `computed()` from `sort` and your filters.
659
752
  - 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`.
660
753
 
@@ -666,6 +759,24 @@ Selects or clears one row.
666
759
 
667
760
  ## Migration notes
668
761
 
762
+ ### 0.5.0
763
+
764
+ New `BF_TABLE_SORT_SELECT` injection token: the component a stacked table renders as its "Sort
765
+ by" select (`null`, the default, keeps the native one). `provideBfSelect()` from
766
+ `@bitforgehq/ui/overlay` sets it to the styled `bf-select`, so the core entry still never imports
767
+ the CDK. `BfTableHarness.getSortSelect()` returns a working `BfSelectHarness` either way.
768
+
769
+ ### 0.4.0
770
+
771
+ New: toggle cells for a settings matrix (rows of alert kinds, columns of channels, a `bf-toggle`
772
+ in each cell), documented with the `table/toggle-matrix` example. `BfTableColumn.align` takes
773
+ `'center'`: the header and each cell's control are centred, and while stacked the control sits at
774
+ the tile's end, centred on its label line. New type `BfTableColumnAlign` (`BfTableAlign` plus
775
+ `'center'`; `BfTableAlign` keeps its two members). A stacked cell of a start or end column keeps
776
+ its baseline alignment. `@bitforgehq/ui/testing`: new `BfTableHarness.getCellHarness(row, column,
777
+ query)` returns the harness of a control in one cell, and `selectRow()`, `getSelectedCount()`
778
+ read only the selection column's checkboxes, never a checkbox or toggle in a custom cell.
779
+
669
780
  ### 0.3.3
670
781
 
671
782
  Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
@@ -127,6 +127,7 @@ The button consumes `bf-menu-button`'s tokens (`bf-button`'s, or `bf-icon-button
127
127
  - The three items are `role="menuitemradio"` with `aria-checked`, so a screen reader announces the current choice. The dot is drawn in CSS and adds nothing to the name.
128
128
  - Choosing an item closes the menu and returns focus to the button.
129
129
  - Hit area: at least 32 px, 44 px under a coarse pointer, as `bf-menu-button`.
130
+ - Motion: the menu fades and grows out of the button as it opens, and closes at once. Under `prefers-reduced-motion` it only fades in.
130
131
 
131
132
  ### Keyboard
132
133
 
@@ -138,6 +138,8 @@ The field shows the `primary` border and focus ring while any part has focus, th
138
138
 
139
139
  The parts are a `role="group"` named by `label` (else `ariaLabel` or `ariaLabelledby`); each part is a `role="spinbutton"` named from the `timeInputHour`, `timeInputMinute`, `timeInputSecond` and `timeInputDayPeriod` labels, with `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and `aria-valuetext` (the AM or PM name), and the field's `aria-invalid`, `aria-errormessage`, `aria-required`, `aria-readonly` and `aria-disabled`. The clock button is named by `timeInputOpenList`, with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. The list is a `role="listbox"` named by the same label, its times `role="option"` with `aria-selected` and `aria-disabled`; focus stays on the list and `aria-activedescendant` names the active time.
140
140
 
141
+ Motion: the list grows out of the field as it opens (a fade, a slight scale and a 4 px slide, 180 ms) and fades back as it closes (130 ms). Focus returns at once and the closing list is inert. Under `prefers-reduced-motion` it only fades.
142
+
141
143
  ### Keyboard
142
144
 
143
145
  | Key | Where | Action |
@@ -0,0 +1,144 @@
1
+ # bf-timeline-event
2
+
3
+ One timeline event with a coloured marker, its text, actor and time
4
+
5
+ `BfTimelineEventComponent` from `@bitforgehq/ui` · Data display · since 0.4.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { RouterLink } from '@angular/router';
12
+ import { BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
13
+
14
+ /**
15
+ * The bf-timeline-event overview.
16
+ *
17
+ * Live demo of `bf-timeline-event`: one event per tone in a `bf-timeline`, with and without an
18
+ * actor, one with a link in its text and one with a relative `timeText`.
19
+ */
20
+ @Component({
21
+ selector: 'app-example-timeline-event-overview',
22
+ imports: [RouterLink, BfTimelineComponent, BfTimelineEventComponent],
23
+ template: `
24
+ <bf-timeline label="Event tones" class="max-w-xl">
25
+ <bf-timeline-event actor="Lena Meier" time="2026-10-28T15:40:00Z" tone="primary">
26
+ Opened
27
+ <a routerLink="/components/timeline" fragment="links-in-events">CVE-2026-1234</a>
28
+ </bf-timeline-event>
29
+ <bf-timeline-event actor="CI" time="2026-10-28T15:10:00Z" tone="success">
30
+ All checks passed
31
+ </bf-timeline-event>
32
+ <bf-timeline-event actor="Dependency bot" time="2026-10-28T14:00:00Z" tone="warning">
33
+ Found an outdated base image
34
+ </bf-timeline-event>
35
+ <bf-timeline-event actor="CI" time="2026-10-28T13:05:00Z" tone="danger">
36
+ CI failed on the security scan
37
+ </bf-timeline-event>
38
+ <bf-timeline-event time="2026-10-28T12:00:00Z" timeText="4 hours ago" tone="info">
39
+ A new advisory was published
40
+ </bf-timeline-event>
41
+ <bf-timeline-event actor="Jonas Keller" time="2026-10-27T16:02:00Z">
42
+ Assigned the finding to Lena Meier
43
+ </bf-timeline-event>
44
+ </bf-timeline>
45
+ `,
46
+ })
47
+ export class TimelineEventOverviewExampleComponent {}
48
+ ```
49
+
50
+ ## Overview
51
+
52
+ One event of a `bf-timeline`: a marker coloured by `tone`, the event text (the default slot,
53
+ which may hold links), then who did it (`actor`) and when (`time`, in a `time` element).
54
+
55
+ 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.
56
+
57
+ ## When to use
58
+
59
+ 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.
60
+
61
+ ## Basic usage
62
+
63
+ `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.
64
+
65
+ ```ts
66
+ import { Component } from '@angular/core';
67
+ import { BfTimelineComponent, BfTimelineEventComponent } from '@bitforgehq/ui';
68
+
69
+ /**
70
+ * An event's actor, time and tone.
71
+ *
72
+ * Two events of a CVE side panel: one by a person with an exact time, one by a bot with a relative
73
+ * `timeText` whose `datetime` keeps the exact value.
74
+ */
75
+ @Component({
76
+ selector: 'app-example-timeline-event-basic',
77
+ imports: [BfTimelineComponent, BfTimelineEventComponent],
78
+ template: `
79
+ <bf-timeline label="CVE history" class="max-w-sm">
80
+ <bf-timeline-event actor="Lena Meier" time="2026-10-28T15:40:00Z" tone="warning">
81
+ Accepted the risk until the next release
82
+ </bf-timeline-event>
83
+ <bf-timeline-event
84
+ actor="Advisory bot"
85
+ time="2026-10-28T10:00:00Z"
86
+ timeText="6 hours ago"
87
+ tone="info"
88
+ >
89
+ Imported the advisory
90
+ </bf-timeline-event>
91
+ </bf-timeline>
92
+ `,
93
+ })
94
+ export class TimelineEventBasicExampleComponent {}
95
+ ```
96
+
97
+ ## Desktop
98
+
99
+ 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()`.
100
+
101
+ ## Theming
102
+
103
+ - Marker: `text-subtle`, `primary`, `success`, `warning`, `danger` or `info`; `CanvasText` under forced colours. Line: `border`.
104
+ - Text `text-body` in `text`, links `primary-text`; meta line `text-caption` in `text-muted`.
105
+
106
+ ## Accessibility
107
+
108
+ 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.
109
+
110
+ ## Inputs
111
+
112
+ | Name | Type | Default | Description |
113
+ | ---------- | ------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
114
+ | `actor` | `string` | `''` | Who did it, such as a person or a bot; empty for a system event. |
115
+ | `time` | `Date \| string \| null` | `null` | When it happened: a Date or an ISO 8601 string; empty or invalid shows no time. |
116
+ | `timeText` | `string` | `''` | Shown in place of the formatted time, such as "vor 2 Std."; the `time` element's `datetime` keeps the exact value. |
117
+ | `tone` | `BfTimelineTone` (`'neutral' \| 'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'`) | `'neutral'` | The marker's colour. |
118
+
119
+ ## Slots
120
+
121
+ - `default`: The event text; links in it take the link style.
122
+ - `end`: Content at the inline end on a wide timeline and below the text on a narrow one, such as a button.
123
+
124
+ ## Notes
125
+
126
+ ### Do
127
+
128
+ - Write the text as what happened ("Assigned to Lena Meier"), and link the object it names.
129
+
130
+ ### Don't
131
+
132
+ - Don't put more than one small action in the `end` slot; open the details elsewhere.
133
+
134
+ ## Migration notes
135
+
136
+ ### 0.4.0
137
+
138
+ New: `bf-timeline-event`, one event of a `bf-timeline`: a 10 px marker coloured by `tone`
139
+ (`neutral`, `primary`, `success`, `warning`, `danger`, `info`; new type `BfTimelineTone`) on a
140
+ `border` line that ends at the last event, the event text in the default slot (links in it take
141
+ the link style), then `actor` and `time` (a `Date` or an ISO 8601 string, in a `time` element;
142
+ `timeText` shows a relative text while `datetime` keeps the exact value; an invalid time shows
143
+ nothing). A `bfSlot="end"` action sits at the inline end, or under the text once the event is
144
+ narrow.