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