@bitforgehq/ui 0.3.3 → 0.5.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +394 -0
  2. package/README.md +14 -14
  3. package/ai/MANIFEST.index.json +101 -2
  4. package/ai/MANIFEST.json +2630 -746
  5. package/ai/REFERENCE.index.json +196 -0
  6. package/ai/REFERENCE.json +1625 -140
  7. package/ai/UI_GUIDE.md +83 -21
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/badge.md +39 -2
  10. package/ai/components/chat-composer.md +357 -0
  11. package/ai/components/chat-message.md +251 -0
  12. package/ai/components/chat-thread.md +521 -0
  13. package/ai/components/checkbox.md +7 -0
  14. package/ai/components/combobox.md +12 -0
  15. package/ai/components/date-input.md +9 -0
  16. package/ai/components/datetime-input.md +2 -0
  17. package/ai/components/field.md +10 -0
  18. package/ai/components/file-input.md +498 -0
  19. package/ai/components/header.md +1 -1
  20. package/ai/components/image-input.md +368 -0
  21. package/ai/components/image-viewer.md +309 -0
  22. package/ai/components/list-item.md +105 -9
  23. package/ai/components/media-card.md +478 -0
  24. package/ai/components/menu-button.md +2 -0
  25. package/ai/components/menu.md +10 -0
  26. package/ai/components/multi-combobox.md +7 -0
  27. package/ai/components/nav-group.md +10 -3
  28. package/ai/components/nav-item.md +11 -2
  29. package/ai/components/number-input.md +2 -7
  30. package/ai/components/popover.md +13 -0
  31. package/ai/components/radio-group.md +6 -0
  32. package/ai/components/segmented.md +14 -2
  33. package/ai/components/select.md +100 -42
  34. package/ai/components/sidebar-toggle.md +16 -9
  35. package/ai/components/sidebar.md +41 -13
  36. package/ai/components/step.md +112 -0
  37. package/ai/components/steps.md +124 -0
  38. package/ai/components/tab-group.md +18 -2
  39. package/ai/components/table.md +115 -4
  40. package/ai/components/theme-menu-button.md +1 -0
  41. package/ai/components/time-input.md +2 -0
  42. package/ai/components/timeline-event.md +144 -0
  43. package/ai/components/timeline.md +201 -0
  44. package/ai/components/toggle.md +11 -1
  45. package/ai/components/toolbar.md +7 -18
  46. package/ai/reference/bf-action-directive.md +3 -1
  47. package/ai/reference/bf-badge-emphasis.md +27 -0
  48. package/ai/reference/bf-chat-composer-harness.md +64 -0
  49. package/ai/reference/bf-chat-message-role.md +27 -0
  50. package/ai/reference/bf-chat-message-status.md +28 -0
  51. package/ai/reference/bf-fade-in-directive.md +18 -0
  52. package/ai/reference/bf-file-input-harness.md +67 -0
  53. package/ai/reference/bf-file-input-variant.md +27 -0
  54. package/ai/reference/bf-image-input-harness.md +67 -0
  55. package/ai/reference/bf-image-input-shape.md +27 -0
  56. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  57. package/ai/reference/bf-image-viewer-harness.md +87 -0
  58. package/ai/reference/bf-image-viewer-image.md +25 -0
  59. package/ai/reference/bf-image-viewer-options.md +33 -0
  60. package/ai/reference/bf-image-viewer-origin.md +23 -0
  61. package/ai/reference/bf-image-viewer-ref.md +55 -0
  62. package/ai/reference/bf-image-viewer-service.md +32 -0
  63. package/ai/reference/bf-labels.md +105 -74
  64. package/ai/reference/bf-media-card-aspect.md +28 -0
  65. package/ai/reference/bf-media-card-harness.md +66 -0
  66. package/ai/reference/bf-motion-list-directive.md +22 -0
  67. package/ai/reference/bf-prose-directive.md +16 -0
  68. package/ai/reference/bf-rejected-file-reason.md +28 -0
  69. package/ai/reference/bf-rejected-file.md +28 -0
  70. package/ai/reference/bf-select-harness.md +14 -5
  71. package/ai/reference/bf-select-option.md +2 -0
  72. package/ai/reference/bf-size.md +3 -0
  73. package/ai/reference/bf-step-heading-level.md +28 -0
  74. package/ai/reference/bf-table-align.md +1 -1
  75. package/ai/reference/bf-table-column-align.md +33 -0
  76. package/ai/reference/bf-table-column.md +7 -7
  77. package/ai/reference/bf-table-harness.md +12 -0
  78. package/ai/reference/bf-timeline-tone.md +31 -0
  79. package/ai/reference/bf-toggle-harness.md +55 -0
  80. package/ai/reference/bf_table_sort_select.md +23 -0
  81. package/ai/reference/provide-bf-labels.md +10 -3
  82. package/ai/reference/provide-bf-select.md +20 -0
  83. package/bin/bf-ui.js +1 -0
  84. package/dark-ink.css +124 -0
  85. package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
  86. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  87. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  88. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  89. package/fesm2022/bitforgehq-ui-testing.mjs +406 -11
  90. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  91. package/fesm2022/bitforgehq-ui.mjs +11149 -6957
  92. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  93. package/llms-full.txt +5219 -141
  94. package/llms.txt +40 -1
  95. package/package.json +2 -1
  96. package/theme.css +84 -0
  97. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  98. package/types/bitforgehq-ui-testing.d.ts +510 -7
  99. package/types/bitforgehq-ui.d.ts +1757 -186
@@ -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()`).
@@ -311,6 +311,8 @@ Hover and focus look like `bf-button` (or `bf-icon-button` with `iconOnly`): the
311
311
  last. Closing the menu (`Escape`, `Tab`, an item) returns focus to the button.
312
312
  - The caret is drawn in CSS and adds nothing to the name.
313
313
  - Hit area: at least 32 px, 44 px under a coarse pointer (as `bf-button`).
314
+ - Motion: the menu fades and grows out of the button as it opens, and closes at once. Under
315
+ `prefers-reduced-motion` it only fades in.
314
316
 
315
317
  ### Keyboard
316
318
 
@@ -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
@@ -72,7 +72,7 @@ Put the group's links in its default slot and its name in `label`. Groups stack
72
72
 
73
73
  ## In a collapsed sidebar
74
74
 
75
- While the sidebar is collapsed to an icon rail the heading is hidden visually (`sr-only`), so only the icons show, and it still names the group for screen readers.
75
+ While the sidebar is collapsed to an icon rail the heading's row closes and the text fades out (on `--bf-duration-layout`, at once under reduced motion), so only the icons show, and it still names the group for screen readers. The heading stays on one line and truncates, so its height never changes while the sidebar's width moves.
76
76
 
77
77
  ```ts
78
78
  import { Component } from '@angular/core';
@@ -84,7 +84,7 @@ import { Layers, LayoutList, PanelTop, SquareStack } from 'lucide';
84
84
  /**
85
85
  * Groups in a collapsed sidebar.
86
86
  *
87
- * The headings are hidden visually and still name their groups for screen readers; the items
87
+ * The headings close and fade out (at once under reduced motion) and still name their groups for screen readers; the items
88
88
  * show their icons only.
89
89
  */
90
90
  @Component({
@@ -146,7 +146,7 @@ The group itself is static; its links carry the hover, pressed and focus styles
146
146
 
147
147
  ## Accessibility
148
148
 
149
- The host has `role="group"` and `aria-labelledby` pointing at its heading, so a screen reader announces the group's name when focus enters it ("Workspace, group"). The heading is text, not a heading element, so it does not add to the page outline. Collapsed, the heading is `sr-only` and keeps naming the group.
149
+ The host has `role="group"` and `aria-labelledby` pointing at its heading, so a screen reader announces the group's name when focus enters it ("Workspace, group"). The heading is text, not a heading element, so it does not add to the page outline. Collapsed, the heading is closed to no height and transparent, but stays in the accessibility tree and keeps naming the group.
150
150
 
151
151
  ## Inputs
152
152
 
@@ -172,6 +172,13 @@ The host has `role="group"` and `aria-labelledby` pointing at its heading, so a
172
172
 
173
173
  ## Migration notes
174
174
 
175
+ ### 0.5.0
176
+
177
+ Collapsed, the heading's row closes and its text fades out on `--bf-duration-layout` instead of
178
+ switching to `sr-only`; it stays in the accessibility tree and still names the group. The heading
179
+ stays on one line and truncates (it wrapped before), so its height does not change while the
180
+ width moves.
181
+
175
182
  ### 0.3.1
176
183
 
177
184
  New `bf-nav-group`: a titled group of sidebar links, `role="group"` named by its small heading,
@@ -109,7 +109,7 @@ The router sets `aria-current="page"` on the current page's link (`routerLinkAct
109
109
 
110
110
  ## Collapsed
111
111
 
112
- In a collapsed `bf-sidebar` the item shows only its icon in a square of its height step. The label stays in the DOM as screen-reader text, so the link's name does not change, and becomes the link's `title` (the tooltip on hover, kept up to date when the label changes); your own `title` comes back when the sidebar expands. The badge becomes a small dot on the icon's corner and is still announced. Give every item an icon, or it shows nothing while collapsed. For a styled tooltip that also shows on keyboard focus, add `bfTooltip` from `@bitforgehq/ui/overlay` with the label's text while collapsed (an empty text while expanded shows none) and `bfTooltipPlacement="end"` (see [bf-tooltip](https://bitforge-ui.web.app/components/tooltip)).
112
+ In a collapsed `bf-sidebar` the item shows only its icon in a square of its height step: the row keeps its layout, its start padding centres a 20 px icon, and the label fades out while the sidebar narrows, so the icon never moves. The label stays in the DOM, so the link's name does not change, and becomes the link's `title` (the tooltip on hover, kept up to date when the label changes); your own `title` comes back when the sidebar expands. The badge pill shrinks away while a small dot grows on the icon's corner, on the same timing, and the count is still announced. Give every item an icon, or it shows nothing while collapsed. For a styled tooltip that also shows on keyboard focus, add `bfTooltip` from `@bitforgehq/ui/overlay` with the label's text while collapsed (an empty text while expanded shows none) and `bfTooltipPlacement="end"` (see [bf-tooltip](https://bitforge-ui.web.app/components/tooltip)).
113
113
 
114
114
  ## Sizes
115
115
 
@@ -161,11 +161,12 @@ Hover gives a `surface-muted` fill with the `text` colour and the pointer; press
161
161
  - Current page (`aria-current="page"`): `primary-soft` with `primary-strong`, hover `primary-soft-hover`, pressed `primary-soft-pressed`; under forced colours a `LinkText` outline.
162
162
  - Shape: `rounded-control`; label `font-bold`, `text-label-xs` / `text-label-sm` / `text-label` by size.
163
163
  - Badge: `primary` with `on-primary`, `text-micro`; collapsed, a `primary` dot (`CanvasText` under forced colours).
164
+ - Motion: in a sidebar, the padding, the label and the badge move on `--bf-duration-layout` with `ease-md` (none under reduced motion); hover and press colours fade over 150 ms.
164
165
  - Focus: `ring` (4 px).
165
166
 
166
167
  ## Accessibility
167
168
 
168
- The item is the consumer's native `<a>`: exposed as a link, reached with Tab and followed with Enter, its name being its label (plus the badge announcement). The current page carries `aria-current="page"`, which screen readers announce; the fill is not the only cue, as an outline marks it under forced colours. The icon slot is decorative (a `bf-icon` is `aria-hidden` by default). Collapsed, the label is visually hidden but still the name, and `title` adds a pointer tooltip only; the badge dot is `aria-hidden`, its count announced through the screen-reader text. In Safari, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise).
169
+ The item is the consumer's native `<a>`: exposed as a link, reached with Tab and followed with Enter, its name being its label (plus the badge announcement). The current page carries `aria-current="page"`, which screen readers announce; the fill is not the only cue, as an outline marks it under forced colours. The icon slot is decorative (a `bf-icon` is `aria-hidden` by default). Collapsed, the label is transparent and clipped but still the name, and `title` adds a pointer tooltip only; the badge dot is `aria-hidden`, its count announced through the screen-reader text. In Safari, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise).
169
170
 
170
171
  ### Keyboard
171
172
 
@@ -200,6 +201,14 @@ The item is the consumer's native `<a>`: exposed as a link, reached with Tab and
200
201
 
201
202
  ## Migration notes
202
203
 
204
+ ### 0.5.0
205
+
206
+ In a collapsing sidebar the item no longer switches to a centred square: the row keeps its layout
207
+ and its start padding moves so the icon ends centred in the rail without moving, while the label
208
+ fades out (it stays the accessible name; it is no longer `sr-only`). The count pill shrinks away
209
+ while the dot (new element, `[data-badge-dot]`) grows in on the same timing. Each item clips
210
+ what does not fit the rail. No animation under reduced motion or in a top nav.
211
+
203
212
  ### 0.3.1
204
213
 
205
214
  New `a[bfNavItem]`: one destination of a `bf-sidebar` or `bf-top-nav` on the app's own
@@ -165,13 +165,8 @@ The control works in Reactive Forms through Angular's Signal Forms interop (no `
165
165
  ```ts
166
166
  import { Component } from '@angular/core';
167
167
  import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
168
- import {
169
- BfButtonComponent,
170
- BfCheckboxComponent,
171
- BfInputComponent,
172
- BfSelectComponent,
173
- type BfSelectOption,
174
- } from '@bitforgehq/ui';
168
+ import { BfButtonComponent, BfCheckboxComponent, BfInputComponent } from '@bitforgehq/ui';
169
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
175
170
  import { BfNumberInputComponent } from '@bitforgehq/ui/number-input';
176
171
 
177
172
  /**
@@ -207,6 +207,9 @@ in the Capacitor shell a `bf-sheet` suits phone screens better.
207
207
  `Shift+Tab` before the first control closes it onto the trigger.
208
208
  - `Escape` goes to the popover first: inside a dialog, the first `Escape` closes the popover and
209
209
  the next one the dialog.
210
+ - Motion: the panel grows out of the trigger as it opens (a fade, a slight scale and a 4 px slide,
211
+ 180 ms) and shrinks back as it closes (130 ms). Focus and `aria-expanded` change at once; the
212
+ closing panel is inert while it fades. Under `prefers-reduced-motion` it only fades.
210
213
 
211
214
  ### Keyboard
212
215
 
@@ -238,6 +241,16 @@ in the Capacitor shell a `bf-sheet` suits phone screens better.
238
241
 
239
242
  ## Migration notes
240
243
 
244
+ ### 0.5.0
245
+
246
+ The panel animates open and closed. It grows out of the trigger (a fade, a scale from 96 % and a
247
+ 4 px slide away from the trigger, 180 ms, ease-out; transform origin on the trigger's corner,
248
+ following a flip above or below) and shrinks back on close (130 ms). Focus, `aria-expanded` and
249
+ `bfPopoverClosed` still change at once; the closing panel stays in the overlay container, inert
250
+ and click-through, until its exit has played, and opening again during the exit replaces it. Under
251
+ `prefers-reduced-motion` it only fades. Where the Web Animations API is missing or stubbed
252
+ (jsdom), the panel is still removed at once.
253
+
241
254
  ### 0.3.1
242
255
 
243
256
  New `[bfPopoverTriggerFor]` (`BfPopoverTriggerDirective`, exported as `bfPopoverTrigger`) and the