@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,400 @@ All notable changes to `@bitforgehq/ui`. One `##` section per version; one `###`
4
4
 
5
5
  The heading for the version being worked on carries `(unreleased)` in place of a date. At release time, that placeholder is replaced with the release date (`## 0.3.1 (unreleased)` becomes `## 0.3.1 (YYYY-MM-DD)`) in the same commit that bumps `projects/ui/package.json`. See "Releasing" in the root `README.md`. 0.3.0 is the first version published to npm; the 0.2.0 and 0.1.0 sections below are the history of versions that were never published.
6
6
 
7
+ ## 0.5.0 (unreleased)
8
+
9
+ ### Migration
10
+
11
+ Breaking changes from 0.4.x, each with its fix:
12
+
13
+ 1. `bf-select` moved to `@bitforgehq/ui/overlay`. Before:
14
+ `import { BfSelectComponent } from '@bitforgehq/ui'`. After:
15
+ `import { BfSelectComponent } from '@bitforgehq/ui/overlay'`, which needs `@angular/cdk` and
16
+ `rxjs` (`bun add @angular/cdk rxjs`). The selector, inputs, outputs and Signal Forms binding
17
+ are unchanged; `BfSelectOption` is still exported from `@bitforgehq/ui` (and now from
18
+ `/overlay` too).
19
+ 2. Under a mouse `bf-select` is no longer a native `<select>`. `checkValidity()` and the
20
+ browser's "required" popup do not apply in the styled mode; enforce `required` through Signal
21
+ Forms. Tests that set a `<select>`'s value directly pick by label with `BfSelectHarness`
22
+ instead, which drives both modes.
23
+ 3. A stacked `bf-table`'s "Sort by" control stays the native select unless the app adds
24
+ `provideBfSelect()` from `@bitforgehq/ui/overlay` to its providers. Without it, that
25
+ control's host element changed from `bf-select` to `bf-native-select[data-bf-select]`: update
26
+ app CSS and DOM queries such as `bf-table bf-select`.
27
+ 4. A collapsed `bf-sidebar` is a fixed rail (56, 64 or 72 px for `controlSize` `sm`, `md` and
28
+ `lg`; 68 px under a coarse pointer for `sm` and `md`) instead of `w-auto`, so its width can
29
+ animate. Its regions no longer centre their content while collapsed. Before: header or footer
30
+ content wider than the icons widened the rail, and narrower content was centred. After: the
31
+ rail keeps its width and clips wider content, and content is aligned to the inline start. Fix:
32
+ make collapsed header and footer content fit the rail, using `in-data-collapsed:` to show a
33
+ compact brand and to fade or close the rest (see the sidebar's Motion section), and put
34
+ stacked icon buttons in a `flex flex-col items-start` column.
35
+
36
+ ### file-input
37
+
38
+ New: `bf-file-input`, a file picker field. The `field` look (the default) is a tonal "Choose
39
+ file" button inside a field box that also takes dropped files; `variant="dropzone"` is a dashed
40
+ area that is the control as a whole (click, Enter, Space or a drop). The chosen files are listed
41
+ under it with a thumbnail (images) or the extension, the name, the size and a remove button. The
42
+ value is a `File[]` (one file at most without `multiple`), bound with `[(value)]` or
43
+ `[formField]`. `accept` (MIME types, `type/*` and `.ext`, checked on drops too), `maxSize`
44
+ (bytes) and `maxFiles` decide what is added; a file that breaks one never enters the value and
45
+ never sets `invalid`: it shows as a red row with the reason until the next pick and is emitted
46
+ through `rejected` (new types `BfRejectedFile`: `file`, `reason`, `message`; and
47
+ `BfRejectedFileReason`: `'type' | 'size' | 'count'`). It only picks; uploading stays with the app. New type `BfFileInputVariant`, new label
48
+ keys `fileInputChoose`, `fileInputDropHint`, `fileInputDropzone`, `fileInputTapToChoose`,
49
+ `fileInputRemove`, `fileInputDismiss`, `fileInputAdded`, `fileInputNotAdded`,
50
+ `fileInputRemoved`, `fileInputCount`, `fileRejectedType`, `fileRejectedSize` and
51
+ `fileRejectedCount`. `@bitforgehq/ui/testing`: new `BfFileInputHarness` (`addFiles()` drops files
52
+ through the same rules, `getFileNames()`, `getRejected()`, `removeFile()`, `getVariant()`).
53
+
54
+ ### image-input
55
+
56
+ New: `bf-image-input`, a single-image picker for an avatar, logo or cover: a `round` or `square`
57
+ tile (`shape`, new type `BfImageInputShape`; 64, 96 or 128 px with `size`) that shows the image or
58
+ a camera glyph, with Upload/Replace and Remove beside it. The value is a `File | null`. `src`, a
59
+ two-way `model()`, shows the image the app already has; Remove clears both `value` and `src`, so
60
+ the app can tell a new picture (`value` set), a removed one (both null) and an unchanged one
61
+ (`src` kept, `value` null) apart. `accept` (default `image/*`) and `maxSize` refuse an image
62
+ without touching the current one: the reason shows under the buttons and `rejected` emits it. It
63
+ only picks; uploading stays with the app. New label keys `imageInputChoose`, `imageInputUpload`,
64
+ `imageInputReplace` and `imageInputRemove`. `@bitforgehq/ui/testing`: new `BfImageInputHarness`
65
+ (`addFile()`, `hasPreview()`, `getPreviewSrc()`, `remove()`, `getRejectedMessage()`).
66
+
67
+ ### select
68
+
69
+ **Breaking:** `bf-select` moved from `@bitforgehq/ui` to `@bitforgehq/ui/overlay` (see
70
+ Migration). Under a fine pointer it now opens a styled list, the combobox's panel without the
71
+ search, exactly as wide as the field: a focusable `role="combobox"` field following the APG
72
+ select-only combobox pattern (Enter, Space, the arrows and Alt+ArrowDown open; the arrows,
73
+ Home/End and PageUp/PageDown move; Enter, Space, Tab or a click pick; Escape closes without
74
+ closing a dialog underneath). Typeahead (500 ms buffer, case and accents ignored) moves the active
75
+ option, open or closed, and never commits on its own. Under a coarse pointer
76
+ (`matchMedia('(pointer: coarse)')`, followed live) it renders the native `<select>` as before;
77
+ on the server it renders the styled field. With `name` set, a hidden input carries the chosen
78
+ option's key, so plain form posts keep working (none while disabled). With a visible `label` the
79
+ field is named by `aria-labelledby` pointing at the label (a `<label for>` cannot name a div);
80
+ `ariaLabel` and `ariaLabelledby` still apply only without one. Under forced colours a disabled
81
+ field shows `GrayText`. `@bitforgehq/ui/testing`: `BfSelectHarness` drives both modes by label,
82
+ and has a new `isOpen()`; in both modes `getOptionLabels()` reads a read-only or disabled
83
+ select's options, and `selectOption()` on a read-only select changes nothing.
84
+
85
+ The styled list animates open and closed with the popover's motion (180 ms in, 130 ms out). The
86
+ field's `aria-expanded` and focus change at once; the closing list is inert and keeps the options
87
+ it showed until it is gone. Under `prefers-reduced-motion` it only fades. The native select under
88
+ touch is unchanged.
89
+
90
+ ### field
91
+
92
+ New `labelId`: the label's id (`<controlId>-label`), which the label now carries, for a control
93
+ that names itself with `aria-labelledby`.
94
+
95
+ The error message fades in when it appears (new `--animate-message-in`, 150 ms of opacity),
96
+ including a message held back during a press, which fades in once the press ends. `bf-toggle` and
97
+ `bf-calendar` messages do the same. It is an opacity fade only, so it stays under reduced motion.
98
+
99
+ ### overlay core
100
+
101
+ Anchored panels (combobox, select, popover, tooltip, date and time pickers), dialogs, side panels
102
+ and desktop toasts build their CDK overlays with the CDK's standalone factories
103
+ (`createOverlayRef()` and the `create*Strategy()` functions) instead of the `Overlay` service, so
104
+ an app's bundle no longer carries the scroll and position strategies it never uses.
105
+
106
+ ### combobox
107
+
108
+ The list panel is exactly as wide as the field (it shrank to its content inside the CDK pane).
109
+
110
+ The list of `bf-combobox` and `bf-multi-combobox` animates open and closed with the popover's
111
+ motion (180 ms in, 130 ms out). `aria-expanded` changes at once; the closing list is inert and
112
+ keeps the options it showed, so a reset query does not reflow it while it fades, and typing or
113
+ `ArrowDown` during the exit opens a fresh list. Under `prefers-reduced-motion` it only fades.
114
+
115
+ ### multi-combobox
116
+
117
+ The list panel is exactly as wide as the field. The inner text input no longer shows a square
118
+ border and ring on focus in apps that load `@tailwindcss/forms`.
119
+
120
+ ### table
121
+
122
+ New `BF_TABLE_SORT_SELECT` injection token: the component a stacked table renders as its "Sort
123
+ by" select (`null`, the default, keeps the native one). `provideBfSelect()` from
124
+ `@bitforgehq/ui/overlay` sets it to the styled `bf-select`, so the core entry still never imports
125
+ the CDK. `BfTableHarness.getSortSelect()` returns a working `BfSelectHarness` either way.
126
+
127
+ ### theme
128
+
129
+ New motion token `--bf-duration-layout` (200 ms) in `:root`, beside `--bf-duration-fast` and
130
+ `--bf-duration-overlay`: the timing of size and position changes (the sidebar's width, sliding
131
+ selection indicators). Use it with `ease-md`, as in
132
+ `transition-[width] duration-(--bf-duration-layout) ease-md motion-reduce:transition-none`.
133
+
134
+ ### sidebar
135
+
136
+ Collapsing and expanding now animate: the width moves over `--bf-duration-layout` on `ease-md`
137
+ from `w-60` to a fixed rail (see Migration 4), and the content beside it (the `bf-app-frame`
138
+ column) moves with it. Under `prefers-reduced-motion: reduce` it still switches at once. The first
139
+ render never animates, so a collapsed state restored by `provideBfSidebarState()` does not slide
140
+ on load. The host carries `data-collapsed` as before; header and footer content can follow on the
141
+ same timing with `in-data-collapsed:` (new Motion section in the docs).
142
+
143
+ ### nav-item
144
+
145
+ In a collapsing sidebar the item no longer switches to a centred square: the row keeps its layout
146
+ and its start padding moves so the icon ends centred in the rail without moving, while the label
147
+ fades out (it stays the accessible name; it is no longer `sr-only`). The count pill shrinks away
148
+ while the dot (new element, `[data-badge-dot]`) grows in on the same timing. Each item clips
149
+ what does not fit the rail. No animation under reduced motion or in a top nav.
150
+
151
+ ### nav-group
152
+
153
+ Collapsed, the heading's row closes and its text fades out on `--bf-duration-layout` instead of
154
+ switching to `sr-only`; it stays in the accessibility tree and still names the group. The heading
155
+ stays on one line and truncates (it wrapped before), so its height does not change while the
156
+ width moves.
157
+
158
+ ### checkbox
159
+
160
+ The check mark and the dash grow in as they appear and shrink as they go (150 ms on `ease-md`),
161
+ instead of switching opacity at once; under reduced motion they only fade. An error message fades
162
+ in when it appears.
163
+
164
+ ### radio-group
165
+
166
+ The selected option's dot grows in and the previous one shrinks away (150 ms on `ease-md`);
167
+ under reduced motion they only fade. An error message fades in when it appears.
168
+
169
+ ### popover
170
+
171
+ The panel animates open and closed. It grows out of the trigger (a fade, a scale from 96 % and a
172
+ 4 px slide away from the trigger, 180 ms, ease-out; transform origin on the trigger's corner,
173
+ following a flip above or below) and shrinks back on close (130 ms). Focus, `aria-expanded` and
174
+ `bfPopoverClosed` still change at once; the closing panel stays in the overlay container, inert
175
+ and click-through, until its exit has played, and opening again during the exit replaces it. Under
176
+ `prefers-reduced-motion` it only fades. Where the Web Animations API is missing or stubbed
177
+ (jsdom), the panel is still removed at once.
178
+
179
+ ### menu
180
+
181
+ The menu animates open: it grows out of its trigger (a fade, a scale from 96 % and a 4 px slide,
182
+ 180 ms, ease-out), a submenu out of its item. It still closes at once, because the CDK menu
183
+ removes it immediately. Under `prefers-reduced-motion` it only fades in. `bf-menu-button` and
184
+ `bf-theme-menu-button` get the same enter.
185
+
186
+ ### date-input
187
+
188
+ The calendar popover of `bf-date-input`, the time list of `bf-time-input` and the popover of
189
+ `bf-datetime-input` animate open and closed with the popover's motion (180 ms in, 130 ms out).
190
+ Focus returns to the button at once; the closing panel is inert until its exit has played. Under
191
+ `prefers-reduced-motion` they only fade.
192
+
193
+ ### motion
194
+
195
+ New duration utilities in the theme: `duration-fast` (150 ms), `duration-base` (200 ms) and
196
+ `duration-slow` (260 ms), the timings the widgets use (`--transition-duration-*` reading
197
+ `--bf-duration-fast`, `--bf-duration-layout` and `--bf-duration-overlay`, so redefining those
198
+ retimes both). New keyframe utilities for app lists with Angular's `animate.enter` and
199
+ `animate.leave`: `animate-item-enter` (fades and rises in, 200 ms on `ease-settle`) and
200
+ `animate-item-leave` (fades out, 150 ms), used behind `motion-safe:`. New `bfMotionList`
201
+ directive (`BfMotionListDirective`): on a list or grid container it glides items to their new
202
+ place when items are added, removed or reordered (FLIP, 200 ms, none under reduced motion or on a
203
+ resize). New `img[bfFadeIn]` directive (`BfFadeInDirective`): the image stays transparent until it
204
+ loads or fails, then fades in over 260 ms; at once from the cache and under reduced motion. The
205
+ Theming guide has a new Motion section with a live example.
206
+
207
+ ### tab-group
208
+
209
+ The selected tab's underline is now one indicator element of the row that slides to the newly
210
+ selected tab, changing width as it goes (`--bf-duration-layout`, `ease-md`). The first placement
211
+ and a resize land at once, and so does everything under `prefers-reduced-motion`. It is measured
212
+ from the selected tab's inline-start offset, so it mirrors under `dir="rtl"`, and is re-measured
213
+ on resize and when labels, badges, size or `fitted` change. Until it has been measured (jsdom,
214
+ server render, a hidden group) the tab keeps drawing its own underline, so unit tests and the
215
+ first paint are unchanged. The indicator is `aria-hidden`; roles, keyboard and focus are
216
+ unchanged.
217
+
218
+ The panel a user switches to fades in (200 ms of opacity, new `--animate-panel-in`, behind
219
+ `motion-safe:`); the panel shown on the first render never does.
220
+
221
+ ### segmented
222
+
223
+ The selected segment's pill (surface fill, `shadow-segment`, `primary-strong` outline,
224
+ `Highlight` under forced colours) is now one indicator element of the track that slides to the
225
+ newly chosen segment, changing width as it goes (`--bf-duration-layout`, `ease-md`). The first
226
+ placement and a resize land at once, and so does everything under `prefers-reduced-motion`. The
227
+ selected segment's hover and pressed fills move with it. Until it has been measured (jsdom,
228
+ server render, a hidden control) the segment keeps painting its own pill. `bf-theme-switch`
229
+ slides the same way. The indicator is `aria-hidden`; roles, keyboard and focus are unchanged.
230
+
231
+ ### chat-thread
232
+
233
+ New: `bf-chat-thread` and `bf-chat-message`, the message list of a chat with an assistant. The
234
+ thread is a scrolling `role="log"` (`aria-live="polite"`, named by `label` or the new
235
+ `chatThread` label) that stays pinned to the newest message while text arrives. When the user
236
+ scrolls up it stops following and shows a "Jump to latest" button (new label
237
+ `chatJumpToLatest`, optional `jump-icon` slot), which scrolls back down (smoothly, at once under
238
+ reduced motion) and follows again. While a message streams the log is `aria-busy`, so the reply
239
+ is announced once, when it is complete. Messages added after the first render rise 4 px and fade
240
+ in (new `--animate-chat-message-in`, 200 ms on `--ease-settle`, off under reduced motion). A
241
+ message's `role` (new type `BfChatMessageRole`: `'user' | 'assistant'`) draws a `primary-soft`
242
+ bubble at the inline end or a full-width reply; `author`, `time` and `timeText` fill an optional
243
+ header; `streaming` shows a pulsing caret at the end of the text (new `--animate-chat-caret`,
244
+ static under reduced motion); `status` (new type `BfChatMessageStatus`:
245
+ `'done' | 'stopped' | 'failed'`) adds a status line (new labels `chatMessageStopped`,
246
+ `chatMessageFailed`, or `statusMessage`) with an `action` slot for a Retry button; the `content`
247
+ slot holds cards, images or chips under the text. New directive `bfProse` (`BfProseDirective`)
248
+ styles the app's rendered markdown (headings, lists, links, code, quotes) with the semantic tokens;
249
+ the message body uses it.
250
+
251
+ ### chat-composer
252
+
253
+ New: `bf-chat-composer` in `@bitforgehq/ui/overlay`, the input row of a chat. A `bf-textarea`
254
+ with `autoResize` grows from one line to `maxRows` (8), then scrolls. `Enter` sends and
255
+ `Shift+Enter` starts a new line; nothing sends during IME composition or with whitespace-only
256
+ text. `value` is a `model()` that clears after a send; `sent` emits the trimmed text, `changed`
257
+ each edit. While `running` the send button is a stop button that emits `stopped`. Icons go in the
258
+ `send-icon` and `stop-icon` slots (the text stays as the button's name, and shows without an
259
+ icon); `start` and `end` slots take extra controls. `disabled` with `disabledReason` shows the
260
+ reason as the button's tooltip (`aria-disabled`, still focusable) and the empty field's
261
+ placeholder. `size` follows `fieldSize`. New labels `chatComposer`, `chatSend` and `chatStop`.
262
+ `@bitforgehq/ui/testing`: new `BfChatComposerHarness` (`setValue()`, `getValue()`, `send()`,
263
+ `pressEnter()`, `stop()`, `isRunning()`, `isDisabled()`, `canAct()`, `getButtonLabel()`).
264
+
265
+ ### media-card
266
+
267
+ New: `bf-media-card`, a card led by a picture (a game pitch, a generated image). The card renders
268
+ the picture itself from `src` and `alt` in a top area at a fixed `aspect` (new type
269
+ `BfMediaCardAspect`: `'16/9'` (default), `'4/3'`, `'1/1'`, `'3/4'`). While it loads, and while
270
+ `src` is empty but `loading` is set (a picture still being made), the area shows a `bf-skeleton`
271
+ shimmer with an optional `loadingLabel` caption ("Drawing…") and is `aria-busy`. The picture fades
272
+ in over 260 ms (`bfFadeIn`) while the skeleton fades out under it; at once under reduced motion.
273
+ A load error, or `failed` set by the app, shows `failedLabel` (default: the new label
274
+ `imageLoadFailed`) and the `bfSlot="failed-action"` content (Retry); a new `src` starts over.
275
+ `loaded` and `errored` report the image's events. A body slot and a `footer` slot follow. A
276
+ `bfAction` link or button in the title makes the whole card clickable, as on `bf-card` (pointer,
277
+ `shadow-floating` hover, focus ring, fading on `duration-fast`). `selectable` adds a toggle
278
+ button (`aria-pressed`) bound with `[(selected)]` (a `model()`), emitting `changed` on each user
279
+ change: the whole card without a `bfAction`, else a round check button in the image's corner; a
280
+ selected card has a 2 px primary ring and a filled check. `label` names it ("Select" + `label`,
281
+ new label `mediaCardSelect`). `@bitforgehq/ui/testing`: new `BfMediaCardHarness`
282
+ (`getState()`, `loadImage()`, `failImage()`, `getLoadingLabel()`, `getFailedMessage()`,
283
+ `isSelected()`, `toggleSelected()`).
284
+
285
+ ### image-viewer
286
+
287
+ New: `BfImageViewerService` in `@bitforgehq/ui/overlay`, a full-screen image viewer (lightbox).
288
+ `open({ images: [{ src, label, caption? }], index?, origin?, actions?, ariaLabel?,
289
+ closeOnNavigation?, injector? })` (new types `BfImageViewerOptions`, `BfImageViewerImage`,
290
+ `BfImageViewerOrigin`, `BfImageViewerActionsContext`) renders `bf-image-viewer` over a dark scrim
291
+ and returns a `BfImageViewerRef` (`index()`, `next()`, `previous()`, `goTo()`, `close()`,
292
+ `afterClosed`, `closed()`). Previous and next buttons, the arrow keys (mirrored under rtl), Home
293
+ and End step through the set, wrapping at both ends; a counter shows "2 of 5" and a polite live
294
+ region announces each step. It is a modal dialog: focus moves to its close button and is trapped,
295
+ `Escape`, the close button and a click on the scrim close it, the page behind is `inert`, and
296
+ focus returns to the opener. The `actions` template (context `{ $implicit: image, index, ref }`)
297
+ sits beside the caption. It zooms out of `origin` (the clicked thumbnail, or a function returning
298
+ any image's thumbnail) with a 260 ms FLIP on the overlay curve and zooms back into it on close
299
+ when it is on screen, else fades; images cross-fade on a step; reduced motion only fades. While a
300
+ large image loads it shows the thumbnail and a spinner, then fades the image in. New labels
301
+ `imageViewer`, `imageViewerPrevious`, `imageViewerNext` and `imageViewerPosition`.
302
+ `@bitforgehq/ui/testing`: new `BfImageViewerHarness` (`getIndex()`, `getCount()`,
303
+ `getCounterText()`, `getSrc()`, `getAlt()`, `getCaption()`, `next()`, `previous()`,
304
+ `pressKey()`, `close()`, `loadImage()`, `failImage()`, `isLoaded()`, `hasFailed()`).
305
+
306
+ ## 0.5.0-next.3 (2026-11-07)
307
+
308
+ Pre-release of 0.5.0: its changes are listed under "0.5.0 (unreleased)" above.
309
+
310
+ ## 0.4.0 (2026-10-29)
311
+
312
+ ### timeline
313
+
314
+ New: `bf-timeline`, a dated history of events (who did what, and when) for a record's history
315
+ or an audit trail, in both shells. A `role="list"` named by `label`, in the order the consumer
316
+ gives (it never sorts); each event's time is formatted with `Intl.DateTimeFormat` in the app's
317
+ `LOCALE_ID` (`timeFormat`, default a medium date and a short time; `locale` overrides it).
318
+
319
+ ### timeline-event
320
+
321
+ New: `bf-timeline-event`, one event of a `bf-timeline`: a 10 px marker coloured by `tone`
322
+ (`neutral`, `primary`, `success`, `warning`, `danger`, `info`; new type `BfTimelineTone`) on a
323
+ `border` line that ends at the last event, the event text in the default slot (links in it take
324
+ the link style), then `actor` and `time` (a `Date` or an ISO 8601 string, in a `time` element;
325
+ `timeText` shows a relative text while `datetime` keeps the exact value; an invalid time shows
326
+ nothing). A `bfSlot="end"` action sits at the inline end, or under the text once the event is
327
+ narrow.
328
+
329
+ ### table
330
+
331
+ New: toggle cells for a settings matrix (rows of alert kinds, columns of channels, a `bf-toggle`
332
+ in each cell), documented with the `table/toggle-matrix` example. `BfTableColumn.align` takes
333
+ `'center'`: the header and each cell's control are centred, and while stacked the control sits at
334
+ the tile's end, centred on its label line. New type `BfTableColumnAlign` (`BfTableAlign` plus
335
+ `'center'`; `BfTableAlign` keeps its two members). A stacked cell of a start or end column keeps
336
+ its baseline alignment. `@bitforgehq/ui/testing`: new `BfTableHarness.getCellHarness(row, column,
337
+ query)` returns the harness of a control in one cell, and `selectRow()`, `getSelectedCount()`
338
+ read only the selection column's checkboxes, never a checkbox or toggle in a custom cell.
339
+
340
+ ### toggle
341
+
342
+ A label-less `sm` toggle (a table cell) takes a 44 px min-width under a coarse pointer, its track
343
+ centred, so its touch target is 44 × 44 px (it was 40 px wide). Nothing changes under a fine
344
+ pointer or for `md` and `lg`. `@bitforgehq/ui/testing`: new `BfToggleHarness` (`with({ label })`
345
+ by the switch's computed name, `getLabel()`, `isChecked()`, `isDisabled()`, `isReadonly()`,
346
+ `toggle()`, `check()`, `uncheck()`, `getErrorText()`).
347
+
348
+ ### badge
349
+
350
+ New `emphasis` input (`BfBadgeEmphasis`: `soft`, the default, or `strong`). `strong` is a solid
351
+ fill with contrasting text in every variant, for the one state that must stand out ("Critical");
352
+ `primary` uses the brand fill. A strong count pill takes its fill from `variant` (a soft count pill
353
+ stays primary, as before). In forced colours a strong badge keeps a `CanvasText` outline. Opt-in:
354
+ without `emphasis` nothing changes.
355
+
356
+ ### theme
357
+
358
+ New strong fill tokens in both modes, for the strong badge: `--color-neutral-strong`,
359
+ `--color-success-strong`, `--color-warning-strong`, `--color-danger-strong` and
360
+ `--color-info-strong`, each with its text token `--color-on-<tone>-strong` (`bg-danger-strong`,
361
+ `text-on-danger-strong`, …). Every pair reaches 4.5:1 and every fill 3:1 on `bg`, `surface` and
362
+ `surface-sunken` in both modes; danger is `#b3261e` / `#ffffff` in light and `#c9372c` /
363
+ `#ffffff` in dark.
364
+
365
+ New: Ink, an opt-in dark preset, `@bitforgehq/ui/dark-ink.css`. Import it after `theme.css`; it
366
+ redefines tokens under `.dark` only (light mode is untouched): near-black surfaces one small step
367
+ apart (`bg` `#0b0c0e`, `surface` `#121316`), see-through status tints (the tone at 12 %, so they
368
+ sit on any surface; `danger-soft` stays opaque because the danger button writes it as text on
369
+ the danger fill), off-white text, a neutral focus ring and a 1 px edge inside `shadow-raised` and
370
+ `shadow-floating`. It sets every colour and shadow token of the default `.dark` block except the
371
+ brand set, so the app's primary stays; the Theming guide has an optional monochrome primary block.
372
+ Every pair is contrast-checked on `bg`, `surface`, `surface-muted` and `surface-sunken`. Four
373
+ mockup values were lifted to pass: `border-strong`, `control-off` and `chart-axis` `#64676e`
374
+ (3:1 on `surface-muted`), `text-muted` `#90949b` (4.5:1 on `primary-soft`) and `text-subtle`
375
+ `#63676f` (3:1 on `surface-muted`). The default dark palette does not change.
376
+
377
+ ### list-item
378
+
379
+ New `unread` input (boolean, default false) for a row not yet opened: a `primary` dot at the
380
+ row's inline start on the title line, a bolder title (`font-extrabold`) and a visually hidden
381
+ state read before the title, from the new label key `listItemUnread` ("Unread"). A row's
382
+ `bfAction` link or button then names itself `aria-labelledby="<state> <itself>"` ("Unread,
383
+ Report"; an id is added if it has none, and its own `aria-labelledby` is kept after the state).
384
+ The dot sits in the row's own inline-start padding, so read and unread rows stay aligned. Opt-in: without `unread` nothing
385
+ changes.
386
+
387
+ ### steps
388
+
389
+ New `bf-steps`: numbered instructions a user follows in order ("Set up the app on your phone"),
390
+ a `role="list"` of `bf-step` items whose markers are joined by a line. `start` (a number, 1 by
391
+ default) continues a list; a continued list sets `aria-posinset` and `aria-setsize` on its steps
392
+ so the announced position matches the visible number.
393
+
394
+ ### step
395
+
396
+ New `bf-step`: one step of a `bf-steps` list, with `title` (the short instruction, text by
397
+ default; `headingLevel` 2, 3 or 4, a `BfStepHeadingLevel`, makes it a heading) and the details
398
+ in the default slot. Its number is drawn in a `primary-soft` circle and hidden from assistive
399
+ technology, which reads the list position instead.
400
+
7
401
  ## 0.3.3 (2026-10-29)
8
402
 
9
403
  ### header
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Bitforge's shared Angular component library: Meadow Sage design, Tailwind CSS v4, signals-first, Angular 22.
4
4
 
5
- The theme is the Meadow Sage palette (warm off-white canvas, white surfaces, sage-tinted neutrals and status colours, pill controls). Its default primary is a neutral indigo (`#2b26ba`), not the design's sage green: it is meant to be overridden with your brand colour (`--color-primary` and its set; the docs site's theme configurator derives them), so an indigo primary after install is expected, not a broken install. `bunx bf-ui theme '#238652' --dark '#5cc48a'` prints the whole set for a brand colour, as a `@theme` and a `.dark` block to paste after the theme import, with every contrast pair's ratio, and exits 1 naming the failing pairs and a shade that passes (the docs site's Theming page has the recipe and a Meadow Sage green worked example).
5
+ The theme is the Meadow Sage palette (warm off-white canvas, white surfaces, sage-tinted neutrals and status colours, pill controls). Its default primary is a neutral indigo (`#2b26ba`), not the design's sage green: it is meant to be overridden with your brand colour (`--color-primary` and its set; the docs site's theme configurator derives them), so an indigo primary after install is expected, not a broken install. `bunx bf-ui theme '#238652' --dark '#5cc48a'` prints the whole set for a brand colour, as a `@theme` and a `.dark` block to paste after the theme import, with every contrast pair's ratio, and exits 1 naming the failing pairs and a shade that passes (the docs site's Theming page has the recipe and a Meadow Sage green worked example). For a near-black dark mode, `@bitforgehq/ui/dark-ink.css` (the opt-in Ink preset, imported right after the theme) redefines the dark tokens and leaves light mode and your brand as they are.
6
6
 
7
7
  - Widgets: `@bitforgehq/ui`
8
8
  - Number field: `@bitforgehq/ui/number-input` (`bf-number-input`; loads Angular Forms, peer
@@ -83,7 +83,7 @@ In `src/index.html`, give `<body>` the theme background and text colour, so the
83
83
 
84
84
  ### 7. Switch to dark mode
85
85
 
86
- 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.
86
+ 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.
87
87
 
88
88
  ```html
89
89
  <html lang="en" class="dark">
@@ -140,17 +140,17 @@ Render library components only in the browser, for example in a `@defer` block o
140
140
  route. Support will be decided before 1.0.
141
141
 
142
142
  The one server-side check is a render smoke, not a support test: the library repository's
143
- `tools/ssr-smoke.ts` renders 43 fixtures with `renderApplication()` (every root widget, `bf-icon`,
144
- `bf-number-input`, a `/charts` line chart, a `/mobile` page with `provideBfShell()`, `bf-page`,
145
- `bf-header` and `bf-content`, `bf-tabs` with `bf-stack-outlet` under the router, and an `/overlay`
146
- dialog), and all of them render without an error. 27 of them render identically twice. The other 16
147
- number their ids differently on every render, from a module-level counter, so they could not
148
- hydrate: `bf-app-frame`, `bf-calendar`, `bf-checkbox`, `bf-input`, `bf-radio-group`, `bf-segmented`,
149
- `bf-select`, `bf-sidebar`, `bf-tab-group`, `bf-table`, `bf-textarea`, `bf-theme-switch`,
150
- `bf-toggle`, `bf-line-chart`, `bf-number-input` and the `/overlay` dialog. Hydration itself is still
151
- not supported or tested, and more stands in its way: `/mobile` writes to `<html>` and adds a
152
- keyboard probe to `<body>`, and an `/overlay` dialog renders outside the app root and marks it
153
- inert.
143
+ `tools/ssr-smoke.ts` renders 49 fixtures with `renderApplication()` (every root widget, `bf-icon`,
144
+ `bf-number-input`, `bf-select` from `/overlay`, a `/charts` line chart, a `/mobile` page with
145
+ `provideBfShell()`, `bf-page`, `bf-header` and `bf-content`, `bf-tabs` with `bf-stack-outlet` under
146
+ the router, and an `/overlay` dialog), and all of them render without an error. 31 of them render
147
+ identically twice. The other 18 number their ids differently on every render, from a module-level
148
+ counter, so they could not hydrate: `bf-app-frame`, `bf-calendar`, `bf-checkbox`, `bf-file-input`, `bf-image-input`, `bf-input`,
149
+ `bf-radio-group`, `bf-segmented`, `bf-select`, `bf-sidebar`, `bf-tab-group`, `bf-table`,
150
+ `bf-textarea`, `bf-theme-switch`, `bf-toggle`, `bf-line-chart`, `bf-number-input` and the `/overlay`
151
+ dialog. Hydration itself is still not supported or tested, and more stands in its way: `/mobile`
152
+ writes to `<html>` and adds a keyboard probe to `<body>`, and an `/overlay` dialog renders outside
153
+ the app root and marks it inert.
154
154
 
155
155
  ## Testing
156
156
 
@@ -163,7 +163,7 @@ overlays and themes leave on the document after each test (when TestBed destroys
163
163
  environment, so keep its default `teardown: { destroyAfterEach: true }`), and the CDK harnesses
164
164
  (`BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`,
165
165
  `BfMenuButtonHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfDesktopToastHarness`,
166
- `BfTableHarness`, `BfPaginationHarness`, `BfTabGroupHarness`, `BfCalendarHarness`, `BfChartHarness`) drive the
166
+ `BfTableHarness`, `BfPaginationHarness`, `BfTabGroupHarness`, `BfCalendarHarness`, `BfFileInputHarness`, `BfImageInputHarness`, `BfMediaCardHarness`, `BfImageViewerHarness`, `BfChatComposerHarness`, `BfChartHarness`) drive the
167
167
  widgets whose DOM a spec should not query by hand; overlays come from
168
168
  `TestbedHarnessEnvironment.documentRootLoader(fixture)`. An app on the mobile shell uses
169
169
  `provideBfShellTesting()` from `@bitforgehq/ui/mobile` instead (router, shell, an in-memory theme
@@ -136,6 +136,33 @@
136
136
  "summary": "Card around one chart with a title, period control and loading, empty and error states",
137
137
  "since": "0.3.1"
138
138
  },
139
+ {
140
+ "name": "chat-composer",
141
+ "selector": "bf-chat-composer",
142
+ "className": "BfChatComposerComponent",
143
+ "category": "Forms",
144
+ "entryPoint": "@bitforgehq/ui/overlay",
145
+ "summary": "Chat input that grows with the text and sends on Enter, or stops a reply",
146
+ "since": "0.5.0"
147
+ },
148
+ {
149
+ "name": "chat-message",
150
+ "selector": "bf-chat-message",
151
+ "className": "BfChatMessageComponent",
152
+ "category": "Data display",
153
+ "entryPoint": "@bitforgehq/ui",
154
+ "summary": "One chat message, a user bubble or a full-width assistant reply that can stream",
155
+ "since": "0.5.0"
156
+ },
157
+ {
158
+ "name": "chat-thread",
159
+ "selector": "bf-chat-thread",
160
+ "className": "BfChatThreadComponent",
161
+ "category": "Data display",
162
+ "entryPoint": "@bitforgehq/ui",
163
+ "summary": "Scrolling chat message log that follows new text and offers a jump to latest",
164
+ "since": "0.5.0"
165
+ },
139
166
  {
140
167
  "name": "checkbox",
141
168
  "selector": "bf-checkbox",
@@ -262,6 +289,15 @@
262
289
  "summary": "Label, help text and error text wrapper that wires ids for assistive tech",
263
290
  "since": "0.1.0"
264
291
  },
292
+ {
293
+ "name": "file-input",
294
+ "selector": "bf-file-input",
295
+ "className": "BfFileInputComponent",
296
+ "category": "Forms",
297
+ "entryPoint": "@bitforgehq/ui",
298
+ "summary": "File picker field that lists picked or dropped files and checks type, size and count",
299
+ "since": "0.5.0"
300
+ },
265
301
  {
266
302
  "name": "header",
267
303
  "selector": "bf-header",
@@ -289,6 +325,24 @@
289
325
  "summary": "Round icon-only button for toolbars and rows, with a required accessible label",
290
326
  "since": "0.1.0"
291
327
  },
328
+ {
329
+ "name": "image-input",
330
+ "selector": "bf-image-input",
331
+ "className": "BfImageInputComponent",
332
+ "category": "Forms",
333
+ "entryPoint": "@bitforgehq/ui",
334
+ "summary": "Single-image picker with a round or square preview tile and replace or remove",
335
+ "since": "0.5.0"
336
+ },
337
+ {
338
+ "name": "image-viewer",
339
+ "selector": "bf-image-viewer",
340
+ "className": "BfImageViewerComponent",
341
+ "category": "Overlays",
342
+ "entryPoint": "@bitforgehq/ui/overlay",
343
+ "summary": "Full-screen image viewer that zooms from a thumbnail and steps through a set",
344
+ "since": "0.5.0"
345
+ },
292
346
  {
293
347
  "name": "input",
294
348
  "selector": "bf-input",
@@ -325,6 +379,15 @@
325
379
  "summary": "List row with leading slot, title, subtitle, trailing slot and an optional chevron",
326
380
  "since": "0.1.0"
327
381
  },
382
+ {
383
+ "name": "media-card",
384
+ "selector": "bf-media-card",
385
+ "className": "BfMediaCardComponent",
386
+ "category": "Data display",
387
+ "entryPoint": "@bitforgehq/ui",
388
+ "summary": "Card with an image that loads, fades in or fails, plus a body and actions",
389
+ "since": "0.5.0"
390
+ },
328
391
  {
329
392
  "name": "menu",
330
393
  "selector": "bf-menu",
@@ -465,8 +528,8 @@
465
528
  "selector": "bf-select",
466
529
  "className": "BfSelectComponent",
467
530
  "category": "Forms",
468
- "entryPoint": "@bitforgehq/ui",
469
- "summary": "Native single-choice dropdown styled like the text field, with Signal Forms binding",
531
+ "entryPoint": "@bitforgehq/ui/overlay",
532
+ "summary": "Single-choice dropdown with a styled list, native under touch, with Signal Forms binding",
470
533
  "since": "0.1.0"
471
534
  },
472
535
  {
@@ -550,6 +613,24 @@
550
613
  "summary": "One labelled figure with an optional icon tile and a supporting line",
551
614
  "since": "0.3.1"
552
615
  },
616
+ {
617
+ "name": "step",
618
+ "selector": "bf-step",
619
+ "className": "BfStepComponent",
620
+ "category": "Data display",
621
+ "entryPoint": "@bitforgehq/ui",
622
+ "summary": "One numbered step of an instruction list, with a title and its details",
623
+ "since": "0.4.0"
624
+ },
625
+ {
626
+ "name": "steps",
627
+ "selector": "bf-steps",
628
+ "className": "BfStepsComponent",
629
+ "category": "Data display",
630
+ "entryPoint": "@bitforgehq/ui",
631
+ "summary": "Numbered instruction steps a user follows in order, with joined markers",
632
+ "since": "0.4.0"
633
+ },
553
634
  {
554
635
  "name": "tab",
555
636
  "selector": "bf-tab",
@@ -640,6 +721,24 @@
640
721
  "summary": "Typed time-of-day field with a popover list of times, valued as an ISO string",
641
722
  "since": "0.3.1"
642
723
  },
724
+ {
725
+ "name": "timeline",
726
+ "selector": "bf-timeline",
727
+ "className": "BfTimelineComponent",
728
+ "category": "Data display",
729
+ "entryPoint": "@bitforgehq/ui",
730
+ "summary": "Dated list of events with who did what and when, on a vertical line",
731
+ "since": "0.4.0"
732
+ },
733
+ {
734
+ "name": "timeline-event",
735
+ "selector": "bf-timeline-event",
736
+ "className": "BfTimelineEventComponent",
737
+ "category": "Data display",
738
+ "entryPoint": "@bitforgehq/ui",
739
+ "summary": "One timeline event with a coloured marker, its text, actor and time",
740
+ "since": "0.4.0"
741
+ },
643
742
  {
644
743
  "name": "toast",
645
744
  "selector": "bf-toast",