@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,478 @@
1
+ # bf-media-card
2
+
3
+ Card with an image that loads, fades in or fails, plus a body and actions
4
+
5
+ `BfMediaCardComponent` from `@bitforgehq/ui` · Data display · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, DestroyRef, type WritableSignal, inject, signal } from '@angular/core';
11
+ import { BfButtonComponent, BfMediaCardComponent } from '@bitforgehq/ui';
12
+
13
+ /** One generated picture as the app tracks it. */
14
+ interface Generation {
15
+ readonly src: string;
16
+ readonly generating: boolean;
17
+ readonly failed: boolean;
18
+ }
19
+
20
+ /** How long the simulated generation takes (a real one takes 30 to 90 seconds). */
21
+ const GENERATION_MS = 4000;
22
+
23
+ /**
24
+ * The bf-media-card overview.
25
+ *
26
+ * Live demo of `bf-media-card` with pictures that take a while to exist: the first is being drawn
27
+ * (a timer stands in for the image service) and fades in when it arrives; the second fails, and
28
+ * its Retry button starts it again; the third is already there.
29
+ */
30
+ @Component({
31
+ selector: 'app-example-media-card-overview',
32
+ imports: [BfButtonComponent, BfMediaCardComponent],
33
+ template: `
34
+ <div class="grid gap-4 @md:grid-cols-3">
35
+ <bf-media-card
36
+ alt="A neon city skyline at dusk"
37
+ loadingLabel="Drawing…"
38
+ [src]="starfall().src"
39
+ [loading]="starfall().generating"
40
+ >
41
+ <h2 class="text-heading">Starfall</h2>
42
+ <p class="text-body text-text-secondary">A city that falls one floor a night.</p>
43
+ <bf-button
44
+ bfSlot="footer"
45
+ class="relative z-1 self-start"
46
+ variant="tonal"
47
+ size="sm"
48
+ [disabled]="starfall().generating"
49
+ (clicked)="generate(starfall, 'media/skyline.svg', false)"
50
+ >
51
+ Draw again
52
+ </bf-button>
53
+ </bf-media-card>
54
+
55
+ <bf-media-card
56
+ alt="A harbour under a low sun"
57
+ loadingLabel="Drawing…"
58
+ failedLabel="The picture could not be drawn"
59
+ [src]="tidewalk().src"
60
+ [loading]="tidewalk().generating"
61
+ [failed]="tidewalk().failed"
62
+ >
63
+ <h2 class="text-heading">Tidewalk</h2>
64
+ <p class="text-body text-text-secondary">A harbour town that floods at every tide.</p>
65
+ <bf-button
66
+ bfSlot="failed-action"
67
+ variant="tonal"
68
+ size="sm"
69
+ (clicked)="generate(tidewalk, 'media/harbour.svg', false)"
70
+ >
71
+ Retry
72
+ </bf-button>
73
+ </bf-media-card>
74
+
75
+ <bf-media-card src="media/forest.svg" alt="A pine forest under a pale sun">
76
+ <h2 class="text-heading">Rootbound</h2>
77
+ <p class="text-body text-text-secondary">A forest that rearranges itself at night.</p>
78
+ </bf-media-card>
79
+ </div>
80
+ `,
81
+ })
82
+ export class MediaCardOverviewExampleComponent {
83
+ protected readonly starfall = signal<Generation>({ src: '', generating: true, failed: false });
84
+ protected readonly tidewalk = signal<Generation>({ src: '', generating: true, failed: false });
85
+ private readonly timers = new Set<ReturnType<typeof setTimeout>>();
86
+
87
+ constructor() {
88
+ inject(DestroyRef).onDestroy(() => this.timers.forEach((timer) => clearTimeout(timer)));
89
+ this.generate(this.starfall, 'media/skyline.svg', false);
90
+ // The first attempt at Tidewalk fails, so Retry shows.
91
+ this.generate(this.tidewalk, 'media/harbour.svg', true);
92
+ }
93
+
94
+ /** Starts a (simulated) generation that ends with the picture, or with a failure. */
95
+ protected generate(target: WritableSignal<Generation>, src: string, fails: boolean): void {
96
+ target.set({ src: '', generating: true, failed: false });
97
+ const timer = setTimeout(() => {
98
+ this.timers.delete(timer);
99
+ target.set(
100
+ fails
101
+ ? { src: '', generating: false, failed: true }
102
+ : { src, generating: false, failed: false },
103
+ );
104
+ }, GENERATION_MS);
105
+ this.timers.add(timer);
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Overview
111
+
112
+ Card for a picture and its story: a game pitch, a generated image, a product. The top is an
113
+ image area at a fixed aspect ratio that shows a shimmering skeleton while the picture loads
114
+ (also while it does not exist yet: set `loading`), fades the picture in when it arrives, and
115
+ shows a message with a Retry slot when it fails. Below it sit a body and a footer. Make the card
116
+ clickable with a `bfAction` link or button in its title, as on `bf-card`, and selectable with
117
+ `selectable` and `[(selected)]`.
118
+
119
+ A card led by a picture: a game pitch, a generated image, a product. The top is an image area at a fixed aspect ratio that the card fills itself from `src` and `alt`, so it owns the whole life of the picture: a shimmering skeleton while it loads (also while it does not exist yet), a fade when it arrives, and a message with a Retry slot when it fails. Below the image sit a body and a footer, as on `bf-card`.
120
+
121
+ ## When to use
122
+
123
+ Use `bf-media-card` when a picture is the main content of a card, and especially when it is slow to arrive (an image generated in 30 to 90 seconds) or can fail. Use [bf-card](https://bitforge-ui.web.app/components/card) instead for a card without a picture or with a small one inside its text, [bf-image-input](https://bitforge-ui.web.app/components/image-input) to let the user pick an image, and [bf-skeleton](https://bitforge-ui.web.app/components/skeleton) for a placeholder of other content. Open the full-size picture with [bf-image-viewer](https://bitforge-ui.web.app/components/image-viewer).
124
+
125
+ ## Basic usage
126
+
127
+ Give the card the picture's `src` and `alt` (empty `alt` marks it decorative), and put a title and text in its body. The image area keeps its ratio (`16/9` by default) whether the picture is there or not, so a grid of cards never jumps.
128
+
129
+ ```ts
130
+ import { Component } from '@angular/core';
131
+ import { BfBadgeComponent, BfButtonComponent, BfMediaCardComponent } from '@bitforgehq/ui';
132
+
133
+ /**
134
+ * A media card.
135
+ *
136
+ * The card shows the picture from `src` in a 16:9 area, with a title, a line of text and a footer
137
+ * of a badge and a button under it.
138
+ */
139
+ @Component({
140
+ selector: 'app-example-media-card-basic',
141
+ imports: [BfBadgeComponent, BfButtonComponent, BfMediaCardComponent],
142
+ template: `
143
+ <bf-media-card class="max-w-sm" src="media/skyline.svg" alt="A neon city skyline at dusk">
144
+ <h3 class="text-heading">Starfall</h3>
145
+ <p class="text-body text-text-secondary">
146
+ A roguelike set in a city that falls one floor a night.
147
+ </p>
148
+ <div bfSlot="footer" class="flex items-center justify-between gap-2">
149
+ <bf-badge>Roguelike</bf-badge>
150
+ <bf-button variant="tonal" size="sm">Read the pitch</bf-button>
151
+ </div>
152
+ </bf-media-card>
153
+ `,
154
+ })
155
+ export class MediaCardBasicExampleComponent {}
156
+ ```
157
+
158
+ ## Loading, arriving and failing
159
+
160
+ While the picture loads, the area shows a `bf-skeleton` shimmer. A picture that does not exist yet (it is still being made) has no `src`: set `loading` and the skeleton shows until `src` arrives. `loadingLabel` puts a caption over the skeleton ("Drawing…"); without it, screen readers hear the `loading` label ("Loading") and the area is `aria-busy`.
161
+
162
+ When the picture arrives it fades in over 260 ms (`bfFadeIn`) while the skeleton fades out under it, instead of popping in line by line; under reduced motion it appears at once.
163
+
164
+ A picture that fails to load (its `error` event), or one the app could not make (`failed`), shows a message, `failedLabel` or the `imageLoadFailed` label ("The image could not be loaded"), and the `bfSlot="failed-action"` content under it, such as a Retry button. A new `src` starts over: skeleton, fade and a forgotten load error. To retry the same URL, change it (add a query such as `?retry=2`). `failed` is the app's to clear. `loaded` and `errored` report the picture's events.
165
+
166
+ ```ts
167
+ import { Component, DestroyRef, type WritableSignal, inject, signal } from '@angular/core';
168
+ import { BfButtonComponent, BfMediaCardComponent } from '@bitforgehq/ui';
169
+
170
+ /** One generated picture as the app tracks it. */
171
+ interface Generation {
172
+ readonly src: string;
173
+ readonly generating: boolean;
174
+ readonly failed: boolean;
175
+ }
176
+
177
+ /** How long the simulated generation takes (a real one takes 30 to 90 seconds). */
178
+ const GENERATION_MS = 4000;
179
+
180
+ /**
181
+ * The bf-media-card overview.
182
+ *
183
+ * Live demo of `bf-media-card` with pictures that take a while to exist: the first is being drawn
184
+ * (a timer stands in for the image service) and fades in when it arrives; the second fails, and
185
+ * its Retry button starts it again; the third is already there.
186
+ */
187
+ @Component({
188
+ selector: 'app-example-media-card-overview',
189
+ imports: [BfButtonComponent, BfMediaCardComponent],
190
+ template: `
191
+ <div class="grid gap-4 @md:grid-cols-3">
192
+ <bf-media-card
193
+ alt="A neon city skyline at dusk"
194
+ loadingLabel="Drawing…"
195
+ [src]="starfall().src"
196
+ [loading]="starfall().generating"
197
+ >
198
+ <h2 class="text-heading">Starfall</h2>
199
+ <p class="text-body text-text-secondary">A city that falls one floor a night.</p>
200
+ <bf-button
201
+ bfSlot="footer"
202
+ class="relative z-1 self-start"
203
+ variant="tonal"
204
+ size="sm"
205
+ [disabled]="starfall().generating"
206
+ (clicked)="generate(starfall, 'media/skyline.svg', false)"
207
+ >
208
+ Draw again
209
+ </bf-button>
210
+ </bf-media-card>
211
+
212
+ <bf-media-card
213
+ alt="A harbour under a low sun"
214
+ loadingLabel="Drawing…"
215
+ failedLabel="The picture could not be drawn"
216
+ [src]="tidewalk().src"
217
+ [loading]="tidewalk().generating"
218
+ [failed]="tidewalk().failed"
219
+ >
220
+ <h2 class="text-heading">Tidewalk</h2>
221
+ <p class="text-body text-text-secondary">A harbour town that floods at every tide.</p>
222
+ <bf-button
223
+ bfSlot="failed-action"
224
+ variant="tonal"
225
+ size="sm"
226
+ (clicked)="generate(tidewalk, 'media/harbour.svg', false)"
227
+ >
228
+ Retry
229
+ </bf-button>
230
+ </bf-media-card>
231
+
232
+ <bf-media-card src="media/forest.svg" alt="A pine forest under a pale sun">
233
+ <h2 class="text-heading">Rootbound</h2>
234
+ <p class="text-body text-text-secondary">A forest that rearranges itself at night.</p>
235
+ </bf-media-card>
236
+ </div>
237
+ `,
238
+ })
239
+ export class MediaCardOverviewExampleComponent {
240
+ protected readonly starfall = signal<Generation>({ src: '', generating: true, failed: false });
241
+ protected readonly tidewalk = signal<Generation>({ src: '', generating: true, failed: false });
242
+ private readonly timers = new Set<ReturnType<typeof setTimeout>>();
243
+
244
+ constructor() {
245
+ inject(DestroyRef).onDestroy(() => this.timers.forEach((timer) => clearTimeout(timer)));
246
+ this.generate(this.starfall, 'media/skyline.svg', false);
247
+ // The first attempt at Tidewalk fails, so Retry shows.
248
+ this.generate(this.tidewalk, 'media/harbour.svg', true);
249
+ }
250
+
251
+ /** Starts a (simulated) generation that ends with the picture, or with a failure. */
252
+ protected generate(target: WritableSignal<Generation>, src: string, fails: boolean): void {
253
+ target.set({ src: '', generating: true, failed: false });
254
+ const timer = setTimeout(() => {
255
+ this.timers.delete(timer);
256
+ target.set(
257
+ fails
258
+ ? { src: '', generating: false, failed: true }
259
+ : { src, generating: false, failed: false },
260
+ );
261
+ }, GENERATION_MS);
262
+ this.timers.add(timer);
263
+ }
264
+ }
265
+ ```
266
+
267
+ ## Aspect ratios
268
+
269
+ `aspect` is `16/9` (default), `4/3`, `1/1` or `3/4` (portrait). The picture covers the area and is cropped to it (`object-cover`).
270
+
271
+ ```ts
272
+ import { Component } from '@angular/core';
273
+ import { type BfMediaCardAspect, BfMediaCardComponent } from '@bitforgehq/ui';
274
+
275
+ /**
276
+ * Aspect ratios.
277
+ *
278
+ * `aspect` sets the ratio of the image area: 16:9 (default), 4:3, square and portrait 3:4. The
279
+ * picture covers the area and is cropped to it.
280
+ */
281
+ @Component({
282
+ selector: 'app-example-media-card-aspects',
283
+ imports: [BfMediaCardComponent],
284
+ template: `
285
+ <div class="grid grid-cols-2 items-start gap-4 @lg:grid-cols-4">
286
+ @for (aspect of aspects; track aspect) {
287
+ <bf-media-card src="media/desert.svg" alt="Dunes under an orange sun" [aspect]="aspect">
288
+ <h3 class="text-label">{{ aspect }}</h3>
289
+ </bf-media-card>
290
+ }
291
+ </div>
292
+ `,
293
+ })
294
+ export class MediaCardAspectsExampleComponent {
295
+ protected readonly aspects: readonly BfMediaCardAspect[] = ['16/9', '4/3', '1/1', '3/4'];
296
+ }
297
+ ```
298
+
299
+ ## Clickable and selectable cards
300
+
301
+ A card that opens something follows the `bf-card` pattern: put a `bfAction` link (navigation) or button (an action) in its title. The whole card is then its click target, with the pointer, the hover lift and the focus ring; other controls in it, such as footer buttons, take `class="relative z-1"`.
302
+
303
+ `selectable` adds a select button, a toggle (`aria-pressed`) bound with `[(selected)]`; `changed` emits each change the user makes. Without a `bfAction`, the whole card is that button, and a check disc in the image's top end corner shows the state. With a `bfAction`, the card opens on a click and the round check button in the corner (32 px, 44 px under a coarse pointer) selects, as in a photo library. A selected card has a 2 px primary ring outside its edge and a filled check. The select button is named "Select" followed by `label`, or by the body's text when `label` is empty: set `label` to the card's title.
304
+
305
+ ```ts
306
+ import { Component, computed, signal } from '@angular/core';
307
+ import { RouterLink } from '@angular/router';
308
+ import { BfActionDirective, BfMediaCardComponent } from '@bitforgehq/ui';
309
+
310
+ /** One concept the team can pick. */
311
+ interface Concept {
312
+ readonly id: string;
313
+ readonly title: string;
314
+ readonly src: string;
315
+ readonly alt: string;
316
+ }
317
+
318
+ /**
319
+ * Selectable cards.
320
+ *
321
+ * Pick the concepts to keep: each card is one select button bound with `[(selected)]`, with a
322
+ * check in its corner. The last card also opens its page through a `bfAction` link, so its check
323
+ * becomes a separate round button.
324
+ */
325
+ @Component({
326
+ selector: 'app-example-media-card-selectable',
327
+ imports: [RouterLink, BfActionDirective, BfMediaCardComponent],
328
+ template: `
329
+ <div class="flex flex-col gap-3">
330
+ <div class="grid gap-4 @md:grid-cols-3">
331
+ @for (concept of concepts; track concept.id) {
332
+ <bf-media-card
333
+ selectable
334
+ aspect="4/3"
335
+ [src]="concept.src"
336
+ [alt]="concept.alt"
337
+ [label]="concept.title"
338
+ [selected]="picked().has(concept.id)"
339
+ (changed)="toggle(concept.id, $event)"
340
+ >
341
+ <h3 class="text-heading">{{ concept.title }}</h3>
342
+ </bf-media-card>
343
+ }
344
+ <bf-media-card
345
+ selectable
346
+ aspect="4/3"
347
+ src="media/glacier.svg"
348
+ alt="A glacier under a small sun"
349
+ label="Coldlight"
350
+ [selected]="picked().has('coldlight')"
351
+ (changed)="toggle('coldlight', $event)"
352
+ >
353
+ <h3 class="text-heading">
354
+ <a bfAction routerLink="/components/media-card">Coldlight</a>
355
+ </h3>
356
+ </bf-media-card>
357
+ </div>
358
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
359
+ </div>
360
+ `,
361
+ })
362
+ export class MediaCardSelectableExampleComponent {
363
+ protected readonly concepts: readonly Concept[] = [
364
+ { id: 'starfall', title: 'Starfall', src: 'media/skyline.svg', alt: 'A neon city skyline' },
365
+ { id: 'rootbound', title: 'Rootbound', src: 'media/forest.svg', alt: 'A pine forest' },
366
+ { id: 'tidewalk', title: 'Tidewalk', src: 'media/harbour.svg', alt: 'A harbour at sunset' },
367
+ ];
368
+ protected readonly picked = signal<ReadonlySet<string>>(new Set(['rootbound']));
369
+ protected readonly status = computed(() => {
370
+ const count = this.picked().size;
371
+ return count === 1 ? '1 concept picked.' : `${count} concepts picked.`;
372
+ });
373
+
374
+ protected toggle(id: string, selected: boolean): void {
375
+ this.picked.update((ids) => {
376
+ const next = new Set(ids);
377
+ if (selected) {
378
+ next.add(id);
379
+ } else {
380
+ next.delete(id);
381
+ }
382
+ return next;
383
+ });
384
+ }
385
+ }
386
+ ```
387
+
388
+ ## Desktop
389
+
390
+ A card with a `bfAction` or a stretched select button shows the pointer and lifts to `shadow-floating` while the pointer is over it, and drops back to `shadow-raised` while pressed; both fade on the fast timing (150 ms). The ring around the whole card shows while its action or select button has keyboard focus, not after a click. The corner check button draws its own focus ring. The selection ring fades in and out on the same timing. Text under a stretched action cannot be selected with the mouse; give text that must stay copyable `class="relative z-1"`.
391
+
392
+ ## Theming
393
+
394
+ - Card: `bg-surface`, `text-text`, `rounded-card`, `shadow-raised` (`shadow-floating` on hover); body padding `p-card` (`--spacing-card`).
395
+ - Image area: `bg-surface-sunken`, the top corners `rounded-card`; the skeleton is a `bf-skeleton` rect; captions and messages `text-caption text-text-muted`.
396
+ - Selection: the ring is a 2 px `primary` outline 2 px outside the card; the check disc is `surface` with a `border-strong` edge, `primary` with an `on-primary` check while selected.
397
+ - Focus: `ring-4 ring-ring` around the card (stretched action) or the corner button.
398
+ - Motion: `duration-fast` (150 ms) for hover, press and selection, `duration-slow` (260 ms) for the picture's fade and the skeleton's fade-out, on `ease-md`; none under reduced motion.
399
+
400
+ ## Accessibility
401
+
402
+ The card is a plain `<div>`; the picture is an `<img>` with `alt`. While loading, the image area is `aria-busy="true"` and its caption (or the visually hidden "Loading") is plain text, not a live region, so a grid of loading cards is not announced card by card; the skeleton is hidden from assistive technology. The failure message is plain text too, followed by the `failed-action` content.
403
+
404
+ A clickable card follows the stretched-link pattern of `bf-card`: the only interactive element is the `bfAction` link or button in its title. The select button is a native `<button>` with `aria-pressed`, named by `aria-labelledby` ("Select" and `label`, or the body's text); its check is decorative. Under forced colours the selection ring becomes a system-coloured outline, and the check is drawn with borders, so both stay visible; an unselected card has no outline there.
405
+
406
+ ### Keyboard
407
+
408
+ | Key | Action |
409
+ | ---------------- | ----------------------------------------------------------------------------- |
410
+ | <kbd>Tab</kbd> | Moves to the card's `bfAction` link or button, then to its select button |
411
+ | <kbd>Enter</kbd> | Follows a `bfAction` link, activates a `bfAction` button or toggles selection |
412
+ | <kbd>Space</kbd> | Activates a `bfAction` button or toggles selection; a link ignores it |
413
+
414
+ ## Inputs
415
+
416
+ | Name | Type | Default | Description |
417
+ | -------------- | --------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
418
+ | `src` | `string` | `''` | URL of the image; empty shows the skeleton while `loading`, else an empty area. A new URL starts over: skeleton, fade-in, and a failed load is forgotten. |
419
+ | `alt` | `string` | `''` | Alternative text of the image; empty marks it decorative. |
420
+ | `aspect` | `BfMediaCardAspect` (`'16/9' \| '4/3' \| '1/1' \| '3/4'`) | `'16/9'` | Aspect ratio of the image area: `16/9` (default), `4/3`, `1/1` or `3/4`. |
421
+ | `loading` | `boolean` | `false` | Shows the skeleton while there is no `src` yet (a picture that is still being made). |
422
+ | `failed` | `boolean` | `false` | Shows the failure message (the app could not make the picture); the app clears it. A load error of `src` shows it too, until `src` changes. |
423
+ | `loadingLabel` | `string` | `''` | Caption shown over the skeleton while loading ("Drawing…"); empty shows none and tells screen readers `provideBfLabels()`'s `loading` ("Loading"). |
424
+ | `failedLabel` | `string` | `''` | Failure message; empty uses `provideBfLabels()`'s `imageLoadFailed`. |
425
+ | `selectable` | `boolean` | `false` | Adds a select button (a toggle with `aria-pressed`): the whole card toggles `selected`, or, with a `bfAction` in the card, a round check button in the image's corner does. |
426
+ | `label` | `string` | `''` | The card's name in its select button ("Select Starfall"); empty names it by the body's text. |
427
+ | `selected` | `boolean` | `false` | Whether the card is selected (a ring around it and a filled check); two-way bindable. Drawn only on a `selectable` card. Two-way: `[(selected)]`. |
428
+
429
+ ## Outputs
430
+
431
+ | Name | Payload | Description |
432
+ | --------- | --------- | ----------------------------------------------------------------------- |
433
+ | `changed` | `boolean` | The user selected or cleared the card; emits the new `selected`. |
434
+ | `loaded` | `void` | The image of `src` has loaded. |
435
+ | `errored` | `void` | The image of `src` failed to load (the card shows the failure message). |
436
+
437
+ ## Slots
438
+
439
+ - `failed-action`: An action shown under the failure message, such as a Retry button.
440
+ - `default`: The card body: a title (with an optional bfAction link or button) and text.
441
+ - `footer`: Content below the body, such as chips and buttons; it sits on the card's bottom edge.
442
+
443
+ ## Notes
444
+
445
+ ### Do
446
+
447
+ - Give every meaningful picture an `alt` that says what it shows.
448
+ - Set `loading` (and a `loadingLabel` such as "Drawing…") while a picture is being made, so the card holds its place.
449
+ - Put a Retry button in `bfSlot="failed-action"`, and retry with a changed `src`.
450
+ - Set `label` on a selectable card to its title, so the select button reads "Select Starfall".
451
+
452
+ ### Don't
453
+
454
+ - Don't project your own `<img>` into the body for the main picture; pass `src`, so the card handles loading, fading and failing.
455
+ - Don't put more than one `bfAction` in a card.
456
+ - Don't use `selected` as a highlight without `selectable`: the ring only shows on a selectable card, whose button tells assistive technology the state.
457
+
458
+ ## Migration notes
459
+
460
+ ### 0.5.0
461
+
462
+ New: `bf-media-card`, a card led by a picture (a game pitch, a generated image). The card renders
463
+ the picture itself from `src` and `alt` in a top area at a fixed `aspect` (new type
464
+ `BfMediaCardAspect`: `'16/9'` (default), `'4/3'`, `'1/1'`, `'3/4'`). While it loads, and while
465
+ `src` is empty but `loading` is set (a picture still being made), the area shows a `bf-skeleton`
466
+ shimmer with an optional `loadingLabel` caption ("Drawing…") and is `aria-busy`. The picture fades
467
+ in over 260 ms (`bfFadeIn`) while the skeleton fades out under it; at once under reduced motion.
468
+ A load error, or `failed` set by the app, shows `failedLabel` (default: the new label
469
+ `imageLoadFailed`) and the `bfSlot="failed-action"` content (Retry); a new `src` starts over.
470
+ `loaded` and `errored` report the image's events. A body slot and a `footer` slot follow. A
471
+ `bfAction` link or button in the title makes the whole card clickable, as on `bf-card` (pointer,
472
+ `shadow-floating` hover, focus ring, fading on `duration-fast`). `selectable` adds a toggle
473
+ button (`aria-pressed`) bound with `[(selected)]` (a `model()`), emitting `changed` on each user
474
+ change: the whole card without a `bfAction`, else a round check button in the image's corner; a
475
+ selected card has a 2 px primary ring and a filled check. `label` names it ("Select" + `label`,
476
+ new label `mediaCardSelect`). `@bitforgehq/ui/testing`: new `BfMediaCardHarness`
477
+ (`getState()`, `loadImage()`, `failImage()`, `getLoadingLabel()`, `getFailedMessage()`,
478
+ `isSelected()`, `toggleSelected()`).
@@ -288,6 +288,55 @@ export class MenuButtonSizesExampleComponent {
288
288
  }
289
289
  ```
290
290
 
291
+ ## Full width
292
+
293
+ `fullWidth` stretches the host and its button to the width of their container, as on
294
+ `bf-button`: in a grid of actions, a "More" menu button then fills its cell like the buttons
295
+ beside it. The label and caret stay centred. It does nothing with `iconOnly`, which stays a
296
+ square.
297
+
298
+ ```ts
299
+ import { Component, signal } from '@angular/core';
300
+ import { BfButtonComponent } from '@bitforgehq/ui';
301
+ import {
302
+ BfMenuButtonComponent,
303
+ BfMenuComponent,
304
+ BfMenuItemDirective,
305
+ } from '@bitforgehq/ui/overlay';
306
+
307
+ /**
308
+ * A two-column grid of actions on a card, where a menu button fills its cell like the buttons
309
+ * beside it.
310
+ *
311
+ * `fullWidth` stretches the host and its button, as on `bf-button`.
312
+ */
313
+ @Component({
314
+ selector: 'app-example-menu-button-full-width',
315
+ imports: [BfButtonComponent, BfMenuButtonComponent, BfMenuComponent, BfMenuItemDirective],
316
+ template: `
317
+ <div class="grid max-w-sm grid-cols-2 gap-2">
318
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Opened.')">Open</bf-button>
319
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Duplicated.')"
320
+ >Duplicate</bf-button
321
+ >
322
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Shared.')">Share</bf-button>
323
+ <bf-menu-button [menu]="more" fullWidth>More</bf-menu-button>
324
+ </div>
325
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">{{ result() }}</p>
326
+
327
+ <ng-template #more>
328
+ <bf-menu>
329
+ <button bfMenuItem (triggered)="result.set('Renamed.')">Rename</button>
330
+ <button bfMenuItem (triggered)="result.set('Archived.')">Archive</button>
331
+ </bf-menu>
332
+ </ng-template>
333
+ `,
334
+ })
335
+ export class MenuButtonFullWidthExampleComponent {
336
+ protected readonly result = signal('');
337
+ }
338
+ ```
339
+
291
340
  ## Desktop
292
341
 
293
342
  Hover and focus look like `bf-button` (or `bf-icon-button` with `iconOnly`): the fill, border or text darkens on hover with the pointer cursor, and the focus ring shows on keyboard focus only. A click opens the menu; a second click on the button closes it. Under a fine pointer the visible box is the hit area. Desktop density is the default `md` step; `provideBfDefaults({ size: 'sm' })`, or `{ size: 'sm' }` in a component's `providers`, makes a dense toolbar, table or sidebar ([density guide](https://bitforge-ui.web.app/density)).
@@ -311,6 +360,8 @@ Hover and focus look like `bf-button` (or `bf-icon-button` with `iconOnly`): the
311
360
  last. Closing the menu (`Escape`, `Tab`, an item) returns focus to the button.
312
361
  - The caret is drawn in CSS and adds nothing to the name.
313
362
  - Hit area: at least 32 px, 44 px under a coarse pointer (as `bf-button`).
363
+ - Motion: the menu fades and grows out of the button as it opens, and closes at once. Under
364
+ `prefers-reduced-motion` it only fades in.
314
365
 
315
366
  ### Keyboard
316
367
 
@@ -325,15 +376,16 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
325
376
 
326
377
  ## Inputs
327
378
 
328
- | Name | Type | Default | Description |
329
- | ----------- | ---------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
330
- | `menu` | `TemplateRef<unknown>` | required | The `ng-template` holding the `bf-menu` to open. |
331
- | `variant` | `BfMenuButtonVariant` (`'outline' \| 'text' \| 'tonal'`) | `'outline'` | Visual style: outline (default), text, tonal. |
332
- | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step: sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
333
- | `placement` | `BfMenuPlacement` (`'below-start' \| 'below-end' \| 'above-start' \| 'above-end'`) | `'below-start'` | Where the menu opens against the button; it flips when it would overflow. |
334
- | `label` | `string` | `''` | Accessible name (`aria-label`). Required with `iconOnly`; with a visible label leave it empty or start it with the visible text. |
335
- | `iconOnly` | `boolean` | `false` | Square button holding only the projected icon, like `bf-icon-button`; set `label`. |
336
- | `disabled` | `boolean` | `false` | Disables the button; the menu cannot open. |
379
+ | Name | Type | Default | Description |
380
+ | ----------- | ---------------------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
381
+ | `menu` | `TemplateRef<unknown>` | required | The `ng-template` holding the `bf-menu` to open. |
382
+ | `variant` | `BfMenuButtonVariant` (`'outline' \| 'text' \| 'tonal'`) | `'outline'` | Visual style: outline (default), text, tonal. |
383
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step: sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
384
+ | `placement` | `BfMenuPlacement` (`'below-start' \| 'below-end' \| 'above-start' \| 'above-end'`) | `'below-start'` | Where the menu opens against the button; it flips when it would overflow. |
385
+ | `label` | `string` | `''` | Accessible name (`aria-label`). Required with `iconOnly`; with a visible label leave it empty or start it with the visible text. |
386
+ | `iconOnly` | `boolean` | `false` | Square button holding only the projected icon, like `bf-icon-button`; set `label`. |
387
+ | `fullWidth` | `boolean` | `false` | Stretches the host and the button to the width of their container, as `bf-button`'s `fullWidth`; the label and caret stay centred. Ignored with `iconOnly`. |
388
+ | `disabled` | `boolean` | `false` | Disables the button; the menu cannot open. |
337
389
 
338
390
  ## Outputs
339
391
 
@@ -361,6 +413,12 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
361
413
 
362
414
  ## Migration notes
363
415
 
416
+ ### 0.5.0
417
+
418
+ New `fullWidth` input, as on `bf-button`: the host and its button stretch to the width of their
419
+ container (label and caret stay centred), so app CSS no longer needs to reach into the inner
420
+ `<button>`. Ignored with `iconOnly`.
421
+
364
422
  ### 0.3.0
365
423
 
366
424
  New `bf-menu-button` in `@bitforgehq/ui/overlay`: a native `<button>` that opens a `bf-menu`
@@ -570,6 +570,9 @@ Built for desktop: hovering an item gives it the `surface-muted` fill and the po
570
570
  closing the dialog.
571
571
  - Pointer: rows are at least 32 px tall, 44 px under a coarse pointer. An open submenu stays open
572
572
  while the pointer moves diagonally towards it across other items (CDK menu aim).
573
+ - Motion: the menu grows out of its trigger as it opens (a fade, a slight scale and a 4 px slide,
574
+ 180 ms; a submenu out of its item). It closes at once, because the CDK menu removes it
575
+ immediately. Under `prefers-reduced-motion` it only fades in.
573
576
 
574
577
  ### Keyboard
575
578
 
@@ -619,6 +622,13 @@ From the [APG menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
619
622
 
620
623
  ## Migration notes
621
624
 
625
+ ### 0.5.0
626
+
627
+ The menu animates open: it grows out of its trigger (a fade, a scale from 96 % and a 4 px slide,
628
+ 180 ms, ease-out), a submenu out of its item. It still closes at once, because the CDK menu
629
+ removes it immediately. Under `prefers-reduced-motion` it only fades in. `bf-menu-button` and
630
+ `bf-theme-menu-button` get the same enter.
631
+
622
632
  ### 0.3.1
623
633
 
624
634
  The menu's positions now come from the anchored-panel positioning the popover and tooltip share.
@@ -171,6 +171,8 @@ coarse pointer its hit area grows to 44 px and every option row is at least 44 p
171
171
  order before the input.
172
172
  - Labels, help and error text, `aria-describedby`, `aria-invalid` and `aria-errormessage` follow
173
173
  `bf-select`. `required` sets the native attribute only while no tag is picked.
174
+ - Motion: the list opens and closes with the same short fade, scale and slide as on
175
+ `bf-combobox`, and only fades under `prefers-reduced-motion`.
174
176
 
175
177
  ### Keyboard
176
178
 
@@ -247,6 +249,11 @@ Focuses the input; Signal Forms calls it from `focusBoundControl()`.
247
249
 
248
250
  ## Migration notes
249
251
 
252
+ ### 0.5.0
253
+
254
+ The list panel is exactly as wide as the field. The inner text input no longer shows a square
255
+ border and ring on focus in apps that load `@tailwindcss/forms`.
256
+
250
257
  ### 0.3.1
251
258
 
252
259
  New `bf-multi-combobox` (`BfMultiComboboxComponent<T>`) in `@bitforgehq/ui/overlay`: the