@bitforgehq/ui 0.5.0-next.3 → 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.
- package/CHANGELOG.md +47 -0
- package/README.md +3 -3
- package/ai/MANIFEST.index.json +9 -0
- package/ai/MANIFEST.json +265 -5
- package/ai/REFERENCE.index.json +28 -0
- package/ai/REFERENCE.json +172 -3
- package/ai/UI_GUIDE.md +14 -5
- package/ai/components/badge.md +41 -1
- package/ai/components/image-tile.md +452 -0
- package/ai/components/menu-button.md +65 -9
- package/ai/components/pagination.md +59 -11
- package/ai/reference/bf-image-tile-aspect.md +28 -0
- package/ai/reference/bf-image-tile-caption-placement.md +27 -0
- package/ai/reference/bf-image-tile-harness.md +70 -0
- package/ai/reference/bf-labels.md +3 -1
- package/ai/reference/bf-pagination-position-label.md +22 -0
- package/ai/reference/provide-bf-labels.md +2 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +26 -3
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +59 -1
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +576 -55
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +779 -9
- package/llms.txt +5 -0
- package/package.json +1 -1
- package/types/bitforgehq-ui-overlay.d.ts +11 -0
- package/types/bitforgehq-ui-testing.d.ts +83 -0
- package/types/bitforgehq-ui.d.ts +239 -12
|
@@ -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()`).
|
|
@@ -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)).
|
|
@@ -327,15 +376,16 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
|
|
|
327
376
|
|
|
328
377
|
## Inputs
|
|
329
378
|
|
|
330
|
-
| Name | Type | Default | Description
|
|
331
|
-
| ----------- | ---------------------------------------------------------------------------------- | --------------- |
|
|
332
|
-
| `menu` | `TemplateRef<unknown>` | required | The `ng-template` holding the `bf-menu` to open.
|
|
333
|
-
| `variant` | `BfMenuButtonVariant` (`'outline' \| 'text' \| 'tonal'`) | `'outline'` | Visual style: outline (default), text, tonal.
|
|
334
|
-
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step: sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`controlSize`, `md` by default).
|
|
335
|
-
| `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.
|
|
336
|
-
| `label` | `string` | `''` | Accessible name (`aria-label`). Required with `iconOnly`; with a visible label leave it empty or start it with the visible text.
|
|
337
|
-
| `iconOnly` | `boolean` | `false` | Square button holding only the projected icon, like `bf-icon-button`; set `label`.
|
|
338
|
-
| `
|
|
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. |
|
|
339
389
|
|
|
340
390
|
## Outputs
|
|
341
391
|
|
|
@@ -363,6 +413,12 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
|
|
|
363
413
|
|
|
364
414
|
## Migration notes
|
|
365
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
|
+
|
|
366
422
|
### 0.3.0
|
|
367
423
|
|
|
368
424
|
New `bf-menu-button` in `@bitforgehq/ui/overlay`: a native `<button>` that opens a `bf-menu`
|
|
@@ -42,7 +42,9 @@ export class PaginationOverviewExampleComponent {
|
|
|
42
42
|
Page navigation for a long list or table: Previous and Next buttons around the page numbers,
|
|
43
43
|
the current one marked, the hidden runs shown as an ellipsis. `compact` shows "Page 3 of 12"
|
|
44
44
|
between the arrows instead, for narrow screens. The page count comes from `pageCount`, or from
|
|
45
|
-
`total` and `pageSize`; the consumer shows the rows of `page`.
|
|
45
|
+
`total` and `pageSize`; the consumer shows the rows of `page`. A compact pagination with its own
|
|
46
|
+
`positionLabel`, `previousLabel` and `nextLabel` steps through anything numbered, such as the
|
|
47
|
+
versions of a model ("‹ v3 of 7 ›").
|
|
46
48
|
|
|
47
49
|
Pagination splits a long list or table into pages and lets the user move between them: Previous and Next buttons around the page numbers, the current page marked, the first and the last page always in reach, and an ellipsis for each run of pages in between. `bf-pagination` holds only the page number. You bind `[(page)]` and show that page's rows, sliced on the client or fetched from the server. It works the same in both shells: in a desktop web app and in the Capacitor shell.
|
|
48
50
|
|
|
@@ -89,6 +91,41 @@ export class PaginationCompactExampleComponent {
|
|
|
89
91
|
}
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
## Stepping through versions
|
|
95
|
+
|
|
96
|
+
A compact pagination also steps through anything else that is numbered, such as the versions of a model: "‹ v3 of 7 ›". `positionLabel` takes a `BfPaginationPositionLabel`, a function from the page and the count to the position text (`` (page, count) => `v${page} of ${count}` ``); unset, the text comes from `pageOf`. `previousLabel` and `nextLabel` name the arrows ("Previous version") where "Previous page" would be wrong, and `label` names the navigation. Use it instead of a [bf-segmented](https://bitforge-ui.web.app/components/segmented) control once there are more than four versions; the arrows are disabled at the ends, as on any pagination.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { Component, signal } from '@angular/core';
|
|
100
|
+
import { BfPaginationComponent, type BfPaginationPositionLabel } from '@bitforgehq/ui';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A compact pager that steps through the versions of a model, "‹ v3 of 7 ›".
|
|
104
|
+
*
|
|
105
|
+
* `positionLabel` writes the position text; `previousLabel` and `nextLabel` name the arrows.
|
|
106
|
+
*/
|
|
107
|
+
@Component({
|
|
108
|
+
selector: 'app-example-pagination-versions',
|
|
109
|
+
imports: [BfPaginationComponent],
|
|
110
|
+
template: `
|
|
111
|
+
<bf-pagination
|
|
112
|
+
compact
|
|
113
|
+
size="sm"
|
|
114
|
+
label="Versions"
|
|
115
|
+
previousLabel="Previous version"
|
|
116
|
+
nextLabel="Next version"
|
|
117
|
+
[positionLabel]="versionOf"
|
|
118
|
+
[pageCount]="7"
|
|
119
|
+
[(page)]="version"
|
|
120
|
+
/>
|
|
121
|
+
`,
|
|
122
|
+
})
|
|
123
|
+
export class PaginationVersionsExampleComponent {
|
|
124
|
+
readonly version = signal(3);
|
|
125
|
+
protected readonly versionOf: BfPaginationPositionLabel = (page, count) => `v${page} of ${count}`;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
92
129
|
## With a table
|
|
93
130
|
|
|
94
131
|
Put the pagination under the table it pages, aligned to the inline end or centred, and show only the current page's rows: slice them in a `computed()` from `page`, or fetch the page from the server in a `httpResource()` keyed on it. When the user sorts or filters, set `page` back to 1. The [table](https://bitforge-ui.web.app/components/table) page has a paged example.
|
|
@@ -123,16 +160,19 @@ At the first and the last page the arrow is disabled with `aria-disabled="true"`
|
|
|
123
160
|
|
|
124
161
|
## Inputs
|
|
125
162
|
|
|
126
|
-
| Name
|
|
127
|
-
|
|
|
128
|
-
| `pageCount`
|
|
129
|
-
| `total`
|
|
130
|
-
| `pageSize`
|
|
131
|
-
| `siblings`
|
|
132
|
-
| `compact`
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
163
|
+
| Name | Type | Default | Description |
|
|
164
|
+
| --------------- | ----------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `pageCount` | `number` | `0` | The number of pages; wins over `total` and `pageSize` when above 0. |
|
|
166
|
+
| `total` | `number` | `0` | The number of items, used with `pageSize` while `pageCount` is 0. |
|
|
167
|
+
| `pageSize` | `number` | `10` | Items per page, used with `total` while `pageCount` is 0. |
|
|
168
|
+
| `siblings` | `number` | `1` | How many pages to show on each side of the current one. |
|
|
169
|
+
| `compact` | `boolean` | `false` | Shows "Page 3 of 12" between the arrows instead of the page buttons, for narrow screens. |
|
|
170
|
+
| `positionLabel` | `BfPaginationPositionLabel \| undefined` | `undefined` | Writes the compact position text from the page and the count, such as "v3 of 7"; unset uses `provideBfLabels()`'s `pageOf` ("Page 3 of 12"). Only used with `compact`. |
|
|
171
|
+
| `previousLabel` | `string` | `''` | Accessible name of the previous button, such as "Previous version"; empty falls back to `provideBfLabels()`'s `previousPage` ("Previous page"). |
|
|
172
|
+
| `nextLabel` | `string` | `''` | Accessible name of the next button, such as "Next version"; empty falls back to `provideBfLabels()`'s `nextPage` ("Next page"). |
|
|
173
|
+
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Button height step: sm 32, md 40, lg 48 px (44 px hit area under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
|
|
174
|
+
| `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `pagination` ("Pagination"). |
|
|
175
|
+
| `page` | `number` | `1` | The current page, 1-based; two-way bindable. The buttons set it; show the rows of this page. Two-way: `[(page)]`. |
|
|
136
176
|
|
|
137
177
|
## Methods
|
|
138
178
|
|
|
@@ -159,6 +199,14 @@ Goes to a page: sets `page` when it is another page within 1…count; else does
|
|
|
159
199
|
|
|
160
200
|
## Migration notes
|
|
161
201
|
|
|
202
|
+
### 0.5.0
|
|
203
|
+
|
|
204
|
+
New `positionLabel` input (new type `BfPaginationPositionLabel`, `(page, count) => string`) that
|
|
205
|
+
writes a compact pagination's position text, such as "v3 of 7"; unset it is the `pageOf` label as
|
|
206
|
+
before. New `previousLabel` and `nextLabel` inputs name the arrows ("Previous version"); empty
|
|
207
|
+
they fall back to `previousPage` and `nextPage`. Together they make a compact pager for stepping
|
|
208
|
+
through versions.
|
|
209
|
+
|
|
162
210
|
### 0.3.1
|
|
163
211
|
|
|
164
212
|
New `bf-pagination` in `@bitforgehq/ui`, page navigation for both shells: a `nav` (named by
|