@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
@@ -0,0 +1,357 @@
1
+ # bf-chat-composer
2
+
3
+ Chat input that grows with the text and sends on Enter, or stops a reply
4
+
5
+ `BfChatComposerComponent` from `@bitforgehq/ui/overlay` · Forms · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
11
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
12
+ import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
13
+ import { ArrowUp, Square } from 'lucide';
14
+
15
+ /* How long the pretend reply runs before it finishes by itself. */
16
+ const REPLY_TIME = 3000;
17
+
18
+ /**
19
+ * The bf-chat-composer overview.
20
+ *
21
+ * Type and press Enter (Shift+Enter for a new line) or the send button: the text is sent, the
22
+ * field clears, and the send button turns into Stop while the pretend reply runs. Stop ends it
23
+ * early. Icons go in the `send-icon` and `stop-icon` slots; their names stay as text.
24
+ */
25
+ @Component({
26
+ selector: 'app-example-chat-composer-overview',
27
+ imports: [BfChatComposerComponent, BfIconComponent],
28
+ template: `
29
+ <div class="flex flex-col gap-3">
30
+ <bf-chat-composer
31
+ placeholder="Ask for ideas"
32
+ [(value)]="draft"
33
+ [running]="running()"
34
+ (sent)="send($event)"
35
+ (stopped)="finish('Stopped.')"
36
+ >
37
+ <bf-icon bfSlot="send-icon" [icon]="arrowUp" />
38
+ <bf-icon bfSlot="stop-icon" [icon]="square" />
39
+ </bf-chat-composer>
40
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
41
+ </div>
42
+ `,
43
+ })
44
+ export class ChatComposerOverviewExampleComponent {
45
+ protected readonly arrowUp = ArrowUp;
46
+ protected readonly square = Square;
47
+
48
+ readonly draft = signal('');
49
+ readonly running = signal(false);
50
+ readonly status = signal('Nothing sent yet.');
51
+
52
+ private timer: ReturnType<typeof setTimeout> | null = null;
53
+
54
+ constructor() {
55
+ inject(DestroyRef).onDestroy(() => this.clearTimer());
56
+ }
57
+
58
+ send(text: string): void {
59
+ this.running.set(true);
60
+ this.status.set(`Sent: "${text}". Replying...`);
61
+ this.timer = setTimeout(() => this.finish('Reply done.'), REPLY_TIME);
62
+ }
63
+
64
+ finish(status: string): void {
65
+ this.clearTimer();
66
+ this.running.set(false);
67
+ this.status.set(status);
68
+ }
69
+
70
+ private clearTimer(): void {
71
+ if (this.timer !== null) {
72
+ clearTimeout(this.timer);
73
+ this.timer = null;
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Overview
80
+
81
+ The input row of a chat: a `bf-textarea` that grows with the text (`autoResize`, up to
82
+ `maxRows`, then it scrolls), a start and an end slot for extra controls, and one button that
83
+ sends the text or, while a reply is `running`, stops it. `Enter` sends and `Shift+Enter`
84
+ starts a new line; nothing sends during IME composition or
85
+ when the text is only whitespace. After a send the text clears and focus stays in the field.
86
+ `disabled` with a `disabledReason` shows the reason as the button's tooltip and as the empty
87
+ field's placeholder.
88
+
89
+ The input row of a chat: a text field that grows with the text, slots for extra controls, and one button that sends the text or stops the running reply. Import it from `@bitforgehq/ui/overlay` (it needs `@angular/cdk` for the disabled reason's tooltip).
90
+
91
+ ## When to use
92
+
93
+ Use `bf-chat-composer` below a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread) to write to an assistant. Use [bf-textarea](https://bitforge-ui.web.app/components/textarea) instead for free text in a form that is saved with the form, such as a comment field.
94
+
95
+ ## Basic usage
96
+
97
+ Bind the text with `[(value)]` and handle `(sent)`, which emits the trimmed text; the field then clears and keeps focus. <kbd>Enter</kbd> sends and <kbd>Shift</kbd>+<kbd>Enter</kbd> starts a new line. Nothing sends while an input method is composing (Japanese, Chinese or Korean input confirm their text with <kbd>Enter</kbd>), and empty or whitespace-only text never sends: the send button is disabled then. `(changed)` emits the text after each edit by the user.
98
+
99
+ ```ts
100
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
101
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
102
+ import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
103
+ import { ArrowUp, Square } from 'lucide';
104
+
105
+ /* How long the pretend reply runs before it finishes by itself. */
106
+ const REPLY_TIME = 3000;
107
+
108
+ /**
109
+ * The bf-chat-composer overview.
110
+ *
111
+ * Type and press Enter (Shift+Enter for a new line) or the send button: the text is sent, the
112
+ * field clears, and the send button turns into Stop while the pretend reply runs. Stop ends it
113
+ * early. Icons go in the `send-icon` and `stop-icon` slots; their names stay as text.
114
+ */
115
+ @Component({
116
+ selector: 'app-example-chat-composer-overview',
117
+ imports: [BfChatComposerComponent, BfIconComponent],
118
+ template: `
119
+ <div class="flex flex-col gap-3">
120
+ <bf-chat-composer
121
+ placeholder="Ask for ideas"
122
+ [(value)]="draft"
123
+ [running]="running()"
124
+ (sent)="send($event)"
125
+ (stopped)="finish('Stopped.')"
126
+ >
127
+ <bf-icon bfSlot="send-icon" [icon]="arrowUp" />
128
+ <bf-icon bfSlot="stop-icon" [icon]="square" />
129
+ </bf-chat-composer>
130
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
131
+ </div>
132
+ `,
133
+ })
134
+ export class ChatComposerOverviewExampleComponent {
135
+ protected readonly arrowUp = ArrowUp;
136
+ protected readonly square = Square;
137
+
138
+ readonly draft = signal('');
139
+ readonly running = signal(false);
140
+ readonly status = signal('Nothing sent yet.');
141
+
142
+ private timer: ReturnType<typeof setTimeout> | null = null;
143
+
144
+ constructor() {
145
+ inject(DestroyRef).onDestroy(() => this.clearTimer());
146
+ }
147
+
148
+ send(text: string): void {
149
+ this.running.set(true);
150
+ this.status.set(`Sent: "${text}". Replying...`);
151
+ this.timer = setTimeout(() => this.finish('Reply done.'), REPLY_TIME);
152
+ }
153
+
154
+ finish(status: string): void {
155
+ this.clearTimer();
156
+ this.running.set(false);
157
+ this.status.set(status);
158
+ }
159
+
160
+ private clearTimer(): void {
161
+ if (this.timer !== null) {
162
+ clearTimeout(this.timer);
163
+ this.timer = null;
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Send and stop
170
+
171
+ While `running` is true (the app's reply is streaming) the button is Stop: it emits `(stopped)` and the app ends the reply and sets `running` back to false. <kbd>Enter</kbd> does not send while a reply runs, and the text stays in the field. The button keeps its place and focus while it switches.
172
+
173
+ ## Icons
174
+
175
+ The `send-icon` and `stop-icon` slots take the button's icons (the library never imports an icon into a widget). With an icon the button is a round icon button and its text ("Send", "Stop", from `chatSend` and `chatStop`) stays as its accessible name; without one the button shows the text.
176
+
177
+ ## Extra controls
178
+
179
+ The `start` slot (before the field) and the `end` slot (between the field and the button) take controls of the app, such as a preset picker or an attach button. They line up with the bottom of the field as it grows.
180
+
181
+ ```ts
182
+ import { Component, signal } from '@angular/core';
183
+ import { BfIconButtonComponent, type BfSelectOption } from '@bitforgehq/ui';
184
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
185
+ import { BfChatComposerComponent, BfSelectComponent } from '@bitforgehq/ui/overlay';
186
+ import { Paperclip } from 'lucide';
187
+
188
+ type Preset = 'names' | 'slogans' | 'menu';
189
+
190
+ /**
191
+ * Extra controls in the start and end slots.
192
+ *
193
+ * A preset picker in the `start` slot and an attach button in the `end` slot, beside the field;
194
+ * without icons in its slots the send button shows its text. The sent line names the preset the
195
+ * message went out with.
196
+ */
197
+ @Component({
198
+ selector: 'app-example-chat-composer-slots',
199
+ imports: [BfChatComposerComponent, BfSelectComponent, BfIconButtonComponent, BfIconComponent],
200
+ template: `
201
+ <div class="flex flex-col gap-3">
202
+ <bf-chat-composer placeholder="Describe the cafe" (sent)="sent.set(preset() + ': ' + $event)">
203
+ <bf-select
204
+ bfSlot="start"
205
+ ariaLabel="Preset"
206
+ class="w-36"
207
+ [options]="presets"
208
+ [(value)]="preset"
209
+ />
210
+ <bf-icon-button bfSlot="end" variant="text" label="Attach a file">
211
+ <bf-icon [icon]="paperclip" />
212
+ </bf-icon-button>
213
+ </bf-chat-composer>
214
+ <p data-sent class="text-caption text-text-muted">{{ sent() || 'Nothing sent yet.' }}</p>
215
+ </div>
216
+ `,
217
+ })
218
+ export class ChatComposerSlotsExampleComponent {
219
+ protected readonly paperclip = Paperclip;
220
+
221
+ readonly presets: readonly BfSelectOption<Preset>[] = [
222
+ { value: 'names', label: 'Names' },
223
+ { value: 'slogans', label: 'Slogans' },
224
+ { value: 'menu', label: 'Menu' },
225
+ ];
226
+ readonly preset = signal<Preset | null>('names');
227
+ readonly sent = signal('');
228
+ }
229
+ ```
230
+
231
+ ## Growing field
232
+
233
+ The field is a [bf-textarea](https://bitforge-ui.web.app/components/textarea) with `autoResize`: it starts at one line and grows with the text up to `maxRows` lines (8 by default), then it scrolls. `maxLength` limits the text. A browser without CSS field sizing keeps the one-line field and scrolls inside it.
234
+
235
+ ## Disabled
236
+
237
+ `disabled` stops typing and sending. With a `disabledReason` (a usage limit, a missing choice) the reason is the empty field's placeholder and the send button's tooltip on hover and keyboard focus; the button then stays focusable, marked `aria-disabled`, so the tooltip can show.
238
+
239
+ ```ts
240
+ import { Component } from '@angular/core';
241
+ import { BfChatComposerComponent } from '@bitforgehq/ui/overlay';
242
+
243
+ /**
244
+ * Disabled with a reason.
245
+ *
246
+ * While nothing can be sent, `disabled` with a `disabledReason` says why: it is the empty
247
+ * field's placeholder, and the send button's tooltip on hover and keyboard focus (the button
248
+ * stays focusable for that, marked `aria-disabled`).
249
+ */
250
+ @Component({
251
+ selector: 'app-example-chat-composer-disabled',
252
+ imports: [BfChatComposerComponent],
253
+ template: `
254
+ <bf-chat-composer disabled disabledReason="You have used today's 20 messages. More tomorrow." />
255
+ `,
256
+ })
257
+ export class ChatComposerDisabledExampleComponent {}
258
+ ```
259
+
260
+ ## Sizes
261
+
262
+ `size` sets the field's padding and type and the button's height together: `sm` 32 px, `md` 40 px (default), `lg` 48 px buttons. Unset, it follows `provideBfDefaults()`'s `fieldSize`; a touch app that sets `fieldSize: 'lg'` gets the 48 px composer.
263
+
264
+ ## Desktop
265
+
266
+ The field shows the text cursor and its focus ring on every focus. The button has bf-button's hover and pressed steps (primary for send, tonal for stop); disabled it shows no hover. With a `disabledReason` its tooltip shows after a 500 ms hover or at once on keyboard focus. Under a coarse pointer the button's hit area grows to 44 px; the field gets no touch extension (set `fieldSize: 'lg'` for touch).
267
+
268
+ ## Theming
269
+
270
+ - Field: see [bf-textarea](https://bitforge-ui.web.app/components/textarea) (`border-strong`, `primary` focus border, `ring` ring, `text-muted` placeholder).
271
+ - Send button: `primary` with `on-primary` text (`primary-hover`, `primary-pressed`); stop button: `primary-soft` with `primary-strong`; disabled: `surface-sunken` with `text-disabled`.
272
+ - Tooltip: see the `bfTooltip` directive.
273
+
274
+ ## Accessibility
275
+
276
+ The field is a native `textarea`, named by `label` (visible), else `ariaLabel` or `ariaLabelledby`, else the `chatComposer` label ("Message"). The button is a native `button` whose text is its name, also when an icon shows ("Send", "Stop"); it is natively `disabled` with nothing to send, and `aria-disabled` while disabled with a reason, which the tooltip then describes (`aria-describedby`) when it shows. After a send focus returns to the field. The tooltip shows nothing under a coarse pointer, so the reason is also the placeholder of the empty field.
277
+
278
+ ### Keyboard
279
+
280
+ | Key | Where | Action |
281
+ | ------------------------------------------------ | -------- | ----------------------------------------------------------------------------- |
282
+ | <kbd>Enter</kbd> | Field | Sends the text (not while composing, while a reply runs, or when it is empty) |
283
+ | <kbd>Shift</kbd>+<kbd>Enter</kbd> | Field | Starts a new line |
284
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Composer | Moves between the start controls, the field, the end controls and the button |
285
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Button | Sends the text, or stops the reply while it runs |
286
+ | <kbd>Escape</kbd> | Button | Hides the disabled reason's tooltip |
287
+
288
+ ## Inputs
289
+
290
+ | Name | Type | Default | Description |
291
+ | ---------------- | ----------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
292
+ | `running` | `boolean` | `false` | A reply is running: the button stops it (`stopped`) instead of sending. |
293
+ | `disabled` | `boolean` | `false` | Nothing can be written or sent. |
294
+ | `disabledReason` | `string` | `''` | Why it is disabled, shown as the button's tooltip and the empty field's placeholder. |
295
+ | `label` | `string` | `''` | Visible label above the field; usually left empty in a chat. |
296
+ | `ariaLabel` | `string` | `''` | The field's accessible name without a visible label; empty uses the `chatComposer` label ("Message"). |
297
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the field without a visible label. |
298
+ | `placeholder` | `string` | `''` | Example text shown while the field is empty. |
299
+ | `maxRows` | `number` | `8` | The lines the field grows to at most; past them it scrolls. |
300
+ | `maxLength` | `number \| undefined` | `undefined` | Maximum number of characters (native `maxlength`). |
301
+ | `name` | `string` | `''` | Native name attribute of the textarea. |
302
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Padding, type and button step: sm, md, lg; unset follows `provideBfDefaults()` (`fieldSize`, `md` by default). |
303
+ | `value` | `string` | `''` | The text being written; two-way bindable. Cleared after a send. Two-way: `[(value)]`. |
304
+
305
+ ## Outputs
306
+
307
+ | Name | Payload | Description |
308
+ | --------- | -------- | ------------------------------------------------------------------------------------ |
309
+ | `changed` | `string` | Emitted with the new text right after the user changes it (each keystroke or paste). |
310
+ | `sent` | `string` | Emitted with the trimmed text when the user sends it (Enter or the send button). |
311
+ | `stopped` | `void` | Emitted when the user presses the stop button while `running`. |
312
+
313
+ ## Slots
314
+
315
+ - `start`: Controls before the field, such as a preset picker or an attach button.
316
+ - `end`: Controls between the field and the send button.
317
+ - `send-icon`: The send button's icon; without one the button shows its text.
318
+ - `stop-icon`: The stop button's icon; without one the button shows its text.
319
+
320
+ ## Methods
321
+
322
+ ### `send(): void`
323
+
324
+ Sends the text: emits `sent` with it trimmed, clears the field and keeps focus in it. Does
325
+ nothing while disabled or running, or when the text is only whitespace.
326
+
327
+ ### `focus(): void`
328
+
329
+ Focuses the text field.
330
+
331
+ ## Notes
332
+
333
+ ### Do
334
+
335
+ - Set `running` from the app's own reply state and turn it off when the reply ends, stops or fails.
336
+ - Give a `disabledReason` whenever the composer is disabled for a reason the user can act on.
337
+
338
+ ### Don't
339
+
340
+ - Don't clear the value yourself after `(sent)`: the composer does it.
341
+ - Don't put the reply's progress in the composer; show it in the thread's streaming message.
342
+
343
+ ## Migration notes
344
+
345
+ ### 0.5.0
346
+
347
+ New: `bf-chat-composer` in `@bitforgehq/ui/overlay`, the input row of a chat. A `bf-textarea`
348
+ with `autoResize` grows from one line to `maxRows` (8), then scrolls. `Enter` sends and
349
+ `Shift+Enter` starts a new line; nothing sends during IME composition or with whitespace-only
350
+ text. `value` is a `model()` that clears after a send; `sent` emits the trimmed text, `changed`
351
+ each edit. While `running` the send button is a stop button that emits `stopped`. Icons go in the
352
+ `send-icon` and `stop-icon` slots (the text stays as the button's name, and shows without an
353
+ icon); `start` and `end` slots take extra controls. `disabled` with `disabledReason` shows the
354
+ reason as the button's tooltip (`aria-disabled`, still focusable) and the empty field's
355
+ placeholder. `size` follows `fieldSize`. New labels `chatComposer`, `chatSend` and `chatStop`.
356
+ `@bitforgehq/ui/testing`: new `BfChatComposerHarness` (`setValue()`, `getValue()`, `send()`,
357
+ `pressEnter()`, `stop()`, `isRunning()`, `isDisabled()`, `canAct()`, `getButtonLabel()`).
@@ -0,0 +1,251 @@
1
+ # bf-chat-message
2
+
3
+ One chat message, a user bubble or a full-width assistant reply that can stream
4
+
5
+ `BfChatMessageComponent` from `@bitforgehq/ui` · Data display · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { BfChatMessageComponent, BfChatThreadComponent } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-chat-message overview.
15
+ *
16
+ * A user message is a tinted bubble at the inline end and an assistant message spans the row
17
+ * with no bubble. Each one names its author and time in a small header; the time is a `time`
18
+ * element with the exact value, shown as hours and minutes or as `timeText`.
19
+ */
20
+ @Component({
21
+ selector: 'app-example-chat-message-overview',
22
+ imports: [BfChatThreadComponent, BfChatMessageComponent],
23
+ template: `
24
+ <bf-chat-thread label="Two messages">
25
+ <bf-chat-message role="user" author="You" time="2026-11-06T09:30:00Z" timeText="9:30">
26
+ What should the cafe's first menu board say?
27
+ </bf-chat-message>
28
+ <bf-chat-message author="Assistant" time="2026-11-06T09:30:00Z" timeText="9:30">
29
+ <p>
30
+ Keep it to five lines: the coffee, the tea, one cake, one sandwich and the price of a
31
+ refill.
32
+ </p>
33
+ <p>Write it by hand. Regulars read a hand-written board as a sign that someone cares.</p>
34
+ </bf-chat-message>
35
+ </bf-chat-thread>
36
+ `,
37
+ })
38
+ export class ChatMessageOverviewExampleComponent {}
39
+ ```
40
+
41
+ ## Overview
42
+
43
+ One message of a `bf-chat-thread`. A `user` message is a tinted bubble at the inline end; an
44
+ `assistant` message is full width with no bubble. The default slot is the message text, styled
45
+ as prose (`bfProse`) for markdown the app renders. An optional header names the `author` and
46
+ the `time`. While `streaming`, a caret pulses at the end of the text and the message is
47
+ `aria-busy`, so the thread announces it once, when it is done. A `stopped` or `failed` message
48
+ shows a short status line, with the `action` slot beside it (a Retry button).
49
+
50
+ One message of a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread): a tinted bubble for the user, a full-width reply without a bubble for the assistant, with an optional header, a streaming caret and a status line.
51
+
52
+ ## When to use
53
+
54
+ Use `bf-chat-message` inside a [bf-chat-thread](https://bitforge-ui.web.app/components/chat-thread), one per message. Use [bf-timeline-event](https://bitforge-ui.web.app/components/timeline-event) instead for an event in a history, and [bf-alert](https://bitforge-ui.web.app/components/alert) for a message from the app itself rather than from a conversation.
55
+
56
+ ## Basic usage
57
+
58
+ `role` is `assistant` (the default: full width, no bubble) or `user` (a `primary-soft` bubble at the inline end, up to 85 % of the row). The default slot is the text: plain text, paragraphs, or the app's rendered markdown. `author` and `time` (a Date or an ISO 8601 string, shown as hours and minutes in the app's locale, or as `timeText`) fill a small header above the message; leave both empty for none.
59
+
60
+ ```ts
61
+ import { Component } from '@angular/core';
62
+ import { BfChatMessageComponent, BfChatThreadComponent } from '@bitforgehq/ui';
63
+
64
+ /**
65
+ * The bf-chat-message overview.
66
+ *
67
+ * A user message is a tinted bubble at the inline end and an assistant message spans the row
68
+ * with no bubble. Each one names its author and time in a small header; the time is a `time`
69
+ * element with the exact value, shown as hours and minutes or as `timeText`.
70
+ */
71
+ @Component({
72
+ selector: 'app-example-chat-message-overview',
73
+ imports: [BfChatThreadComponent, BfChatMessageComponent],
74
+ template: `
75
+ <bf-chat-thread label="Two messages">
76
+ <bf-chat-message role="user" author="You" time="2026-11-06T09:30:00Z" timeText="9:30">
77
+ What should the cafe's first menu board say?
78
+ </bf-chat-message>
79
+ <bf-chat-message author="Assistant" time="2026-11-06T09:30:00Z" timeText="9:30">
80
+ <p>
81
+ Keep it to five lines: the coffee, the tea, one cake, one sandwich and the price of a
82
+ refill.
83
+ </p>
84
+ <p>Write it by hand. Regulars read a hand-written board as a sign that someone cares.</p>
85
+ </bf-chat-message>
86
+ </bf-chat-thread>
87
+ `,
88
+ })
89
+ export class ChatMessageOverviewExampleComponent {}
90
+ ```
91
+
92
+ ## Streaming
93
+
94
+ Set `streaming` while the reply's text is still arriving and append the text as it comes. A caret pulses at the end of the text (after bare text, inside the last paragraph or list item, also one wrapper deep), and the message and the thread are `aria-busy`, so a screen reader reads the reply once, when `streaming` goes back to false. An empty streaming message shows the caret alone, as a "thinking" cue.
95
+
96
+ ## Stopped and failed
97
+
98
+ `status` is `done` (the default), `stopped` (a muted "Stopped" line) or `failed` (a "Something went wrong" line in the danger colour). `statusMessage` replaces the built-in line, such as with the error the server returned. The `action` slot sits beside the line, for a Retry button.
99
+
100
+ ```ts
101
+ import { Component, signal } from '@angular/core';
102
+ import {
103
+ BfButtonComponent,
104
+ BfChatMessageComponent,
105
+ BfChatThreadComponent,
106
+ type BfChatMessageStatus,
107
+ } from '@bitforgehq/ui';
108
+
109
+ /**
110
+ * Stopped and failed messages.
111
+ *
112
+ * A reply the user stopped shows "Stopped" under its text; a reply that failed shows "Something
113
+ * went wrong" in the danger colour, with a Retry button in the message's `action` slot. Retry
114
+ * here answers at once; a real app would ask its model again.
115
+ */
116
+ @Component({
117
+ selector: 'app-example-chat-thread-states',
118
+ imports: [BfChatThreadComponent, BfChatMessageComponent, BfButtonComponent],
119
+ template: `
120
+ <bf-chat-thread label="Message states" class="max-h-[24rem]">
121
+ <bf-chat-message role="user">Write a slogan for the cafe.</bf-chat-message>
122
+ <bf-chat-message status="stopped">
123
+ <p>Good coffee, slow mornings and a</p>
124
+ </bf-chat-message>
125
+ <bf-chat-message role="user">Try a shorter one.</bf-chat-message>
126
+ <bf-chat-message [status]="status()">
127
+ @if (status() === 'done') {
128
+ <p>Stay for the second cup.</p>
129
+ }
130
+ @if (status() === 'failed') {
131
+ <bf-button bfSlot="action" variant="text" size="sm" (clicked)="retry()">Retry</bf-button>
132
+ }
133
+ </bf-chat-message>
134
+ </bf-chat-thread>
135
+ `,
136
+ })
137
+ export class ChatThreadStatesExampleComponent {
138
+ readonly status = signal<BfChatMessageStatus>('failed');
139
+
140
+ retry(): void {
141
+ this.status.set('done');
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## Rich content
147
+
148
+ The body is a `bfProse` element: headings, lists, links, inline code, code blocks, quotes and rules in the library tokens. The `content` slot below it holds inline content the prose styles leave alone, such as cards, images or chips.
149
+
150
+ ```ts
151
+ import { Component, signal } from '@angular/core';
152
+ import {
153
+ BfCardComponent,
154
+ BfChatMessageComponent,
155
+ BfChatThreadComponent,
156
+ BfChipComponent,
157
+ } from '@bitforgehq/ui';
158
+
159
+ /* The app renders its markdown to HTML (and sanitises it) itself; the message styles it. */
160
+ const REPLY_HTML = `
161
+ <h3>Three name ideas</h3>
162
+ <ol>
163
+ <li><strong>Ember and Oat</strong>: warm and rustic.</li>
164
+ <li><strong>Second Cup</strong>: playful, for regulars.</li>
165
+ <li><strong>Field Notes</strong>: quiet and bookish.</li>
166
+ </ol>
167
+ <p>Check the domain with <code>whois embersandoat.com</code> before you print the menus.</p>
168
+ <blockquote>A short name is easier to say out loud and to find.</blockquote>
169
+ `;
170
+
171
+ /**
172
+ * Rendered markdown and inline content.
173
+ *
174
+ * The reply's body is the app's rendered markdown, styled as prose: a heading, a numbered list,
175
+ * inline code and a quote. Below it, the `content` slot holds a card and chips the user can pick
176
+ * from, outside the prose styling.
177
+ */
178
+ @Component({
179
+ selector: 'app-example-chat-thread-rich-content',
180
+ imports: [BfChatThreadComponent, BfChatMessageComponent, BfCardComponent, BfChipComponent],
181
+ template: `
182
+ <bf-chat-thread label="Rich reply" class="max-h-[30rem]">
183
+ <bf-chat-message role="user" author="You">Name ideas, with a logo mood?</bf-chat-message>
184
+ <bf-chat-message author="Assistant">
185
+ <div [innerHTML]="replyHtml"></div>
186
+ <bf-card bfSlot="content" variant="tint" class="w-64">
187
+ <p class="text-label text-text font-bold">Logo mood</p>
188
+ <p class="text-caption text-text-muted">Hand-drawn, charcoal on oat paper</p>
189
+ </bf-card>
190
+ <div bfSlot="content" class="flex flex-wrap gap-2">
191
+ @for (name of names; track name) {
192
+ <bf-chip [selected]="picked() === name" (changed)="picked.set($event ? name : '')">{{
193
+ name
194
+ }}</bf-chip>
195
+ }
196
+ </div>
197
+ </bf-chat-message>
198
+ </bf-chat-thread>
199
+ `,
200
+ })
201
+ export class ChatThreadRichContentExampleComponent {
202
+ readonly replyHtml = REPLY_HTML;
203
+ readonly names = ['Ember and Oat', 'Second Cup', 'Field Notes'];
204
+ readonly picked = signal('');
205
+ }
206
+ ```
207
+
208
+ ## Desktop
209
+
210
+ A message has no hover or pointer state of its own; links in the prose underline more thickly on hover and show the focus ring on keyboard focus, and code blocks scroll sideways with the trackpad. The user bubble stops at 42rem on a wide window.
211
+
212
+ ## Theming
213
+
214
+ - User bubble: `primary-soft` background, `text` text, `rounded-field` corners. Assistant reply: no surface.
215
+ - Header: `text-muted` (the time), `text-secondary` (the author), the caption step.
216
+ - Prose: `text` body and headings, `primary-text` links with a `ring` focus ring, `surface-muted` inline code, `surface` code blocks with a `border` edge, `border-strong` quote rule with `text-secondary` text, `text-muted` list markers.
217
+ - Status line: `text-muted` for stopped, `danger` for failed.
218
+ - Caret: the text colour (`CanvasText` under forced colours), pulsing with `--animate-chat-caret`.
219
+
220
+ ## Accessibility
221
+
222
+ The message is plain content in the thread's log: no role of its own (the `role` input sets the look, and the host drops the attribute). The header's time is a `time` element whose `datetime` keeps the exact value. While `streaming`, the message is `aria-busy="true"` and the caret is a pseudo-element, which is never read. The status line is text in the log, so a screen reader announces it when the status changes. A `user` message is told apart by its `author` text, not only by its colour and side.
223
+
224
+ ## Inputs
225
+
226
+ | Name | Type | Default | Description |
227
+ | --------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
228
+ | `role` | `BfChatMessageRole` (`'user' \| 'assistant'`) | `'assistant'` | Who wrote it: `user` (a bubble at the inline end) or `assistant` (full width, no bubble). |
229
+ | `author` | `string` | `''` | Whose message it is, shown in the header, such as "You" or the assistant's name; empty shows none. |
230
+ | `time` | `Date \| string \| null` | `null` | When it was written: a Date or an ISO 8601 string, shown as hours and minutes; empty or invalid shows no time. |
231
+ | `timeText` | `string` | `''` | Shown in place of the formatted time, such as "just now"; the `time` element's `datetime` keeps the exact value. |
232
+ | `streaming` | `boolean` | `false` | The text is still arriving: a caret pulses at its end and the message is `aria-busy`. |
233
+ | `status` | `BfChatMessageStatus` (`'done' \| 'stopped' \| 'failed'`) | `'done'` | How it ended: `done` (no status line), `stopped` or `failed`. |
234
+ | `statusMessage` | `string` | `''` | Shown in place of the built-in status line (`chatMessageStopped`, `chatMessageFailed`), such as the error the server returned. |
235
+
236
+ ## Slots
237
+
238
+ - `default`: The message text: plain text or the app's rendered markdown, styled as prose.
239
+ - `content`: Inline content below the text, such as cards, images or chips (not styled as prose).
240
+ - `action`: Beside the status line, such as a Retry button.
241
+
242
+ ## Notes
243
+
244
+ ### Do
245
+
246
+ - Set `author` on every message, so its speaker is never shown by colour and position alone.
247
+ - Turn `streaming` off when the last piece of text has arrived, also when the request fails or stops.
248
+
249
+ ### Don't
250
+
251
+ - Don't put buttons in the text: use the `action` slot for one beside the status line.