@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
@@ -748,6 +748,15 @@ The title is not in the tab order; after a push, `bf-stack-outlet` focuses it wi
748
748
 
749
749
  ## Migration notes
750
750
 
751
+ ### 0.3.3
752
+
753
+ Fix: `bf-header` measures its title and slots in the next animation frame after a resize, not
754
+ inside the `ResizeObserver` callback. Its measurement writes the title inset and height, which
755
+ resize the observed elements, and WebKit then reported "ResizeObserver loop completed with
756
+ undelivered notifications": an error event that an app's global error listener
757
+ (`provideBrowserGlobalErrorListeners()`) logs as an error. Several resizes in one frame now
758
+ measure once.
759
+
751
760
  ### 0.3.2
752
761
 
753
762
  The `## Desktop` docs say `bf-app-frame` supplies the app-level header bar below its breakpoint.
@@ -100,6 +100,7 @@ export class ListItemOverviewExampleComponent {
100
100
  One row of a list: leading slot (avatar, icon tile), title and subtitle, trailing slot
101
101
  (time, toggle, badge) and an optional chevron for rows that navigate. A navigation row puts a
102
102
  `bfAction` link in the `title` slot, so the row stays a list item and the link covers it.
103
+ `unread` marks a row not yet opened with a dot and a bolder title.
103
104
 
104
105
  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.
105
106
 
@@ -230,6 +231,87 @@ export class ListItemActionsExampleComponent {
230
231
  }
231
232
  ```
232
233
 
234
+ ## Unread rows
235
+
236
+ `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.
237
+
238
+ ```ts
239
+ import { Component, computed, signal } from '@angular/core';
240
+ import { BfActionDirective, BfListComponent, BfListItemComponent } from '@bitforgehq/ui';
241
+
242
+ interface Notice {
243
+ readonly id: string;
244
+ readonly title: string;
245
+ readonly subtitle: string;
246
+ readonly time: string;
247
+ readonly unread: boolean;
248
+ }
249
+
250
+ /**
251
+ * An inbox with unread rows.
252
+ *
253
+ * Each title is a stretched `<button bfAction>`; opening a row marks it read.
254
+ */
255
+ @Component({
256
+ selector: 'app-example-list-item-unread',
257
+ imports: [BfActionDirective, BfListComponent, BfListItemComponent],
258
+ template: `
259
+ <bf-list>
260
+ @for (notice of notices(); track notice.id) {
261
+ <bf-list-item [unread]="notice.unread" [subtitle]="notice.subtitle">
262
+ <button bfSlot="title" bfAction type="button" class="text-start" (click)="open(notice)">
263
+ {{ notice.title }}
264
+ </button>
265
+ <span bfSlot="trailing" class="text-caption text-text-muted">{{ notice.time }}</span>
266
+ </bf-list-item>
267
+ }
268
+ </bf-list>
269
+ <p data-status aria-live="polite" class="text-caption text-text-muted mt-3">{{ status() }}</p>
270
+ `,
271
+ })
272
+ export class ListItemUnreadExampleComponent {
273
+ protected readonly notices = signal<readonly Notice[]>([
274
+ {
275
+ id: 'scan',
276
+ title: 'Scan finished',
277
+ subtitle: '3 new findings on api.example.com',
278
+ time: '9:41',
279
+ unread: true,
280
+ },
281
+ {
282
+ id: 'cert',
283
+ title: 'Certificate expires soon',
284
+ subtitle: 'shop.example.com, in 12 days',
285
+ time: '8:15',
286
+ unread: true,
287
+ },
288
+ {
289
+ id: 'report',
290
+ title: 'Weekly report',
291
+ subtitle: 'Ready to download',
292
+ time: 'Mon',
293
+ unread: false,
294
+ },
295
+ ]);
296
+ private readonly opened = signal('');
297
+
298
+ protected readonly status = computed(() => {
299
+ const unread = this.notices().filter((notice) => notice.unread).length;
300
+ const opened = this.opened();
301
+ return opened ? `${unread} unread · Opened: ${opened}` : `${unread} unread`;
302
+ });
303
+
304
+ protected open(notice: Notice): void {
305
+ this.opened.set(notice.title);
306
+ this.notices.update((list) =>
307
+ list.map((item) => (item.id === notice.id ? { ...item, unread: false } : item)),
308
+ );
309
+ }
310
+ }
311
+ ```
312
+
313
+ 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.
314
+
233
315
  ## Desktop
234
316
 
235
317
  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()`.
@@ -240,10 +322,11 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
240
322
  - Text: the title, the title slot and the subtitle wrap (`wrap-anywhere`); with `truncate` each keeps one line (`truncate`).
241
323
  - `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.
242
324
  - The `chevron` mark uses `text-subtle`.
325
+ - `unread`: the dot is `primary` (`CanvasText` in forced colours), `rounded-full` and 8 px (`size-2`); the title turns `font-extrabold`.
243
326
 
244
327
  ## Accessibility
245
328
 
246
- `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.
329
+ `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`.
247
330
 
248
331
  ### Keyboard
249
332
 
@@ -255,14 +338,15 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
255
338
 
256
339
  ## Inputs
257
340
 
258
- | Name | Type | Default | Description |
259
- | ------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
260
- | `title` | `string` | `''` | Primary text. For a navigation row, project an `<a bfSlot="title" bfAction>` instead. |
261
- | `subtitle` | `string` | `''` | Secondary text under the title. |
262
- | `interactive` | `boolean` | `false` | Render the row as a button that emits `clicked` (an action row; a row that navigates puts a `bfAction` link in the `title` slot instead). |
263
- | `chevron` | `boolean` | `false` | Show a trailing chevron (navigation rows). |
264
- | `disabled` | `boolean` | `false` | Disable an interactive row. |
265
- | `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. |
341
+ | Name | Type | Default | Description |
342
+ | ------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
343
+ | `title` | `string` | `''` | Primary text. For a navigation row, project an `<a bfSlot="title" bfAction>` instead. |
344
+ | `subtitle` | `string` | `''` | Secondary text under the title. |
345
+ | `interactive` | `boolean` | `false` | Render the row as a button that emits `clicked` (an action row; a row that navigates puts a `bfAction` link in the `title` slot instead). |
346
+ | `chevron` | `boolean` | `false` | Show a trailing chevron (navigation rows). |
347
+ | `disabled` | `boolean` | `false` | Disable an interactive row. |
348
+ | `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. |
349
+ | `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. |
266
350
 
267
351
  ## Outputs
268
352
 
@@ -283,14 +367,26 @@ An `interactive` row and a row holding a `bfAction` link take a `surface-muted`
283
367
 
284
368
  - Keep titles short; a long one wraps. Add `truncate` only where one line per row is the pattern (a mail list).
285
369
  - 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.
370
+ - Set `unread` on rows not yet opened (messages, notifications) and clear it when the row is opened.
286
371
 
287
372
  ### Don't
288
373
 
289
374
  - Don't nest interactive controls inside an `interactive` row (it is one button); use a navigation row with a trailing control instead.
290
375
  - Don't wrap a `bf-list-item` in an `<a>`, and don't navigate from an `interactive` row's `(clicked)`.
376
+ - 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.
291
377
 
292
378
  ## Migration notes
293
379
 
380
+ ### 0.4.0
381
+
382
+ New `unread` input (boolean, default false) for a row not yet opened: a `primary` dot at the
383
+ row's inline start on the title line, a bolder title (`font-extrabold`) and a visually hidden
384
+ state read before the title, from the new label key `listItemUnread` ("Unread"). A row's
385
+ `bfAction` link or button then names itself `aria-labelledby="<state> <itself>"` ("Unread,
386
+ Report"; an id is added if it has none, and its own `aria-labelledby` is kept after the state).
387
+ The dot sits in the row's own inline-start padding, so read and unread rows stay aligned. Opt-in: without `unread` nothing
388
+ changes.
389
+
294
390
  ### 0.3.0
295
391
 
296
392
  **Behaviour:** logical properties: text aligns to the inline start and the chevron sits at the
@@ -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.
@@ -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,7 +489,7 @@ 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 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.
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. In a test, `BfTableHarness.getSortSelect()` returns that select's `BfSelectHarness`.
409
493
 
410
494
  ```ts
411
495
  import { Component, computed, signal } from '@angular/core';
@@ -440,7 +524,8 @@ type Width = 'phone' | 'full';
440
524
  *
441
525
  * `stack="narrow"` turns each row into a tile of label/value pairs while the table is narrower
442
526
  * than 36rem: the selection checkbox and the first cell head the tile, and a bar above the tiles
443
- * holds Select all and a Sort by select. Switch the box between phone width and full width.
527
+ * holds Select all and a Sort by select, whose first option, Not sorted, shows while `sort` is
528
+ * null and clears it. Switch the box between phone width and full width.
444
529
  */
445
530
  @Component({
446
531
  selector: 'app-example-table-stacked',
@@ -585,9 +670,9 @@ Put the table on a `bf-card` (or another surface) yourself; it has no background
585
670
 
586
671
  ## Accessibility
587
672
 
588
- A native `table` with a visually hidden `caption` from `label`, `th scope="col"` headers and one `tr` per row. A sorted column's `th` carries `aria-sort="ascending"` or `"descending"`; the others carry none. A sortable header is a native `button` inside the `th`, so its name is the column label; the arrow is `aria-hidden`. The header checkbox is named by `selectAllLabel`, else `provideBfLabels()`'s `tableSelectAll` ("Select all rows"), and is mixed (`indeterminate`) while some rows are selected. Each row checkbox is named by the row's first data cell through `aria-labelledby`, so it reads as the row's title. The empty row's cell spans every column.
673
+ 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.
589
674
 
590
- A table that can stack (`stack` other than `never`) sets explicit `table`, `rowgroup`, `row`, `columnheader` and `cell` roles and names the table from its caption with `aria-labelledby`, because Safari and some screen readers drop a table's semantics once CSS changes its display; so a stacked table still reads as a table with its column headers. Stacked, the header row is visually hidden but stays in the accessibility tree; its sort buttons and checkbox are hidden (a plain label takes a sortable header's place), so no control is focusable where nobody can see it, and the bar's Select all checkbox and Sort by select replace them. A cell's visible label is CSS generated content with empty alternative text (`content: attr(data-label) / ""`), so a screen reader reads the column header once, not the label as well.
675
+ 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.
591
676
 
592
677
  ### Keyboard
593
678
 
@@ -654,6 +739,7 @@ Selects or clears one row.
654
739
  ### Do
655
740
 
656
741
  - Give every table a `label`, and every sortable column a short header.
742
+ - 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.
657
743
  - Sort and filter the rows in a `computed()` from `sort` and your filters.
658
744
  - Use `stack="narrow"` for a table that may sit in a narrow column, or `stack="coarse"` in the Capacitor shell; otherwise render the rows as `bf-list-item`s below `md`.
659
745
 
@@ -665,6 +751,34 @@ Selects or clears one row.
665
751
 
666
752
  ## Migration notes
667
753
 
754
+ ### 0.4.0
755
+
756
+ New: toggle cells for a settings matrix (rows of alert kinds, columns of channels, a `bf-toggle`
757
+ in each cell), documented with the `table/toggle-matrix` example. `BfTableColumn.align` takes
758
+ `'center'`: the header and each cell's control are centred, and while stacked the control sits at
759
+ the tile's end, centred on its label line. New type `BfTableColumnAlign` (`BfTableAlign` plus
760
+ `'center'`; `BfTableAlign` keeps its two members). A stacked cell of a start or end column keeps
761
+ its baseline alignment. `@bitforgehq/ui/testing`: new `BfTableHarness.getCellHarness(row, column,
762
+ query)` returns the harness of a control in one cell, and `selectRow()`, `getSelectedCount()`
763
+ read only the selection column's checkboxes, never a checkbox or toggle in a custom cell.
764
+
765
+ ### 0.3.3
766
+
767
+ Fix: stacked rows (`stack="narrow"` and `stack="coarse"`) show their column labels again when the
768
+ package is installed from npm. 0.3.2 wrote the label as `content-[attr(data-label)_/_""]`; the
769
+ library build escaped its quotes in the FESM (`\"\"`), so Tailwind built the rule for a class no
770
+ cell carries and every label `::before` computed to `none`. The cells now carry
771
+ `before:content-data-label`, a utility `theme.css` defines (below). Apps that added the
772
+ workaround `@source inline('@max-xl:before:content-[attr(data-label)_/_""] pointer-coarse:before:content-[attr(data-label)_/_""]')`
773
+ can remove it. consumer-smoke now opens a stacked table built from the packed package in
774
+ Chromium, under a fine and a coarse pointer, and fails if any FESM class string holds a
775
+ backslash escape.
776
+
777
+ The stacked Sort by select's first option is Not sorted (new label key `tableSortNone`, "Not
778
+ sorted"): it shows while `sort` is null, so the select is no longer an empty box, and choosing it
779
+ sets `sort` back to null. New `BfTableHarness.getSortSelect()` returns that select's
780
+ `BfSelectHarness` (`null` when the table cannot stack or has no sortable column).
781
+
668
782
  ### 0.3.2
669
783
 
670
784
  New opt-in `stack` input (`BfTableStack`: `'never'` by default, so existing tables do not