@bitforgehq/ui 0.4.0 → 0.5.0-next.4

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 (87) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/README.md +12 -12
  3. package/ai/MANIFEST.index.json +74 -2
  4. package/ai/MANIFEST.json +3507 -1763
  5. package/ai/REFERENCE.index.json +189 -0
  6. package/ai/REFERENCE.json +1617 -139
  7. package/ai/UI_GUIDE.md +70 -20
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/badge.md +41 -1
  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-tile.md +452 -0
  22. package/ai/components/image-viewer.md +309 -0
  23. package/ai/components/media-card.md +478 -0
  24. package/ai/components/menu-button.md +67 -9
  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/pagination.md +59 -11
  31. package/ai/components/popover.md +13 -0
  32. package/ai/components/radio-group.md +6 -0
  33. package/ai/components/segmented.md +14 -2
  34. package/ai/components/select.md +100 -42
  35. package/ai/components/sidebar-toggle.md +16 -9
  36. package/ai/components/sidebar.md +41 -13
  37. package/ai/components/tab-group.md +18 -2
  38. package/ai/components/table.md +17 -2
  39. package/ai/components/theme-menu-button.md +1 -0
  40. package/ai/components/time-input.md +2 -0
  41. package/ai/components/toolbar.md +7 -18
  42. package/ai/reference/bf-chat-composer-harness.md +64 -0
  43. package/ai/reference/bf-chat-message-role.md +27 -0
  44. package/ai/reference/bf-chat-message-status.md +28 -0
  45. package/ai/reference/bf-fade-in-directive.md +18 -0
  46. package/ai/reference/bf-file-input-harness.md +67 -0
  47. package/ai/reference/bf-file-input-variant.md +27 -0
  48. package/ai/reference/bf-image-input-harness.md +67 -0
  49. package/ai/reference/bf-image-input-shape.md +27 -0
  50. package/ai/reference/bf-image-tile-aspect.md +28 -0
  51. package/ai/reference/bf-image-tile-caption-placement.md +27 -0
  52. package/ai/reference/bf-image-tile-harness.md +70 -0
  53. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  54. package/ai/reference/bf-image-viewer-harness.md +87 -0
  55. package/ai/reference/bf-image-viewer-image.md +25 -0
  56. package/ai/reference/bf-image-viewer-options.md +33 -0
  57. package/ai/reference/bf-image-viewer-origin.md +23 -0
  58. package/ai/reference/bf-image-viewer-ref.md +55 -0
  59. package/ai/reference/bf-image-viewer-service.md +32 -0
  60. package/ai/reference/bf-labels.md +107 -75
  61. package/ai/reference/bf-media-card-aspect.md +28 -0
  62. package/ai/reference/bf-media-card-harness.md +66 -0
  63. package/ai/reference/bf-motion-list-directive.md +22 -0
  64. package/ai/reference/bf-pagination-position-label.md +22 -0
  65. package/ai/reference/bf-prose-directive.md +16 -0
  66. package/ai/reference/bf-rejected-file-reason.md +28 -0
  67. package/ai/reference/bf-rejected-file.md +28 -0
  68. package/ai/reference/bf-select-harness.md +14 -5
  69. package/ai/reference/bf-select-option.md +2 -0
  70. package/ai/reference/bf-size.md +3 -0
  71. package/ai/reference/bf_table_sort_select.md +23 -0
  72. package/ai/reference/provide-bf-labels.md +11 -3
  73. package/ai/reference/provide-bf-select.md +20 -0
  74. package/bin/bf-ui.js +1 -0
  75. package/fesm2022/bitforgehq-ui-overlay.mjs +2660 -594
  76. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  77. package/fesm2022/bitforgehq-ui-testing.mjs +414 -10
  78. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  79. package/fesm2022/bitforgehq-ui.mjs +8056 -3993
  80. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  81. package/llms-full.txt +4923 -132
  82. package/llms.txt +36 -1
  83. package/package.json +1 -1
  84. package/theme.css +60 -0
  85. package/types/bitforgehq-ui-overlay.d.ts +695 -7
  86. package/types/bitforgehq-ui-testing.d.ts +517 -6
  87. package/types/bitforgehq-ui.d.ts +1699 -181
package/CHANGELOG.md CHANGED
@@ -4,6 +4,356 @@ 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
+ ### image-tile
307
+
308
+ New: `bf-image-tile`, a small card for grids of thumbnails (3D models with one render each, a
309
+ board of reference pictures). It is mostly picture, at a fixed `aspect` (new type
310
+ `BfImageTileAspect`: `'1/1'` (default), `'4/3'`, `'16/9'`, `'3/4'`), with the title (default slot,
311
+ a heading holding a `bfAction` link or button) and an optional `bfSlot="meta"` line below the
312
+ picture, or with `captionPlacement="overlay"` (new type `BfImageTileCaptionPlacement`: `'below'`
313
+ (default), `'overlay'`) on a soft `scrim` gradient over its bottom. States share
314
+ `bf-media-card`'s logic: without `src` a placeholder (`bfSlot="placeholder-icon"` and
315
+ `placeholderLabel`, default: the new label `imageTileEmpty`, "No image yet"); `loading`, and a
316
+ `src` that is loading, show a `bf-skeleton` shimmer (`aria-busy`); the picture fades in over
317
+ 260 ms; a load error or `failed` shows `failedLabel` (default `imageLoadFailed`). `loaded` and
318
+ `errored` report the picture's events. A `bfAction` makes the whole tile one link or button: the
319
+ pointer, a 2 px lift to `shadow-floating` and a 5 % zoom of the picture on hover (no movement
320
+ under reduced motion), and the focus ring. `selected` draws a 2 px primary ring and starts the
321
+ action's name with the new label `imageTileSelected` ("Selected, Harbour ship"). `bfSlot="badge"`
322
+ puts a `bf-badge` in the picture's top start corner. The title clamps to two lines and always
323
+ takes two, so tiles in a grid row are as tall. `@bitforgehq/ui/testing`: new
324
+ `BfImageTileHarness` (`getTitle()`, `getMeta()`, `getState()`, `getSrc()`, `getAlt()`,
325
+ `getPlaceholderText()`, `getFailedMessage()`, `isSelected()`, `isClickable()`, `click()`,
326
+ `loadImage()`, `failImage()`).
327
+
328
+ ### badge
329
+
330
+ New `icon-start` slot: `<bf-icon bfSlot="icon-start" … />` before the label text, drawn at 12 px
331
+ whatever the icon's `size`, 4 px from the text, with the start padding tightened to 8 px. It
332
+ takes the text colour, so it follows the variant and forced colours. A count pill does not show
333
+ it.
334
+
335
+ ### menu-button
336
+
337
+ New `fullWidth` input, as on `bf-button`: the host and its button stretch to the width of their
338
+ container (label and caret stay centred), so app CSS no longer needs to reach into the inner
339
+ `<button>`. Ignored with `iconOnly`.
340
+
341
+ ### pagination
342
+
343
+ New `positionLabel` input (new type `BfPaginationPositionLabel`, `(page, count) => string`) that
344
+ writes a compact pagination's position text, such as "v3 of 7"; unset it is the `pageOf` label as
345
+ before. New `previousLabel` and `nextLabel` inputs name the arrows ("Previous version"); empty
346
+ they fall back to `previousPage` and `nextPage`. Together they make a compact pager for stepping
347
+ through versions.
348
+
349
+ ## 0.5.0-next.4 (2026-11-07)
350
+
351
+ Pre-release of 0.5.0: its changes are listed under "0.5.0 (unreleased)" above.
352
+
353
+ ## 0.5.0-next.3 (2026-11-07)
354
+
355
+ Pre-release of 0.5.0: its changes are listed under "0.5.0 (unreleased)" above.
356
+
7
357
  ## 0.4.0 (2026-10-29)
8
358
 
9
359
  ### timeline
package/README.md CHANGED
@@ -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 45 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. 29 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 50 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. 32 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`, `BfImageTileHarness`, `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,33 @@
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-tile",
339
+ "selector": "bf-image-tile",
340
+ "className": "BfImageTileComponent",
341
+ "category": "Data display",
342
+ "entryPoint": "@bitforgehq/ui",
343
+ "summary": "Small card for thumbnail grids: a picture and title that act as one target",
344
+ "since": "0.5.0"
345
+ },
346
+ {
347
+ "name": "image-viewer",
348
+ "selector": "bf-image-viewer",
349
+ "className": "BfImageViewerComponent",
350
+ "category": "Overlays",
351
+ "entryPoint": "@bitforgehq/ui/overlay",
352
+ "summary": "Full-screen image viewer that zooms from a thumbnail and steps through a set",
353
+ "since": "0.5.0"
354
+ },
292
355
  {
293
356
  "name": "input",
294
357
  "selector": "bf-input",
@@ -325,6 +388,15 @@
325
388
  "summary": "List row with leading slot, title, subtitle, trailing slot and an optional chevron",
326
389
  "since": "0.1.0"
327
390
  },
391
+ {
392
+ "name": "media-card",
393
+ "selector": "bf-media-card",
394
+ "className": "BfMediaCardComponent",
395
+ "category": "Data display",
396
+ "entryPoint": "@bitforgehq/ui",
397
+ "summary": "Card with an image that loads, fades in or fails, plus a body and actions",
398
+ "since": "0.5.0"
399
+ },
328
400
  {
329
401
  "name": "menu",
330
402
  "selector": "bf-menu",
@@ -465,8 +537,8 @@
465
537
  "selector": "bf-select",
466
538
  "className": "BfSelectComponent",
467
539
  "category": "Forms",
468
- "entryPoint": "@bitforgehq/ui",
469
- "summary": "Native single-choice dropdown styled like the text field, with Signal Forms binding",
540
+ "entryPoint": "@bitforgehq/ui/overlay",
541
+ "summary": "Single-choice dropdown with a styled list, native under touch, with Signal Forms binding",
470
542
  "since": "0.1.0"
471
543
  },
472
544
  {