@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,452 @@
1
+ # bf-image-tile
2
+
3
+ Small card for thumbnail grids: a picture and title that act as one target
4
+
5
+ `BfImageTileComponent` from `@bitforgehq/ui` · Data display · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
11
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
12
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
13
+ import { Box, ImagePlus } from 'lucide';
14
+
15
+ /** One model in the grid, as the app tracks it. */
16
+ interface Model {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly src: string;
20
+ readonly alt: string;
21
+ readonly versions: number;
22
+ readonly updated: string;
23
+ }
24
+
25
+ /** How long the simulated render of the last model takes. */
26
+ const RENDER_MS = 3000;
27
+
28
+ /**
29
+ * The bf-image-tile overview.
30
+ *
31
+ * A grid of 3D models, one picture each: a click selects a tile (the ring, and "Selected" read
32
+ * before its title), a badge counts the versions, the last picture is still rendering and fades
33
+ * in when it arrives, and a model without a render yet shows the placeholder. The titles differ
34
+ * in length, yet every tile in a row is as tall.
35
+ */
36
+ @Component({
37
+ selector: 'app-example-image-tile-overview',
38
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
39
+ template: `
40
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
41
+ @for (model of models(); track model.id) {
42
+ <bf-image-tile
43
+ [src]="model.src"
44
+ [alt]="model.alt"
45
+ [loading]="model.id === 'lantern' && rendering()"
46
+ [selected]="model.id === current()"
47
+ >
48
+ <h2>
49
+ <button bfAction type="button" (click)="current.set(model.id)">{{ model.name }}</button>
50
+ </h2>
51
+ <span bfSlot="meta">{{ model.updated }}</span>
52
+ @if (model.versions > 1) {
53
+ <bf-badge bfSlot="badge" variant="info"
54
+ ><bf-icon bfSlot="icon-start" [icon]="box" />{{ model.versions }} versions</bf-badge
55
+ >
56
+ }
57
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
58
+ </bf-image-tile>
59
+ }
60
+ </div>
61
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">
62
+ Selected: {{ currentName() }}
63
+ </p>
64
+ `,
65
+ })
66
+ export class ImageTileOverviewExampleComponent {
67
+ protected readonly box = Box;
68
+ protected readonly imagePlus = ImagePlus;
69
+ protected readonly current = signal('ship');
70
+ protected readonly rendering = signal(true);
71
+ protected readonly models = signal<Model[]>([
72
+ {
73
+ id: 'ship',
74
+ name: 'Harbour ship',
75
+ src: 'media/harbour.svg',
76
+ alt: 'A ship in a harbour',
77
+ versions: 3,
78
+ updated: 'Today',
79
+ },
80
+ {
81
+ id: 'tower',
82
+ name: 'Glacier watchtower with a long rope bridge',
83
+ src: 'media/glacier.svg',
84
+ alt: 'A tower on a glacier',
85
+ versions: 1,
86
+ updated: 'Yesterday',
87
+ },
88
+ { id: 'well', name: 'Desert well', src: '', alt: '', versions: 1, updated: 'Last week' },
89
+ {
90
+ id: 'lantern',
91
+ name: 'Forest lantern',
92
+ src: '',
93
+ alt: 'A lantern in a pine forest',
94
+ versions: 2,
95
+ updated: 'Rendering…',
96
+ },
97
+ ]);
98
+
99
+ constructor() {
100
+ const timer = setTimeout(() => {
101
+ this.models.update((models) =>
102
+ models.map((m) =>
103
+ m.id === 'lantern' ? { ...m, src: 'media/forest.svg', updated: 'Just now' } : m,
104
+ ),
105
+ );
106
+ this.rendering.set(false);
107
+ }, RENDER_MS);
108
+ inject(DestroyRef).onDestroy(() => clearTimeout(timer));
109
+ }
110
+
111
+ protected currentName(): string {
112
+ return this.models().find((m) => m.id === this.current())?.name ?? '';
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## Overview
118
+
119
+ Small card for a grid of thumbnails: mostly picture, at a fixed aspect ratio (square by
120
+ default), with a title and an optional meta line below it or, with `captionPlacement="overlay"`,
121
+ on a soft gradient over its bottom. Without a picture yet it shows a placeholder (an icon and a
122
+ text); while one loads a shimmering skeleton; the picture fades in when it arrives; a failed load
123
+ shows a short message. Make the whole tile one link or button with a `bfAction` in its title, as
124
+ on `bf-card`: it then lifts on hover, zooms the picture slightly and shows the focus ring.
125
+ `selected` draws a ring. The title clamps to two lines and always takes two, so tiles in a grid
126
+ row are as tall whatever their titles.
127
+
128
+ A small card for a grid of thumbnails: 3D models with one render each, a board of reference pictures, a gallery. It is mostly picture, at a fixed aspect ratio (square by default), with a title and an optional meta line below it or over it on a soft gradient. Like `bf-media-card` it owns the whole life of the picture: a placeholder before there is one, a shimmering skeleton while it loads, a fade when it arrives and a short message when it fails.
129
+
130
+ ## When to use
131
+
132
+ Use `bf-image-tile` for many pictures in a grid, where each one opens or selects something and the picture says more than the text. Use [bf-media-card](https://bitforge-ui.web.app/components/media-card) instead when a card also needs a body text, a footer with buttons or a select-several check button, and [bf-card](https://bitforge-ui.web.app/components/card) for a card without a picture. Open the full-size picture with [bf-image-viewer](https://bitforge-ui.web.app/components/image-viewer).
133
+
134
+ ## Basic usage
135
+
136
+ Give each tile the picture's `src` and `alt`, a heading with a `bfAction` button or link as its title, and an optional `bfSlot="meta"` line such as a date or a count. A `bf-badge` in `bfSlot="badge"` sits in the picture's top start corner. `selected` marks the current tile with a ring. The titles differ in length, yet every tile in a row is as tall.
137
+
138
+ ```ts
139
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
140
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
141
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
142
+ import { Box, ImagePlus } from 'lucide';
143
+
144
+ /** One model in the grid, as the app tracks it. */
145
+ interface Model {
146
+ readonly id: string;
147
+ readonly name: string;
148
+ readonly src: string;
149
+ readonly alt: string;
150
+ readonly versions: number;
151
+ readonly updated: string;
152
+ }
153
+
154
+ /** How long the simulated render of the last model takes. */
155
+ const RENDER_MS = 3000;
156
+
157
+ /**
158
+ * The bf-image-tile overview.
159
+ *
160
+ * A grid of 3D models, one picture each: a click selects a tile (the ring, and "Selected" read
161
+ * before its title), a badge counts the versions, the last picture is still rendering and fades
162
+ * in when it arrives, and a model without a render yet shows the placeholder. The titles differ
163
+ * in length, yet every tile in a row is as tall.
164
+ */
165
+ @Component({
166
+ selector: 'app-example-image-tile-overview',
167
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
168
+ template: `
169
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
170
+ @for (model of models(); track model.id) {
171
+ <bf-image-tile
172
+ [src]="model.src"
173
+ [alt]="model.alt"
174
+ [loading]="model.id === 'lantern' && rendering()"
175
+ [selected]="model.id === current()"
176
+ >
177
+ <h2>
178
+ <button bfAction type="button" (click)="current.set(model.id)">{{ model.name }}</button>
179
+ </h2>
180
+ <span bfSlot="meta">{{ model.updated }}</span>
181
+ @if (model.versions > 1) {
182
+ <bf-badge bfSlot="badge" variant="info"
183
+ ><bf-icon bfSlot="icon-start" [icon]="box" />{{ model.versions }} versions</bf-badge
184
+ >
185
+ }
186
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
187
+ </bf-image-tile>
188
+ }
189
+ </div>
190
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">
191
+ Selected: {{ currentName() }}
192
+ </p>
193
+ `,
194
+ })
195
+ export class ImageTileOverviewExampleComponent {
196
+ protected readonly box = Box;
197
+ protected readonly imagePlus = ImagePlus;
198
+ protected readonly current = signal('ship');
199
+ protected readonly rendering = signal(true);
200
+ protected readonly models = signal<Model[]>([
201
+ {
202
+ id: 'ship',
203
+ name: 'Harbour ship',
204
+ src: 'media/harbour.svg',
205
+ alt: 'A ship in a harbour',
206
+ versions: 3,
207
+ updated: 'Today',
208
+ },
209
+ {
210
+ id: 'tower',
211
+ name: 'Glacier watchtower with a long rope bridge',
212
+ src: 'media/glacier.svg',
213
+ alt: 'A tower on a glacier',
214
+ versions: 1,
215
+ updated: 'Yesterday',
216
+ },
217
+ { id: 'well', name: 'Desert well', src: '', alt: '', versions: 1, updated: 'Last week' },
218
+ {
219
+ id: 'lantern',
220
+ name: 'Forest lantern',
221
+ src: '',
222
+ alt: 'A lantern in a pine forest',
223
+ versions: 2,
224
+ updated: 'Rendering…',
225
+ },
226
+ ]);
227
+
228
+ constructor() {
229
+ const timer = setTimeout(() => {
230
+ this.models.update((models) =>
231
+ models.map((m) =>
232
+ m.id === 'lantern' ? { ...m, src: 'media/forest.svg', updated: 'Just now' } : m,
233
+ ),
234
+ );
235
+ this.rendering.set(false);
236
+ }, RENDER_MS);
237
+ inject(DestroyRef).onDestroy(() => clearTimeout(timer));
238
+ }
239
+
240
+ protected currentName(): string {
241
+ return this.models().find((m) => m.id === this.current())?.name ?? '';
242
+ }
243
+ }
244
+ ```
245
+
246
+ ## States
247
+
248
+ Without a `src` the tile shows a placeholder: the `bfSlot="placeholder-icon"` icon and `placeholderLabel`, or the `imageTileEmpty` label ("No image yet"). `loading` shows a `bf-skeleton` shimmer while there is no `src` yet (a render that is still being made), and the same shimmer shows while a `src` loads. The picture fades in over 260 ms (`bfFadeIn`) while the skeleton fades out under it; under reduced motion it appears at once. A picture that fails to load, or one the app could not make (`failed`), shows `failedLabel` or the `imageLoadFailed` label ("The image could not be loaded"). A new `src` starts over and forgets a load error; `failed` is the app's to clear. `loaded` and `errored` report the picture's events.
249
+
250
+ ```ts
251
+ import { Component } from '@angular/core';
252
+ import { BfImageTileComponent } from '@bitforgehq/ui';
253
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
254
+ import { ImagePlus } from 'lucide';
255
+
256
+ /**
257
+ * The four states of a tile side by side.
258
+ *
259
+ * Without `src` the placeholder shows (an icon and "No image yet"); `loading` shows the skeleton;
260
+ * a `src` fades in when it loads; `failed` (or a load error) shows a short message.
261
+ */
262
+ @Component({
263
+ selector: 'app-example-image-tile-states',
264
+ imports: [BfIconComponent, BfImageTileComponent],
265
+ template: `
266
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
267
+ <bf-image-tile>
268
+ <h2>Placeholder</h2>
269
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
270
+ </bf-image-tile>
271
+ <bf-image-tile loading><h2>Loading</h2></bf-image-tile>
272
+ <bf-image-tile src="media/forest.svg" alt="A pine forest under a pale sun">
273
+ <h2>Loaded</h2>
274
+ </bf-image-tile>
275
+ <bf-image-tile failed failedLabel="Render failed"><h2>Failed</h2></bf-image-tile>
276
+ </div>
277
+ `,
278
+ })
279
+ export class ImageTileStatesExampleComponent {
280
+ protected readonly imagePlus = ImagePlus;
281
+ }
282
+ ```
283
+
284
+ ## Caption over the picture
285
+
286
+ `captionPlacement="overlay"` lays the title and the meta line on a soft gradient over the bottom of the picture, so the tile is as tall as its picture and a board of pictures stays dense. The gradient is the `scrim` colour, darkest under the text, and the caption uses the dark theme's text colours, so it reads on light and dark pictures in both themes. `aspect` is `1/1` (default), `4/3`, `16/9` or `3/4` (portrait); the picture covers its area and is cropped to it.
287
+
288
+ ```ts
289
+ import { Component } from '@angular/core';
290
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
291
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
292
+ import { Check } from 'lucide';
293
+
294
+ /** One reference picture on the board. */
295
+ interface Reference {
296
+ readonly id: string;
297
+ readonly title: string;
298
+ readonly source: string;
299
+ readonly src: string;
300
+ readonly reference: boolean;
301
+ }
302
+
303
+ /**
304
+ * A board of reference pictures with the caption over the picture.
305
+ *
306
+ * `captionPlacement="overlay"` lays the title and meta line on a soft gradient over the bottom of
307
+ * a 4:3 picture; each tile is a link, and a check badge marks the chosen references.
308
+ */
309
+ @Component({
310
+ selector: 'app-example-image-tile-overlay',
311
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
312
+ template: `
313
+ <div class="grid grid-cols-1 gap-4 @sm:grid-cols-2 @lg:grid-cols-3">
314
+ @for (item of references; track item.id) {
315
+ <bf-image-tile [src]="item.src" alt="" aspect="4/3" captionPlacement="overlay">
316
+ <h2>
317
+ <a bfAction [href]="'#' + item.id">{{ item.title }}</a>
318
+ </h2>
319
+ <span bfSlot="meta">{{ item.source }}</span>
320
+ @if (item.reference) {
321
+ <bf-badge bfSlot="badge" variant="success" emphasis="strong"
322
+ ><bf-icon bfSlot="icon-start" [icon]="check" />Reference</bf-badge
323
+ >
324
+ }
325
+ </bf-image-tile>
326
+ }
327
+ </div>
328
+ `,
329
+ })
330
+ export class ImageTileOverlayExampleComponent {
331
+ protected readonly check = Check;
332
+ protected readonly references: Reference[] = [
333
+ {
334
+ id: 'skyline',
335
+ title: 'Skyline at dusk',
336
+ source: 'Moodboard',
337
+ src: 'media/skyline.svg',
338
+ reference: true,
339
+ },
340
+ {
341
+ id: 'desert',
342
+ title: 'Dunes in the late afternoon, warm light from the west',
343
+ source: 'Upload',
344
+ src: 'media/desert.svg',
345
+ reference: false,
346
+ },
347
+ {
348
+ id: 'glacier',
349
+ title: 'Glacier',
350
+ source: 'Moodboard',
351
+ src: 'media/glacier.svg',
352
+ reference: true,
353
+ },
354
+ ];
355
+ }
356
+ ```
357
+
358
+ ## Grids of tiles
359
+
360
+ Put the tiles straight into a CSS grid (`grid grid-cols-2 gap-4`, more columns on wider screens). Each tile stretches to its row. The title is clamped to two lines with an ellipsis and always takes the height of two, so a short title and a long one make tiles of the same height; give every tile in a grid a meta line, or none, for the same reason. Below the picture the caption adds 12 px of padding on each side.
361
+
362
+ ## Desktop
363
+
364
+ A tile with a `bfAction` shows the pointer and, while the pointer is over it, lifts by 2 px to `shadow-floating` and zooms its picture by 5 % inside its frame; pressing it sets it back down. The lift runs on the base timing (200 ms) and the zoom on the slow timing (260 ms). Under reduced motion neither moves: only the shadow changes. The focus ring shows around the whole tile while its action has keyboard focus, not after a click. Text under the stretched action cannot be selected with the mouse.
365
+
366
+ ## Theming
367
+
368
+ - Tile: `bg-surface`, `text-text`, `rounded-card`, `shadow-raised` (`shadow-floating` on hover).
369
+ - Picture area: `bg-surface-sunken`; the skeleton is a `bf-skeleton` rect; the placeholder and failure texts are `text-caption text-text-muted`.
370
+ - Caption: the title is `text-label-sm` bold, the meta line `text-caption text-text-muted`; over the picture, a `scrim` gradient (80 % to transparent) with the `.dark` text tokens.
371
+ - Selection: a 2 px `primary` outline 2 px outside the tile.
372
+ - Forced colours: a 1 px `CanvasText` border keeps each tile's edge; the selection ring is `Highlight`.
373
+ - Focus: `ring-4 ring-ring` around the tile.
374
+ - Motion: `duration-base` (200 ms) for the lift, shadow and selection ring, `duration-slow` (260 ms) for the zoom and the picture's fade, on `ease-md`; no movement under reduced motion.
375
+
376
+ ## Accessibility
377
+
378
+ The tile is a plain `<div>`; the picture is an `<img>` with `alt`. When the title names the picture well enough, as in a board of references, leave `alt` empty so it is not read twice. A clickable tile follows the stretched-link pattern of `bf-card`: the only interactive element is the `bfAction` link or button in its title, and its name is the title. A selected tile starts that name with the `imageTileSelected` label, so a screen reader hears "Selected, Harbour ship"; without a `bfAction` the same text is read in the tile. While loading, the picture area is `aria-busy="true"` with a visually hidden "Loading", not a live region, so a grid of loading tiles is not announced tile by tile. Under forced colours the selection ring becomes a `Highlight` outline and the focus indicator a system outline around the tile.
379
+
380
+ ### Keyboard
381
+
382
+ | Key | Action |
383
+ | ---------------- | ---------------------------------------------------------- |
384
+ | <kbd>Tab</kbd> | Moves to the tile's `bfAction` link or button |
385
+ | <kbd>Enter</kbd> | Follows a `bfAction` link or activates a `bfAction` button |
386
+ | <kbd>Space</kbd> | Activates a `bfAction` button; a link ignores it |
387
+
388
+ ## Inputs
389
+
390
+ | Name | Type | Default | Description |
391
+ | ------------------ | --------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
392
+ | `src` | `string` | `''` | URL of the picture; empty shows the placeholder, or the skeleton while `loading`. A new URL starts over: skeleton, fade-in, and a failed load is forgotten. |
393
+ | `alt` | `string` | `''` | Alternative text of the picture; empty marks it decorative (the title names the tile). |
394
+ | `aspect` | `BfImageTileAspect` (`'1/1' \| '4/3' \| '16/9' \| '3/4'`) | `'1/1'` | Aspect ratio of the picture: `1/1` (default), `4/3`, `16/9` or `3/4`. |
395
+ | `captionPlacement` | `BfImageTileCaptionPlacement` (`'below' \| 'overlay'`) | `'below'` | Where the title and the meta line sit: `below` the picture (default) or `overlay`, on a gradient over its bottom. |
396
+ | `loading` | `boolean` | `false` | Shows the skeleton while there is no `src` yet (a picture that is still being made). |
397
+ | `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. |
398
+ | `selected` | `boolean` | `false` | Marks the tile as the selected one: a ring around it, and "Selected" read before its title. |
399
+ | `placeholderLabel` | `string` | `''` | Placeholder text while there is no picture; empty uses `provideBfLabels()`'s `imageTileEmpty` ("No image yet"). |
400
+ | `failedLabel` | `string` | `''` | Failure message; empty uses `provideBfLabels()`'s `imageLoadFailed`. |
401
+
402
+ ## Outputs
403
+
404
+ | Name | Payload | Description |
405
+ | --------- | ------- | ------------------------------------------------------------------------- |
406
+ | `loaded` | `void` | The picture of `src` has loaded. |
407
+ | `errored` | `void` | The picture of `src` failed to load (the tile shows the failure message). |
408
+
409
+ ## Slots
410
+
411
+ - `placeholder-icon`: The placeholder's icon, shown with its text while there is no picture.
412
+ - `badge`: A `bf-badge` in the picture's top start corner.
413
+ - `default`: The title: a heading holding the tile's `bfAction` link or button.
414
+ - `meta`: A short line under the title, such as a date or a count.
415
+
416
+ ## Notes
417
+
418
+ ### Do
419
+
420
+ - Use a heading for the title, at the level that fits the page, with the `bfAction` inside it.
421
+ - Set `loading` while a picture is being made, so the tile holds its place.
422
+ - Keep the meta line short: a date, a count, a source.
423
+
424
+ ### Don't
425
+
426
+ - Don't put more than one `bfAction` in a tile, or other buttons in it; use a `bf-media-card` for a card with actions.
427
+ - Don't project your own `<img>` for the picture; pass `src`, so the tile handles loading, fading and failing.
428
+ - Don't rely on the ring alone to mark the selected tile in your own text: the tile already tells assistive technology.
429
+
430
+ ## Migration notes
431
+
432
+ ### 0.5.0
433
+
434
+ New: `bf-image-tile`, a small card for grids of thumbnails (3D models with one render each, a
435
+ board of reference pictures). It is mostly picture, at a fixed `aspect` (new type
436
+ `BfImageTileAspect`: `'1/1'` (default), `'4/3'`, `'16/9'`, `'3/4'`), with the title (default slot,
437
+ a heading holding a `bfAction` link or button) and an optional `bfSlot="meta"` line below the
438
+ picture, or with `captionPlacement="overlay"` (new type `BfImageTileCaptionPlacement`: `'below'`
439
+ (default), `'overlay'`) on a soft `scrim` gradient over its bottom. States share
440
+ `bf-media-card`'s logic: without `src` a placeholder (`bfSlot="placeholder-icon"` and
441
+ `placeholderLabel`, default: the new label `imageTileEmpty`, "No image yet"); `loading`, and a
442
+ `src` that is loading, show a `bf-skeleton` shimmer (`aria-busy`); the picture fades in over
443
+ 260 ms; a load error or `failed` shows `failedLabel` (default `imageLoadFailed`). `loaded` and
444
+ `errored` report the picture's events. A `bfAction` makes the whole tile one link or button: the
445
+ pointer, a 2 px lift to `shadow-floating` and a 5 % zoom of the picture on hover (no movement
446
+ under reduced motion), and the focus ring. `selected` draws a 2 px primary ring and starts the
447
+ action's name with the new label `imageTileSelected` ("Selected, Harbour ship"). `bfSlot="badge"`
448
+ puts a `bf-badge` in the picture's top start corner. The title clamps to two lines and always
449
+ takes two, so tiles in a grid row are as tall. `@bitforgehq/ui/testing`: new
450
+ `BfImageTileHarness` (`getTitle()`, `getMeta()`, `getState()`, `getSrc()`, `getAlt()`,
451
+ `getPlaceholderText()`, `getFailedMessage()`, `isSelected()`, `isClickable()`, `click()`,
452
+ `loadImage()`, `failImage()`).