@bitforgehq/ui 0.4.0 → 0.5.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +303 -0
  2. package/README.md +12 -12
  3. package/ai/MANIFEST.index.json +65 -2
  4. package/ai/MANIFEST.json +2221 -737
  5. package/ai/REFERENCE.index.json +161 -0
  6. package/ai/REFERENCE.json +1336 -27
  7. package/ai/UI_GUIDE.md +61 -20
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/chat-composer.md +357 -0
  10. package/ai/components/chat-message.md +251 -0
  11. package/ai/components/chat-thread.md +521 -0
  12. package/ai/components/checkbox.md +7 -0
  13. package/ai/components/combobox.md +12 -0
  14. package/ai/components/date-input.md +9 -0
  15. package/ai/components/datetime-input.md +2 -0
  16. package/ai/components/field.md +10 -0
  17. package/ai/components/file-input.md +498 -0
  18. package/ai/components/header.md +1 -1
  19. package/ai/components/image-input.md +368 -0
  20. package/ai/components/image-viewer.md +309 -0
  21. package/ai/components/media-card.md +478 -0
  22. package/ai/components/menu-button.md +2 -0
  23. package/ai/components/menu.md +10 -0
  24. package/ai/components/multi-combobox.md +7 -0
  25. package/ai/components/nav-group.md +10 -3
  26. package/ai/components/nav-item.md +11 -2
  27. package/ai/components/number-input.md +2 -7
  28. package/ai/components/popover.md +13 -0
  29. package/ai/components/radio-group.md +6 -0
  30. package/ai/components/segmented.md +14 -2
  31. package/ai/components/select.md +100 -42
  32. package/ai/components/sidebar-toggle.md +16 -9
  33. package/ai/components/sidebar.md +41 -13
  34. package/ai/components/tab-group.md +18 -2
  35. package/ai/components/table.md +17 -2
  36. package/ai/components/theme-menu-button.md +1 -0
  37. package/ai/components/time-input.md +2 -0
  38. package/ai/components/toolbar.md +7 -18
  39. package/ai/reference/bf-chat-composer-harness.md +64 -0
  40. package/ai/reference/bf-chat-message-role.md +27 -0
  41. package/ai/reference/bf-chat-message-status.md +28 -0
  42. package/ai/reference/bf-fade-in-directive.md +18 -0
  43. package/ai/reference/bf-file-input-harness.md +67 -0
  44. package/ai/reference/bf-file-input-variant.md +27 -0
  45. package/ai/reference/bf-image-input-harness.md +67 -0
  46. package/ai/reference/bf-image-input-shape.md +27 -0
  47. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  48. package/ai/reference/bf-image-viewer-harness.md +87 -0
  49. package/ai/reference/bf-image-viewer-image.md +25 -0
  50. package/ai/reference/bf-image-viewer-options.md +33 -0
  51. package/ai/reference/bf-image-viewer-origin.md +23 -0
  52. package/ai/reference/bf-image-viewer-ref.md +55 -0
  53. package/ai/reference/bf-image-viewer-service.md +32 -0
  54. package/ai/reference/bf-labels.md +105 -75
  55. package/ai/reference/bf-media-card-aspect.md +28 -0
  56. package/ai/reference/bf-media-card-harness.md +66 -0
  57. package/ai/reference/bf-motion-list-directive.md +22 -0
  58. package/ai/reference/bf-prose-directive.md +16 -0
  59. package/ai/reference/bf-rejected-file-reason.md +28 -0
  60. package/ai/reference/bf-rejected-file.md +28 -0
  61. package/ai/reference/bf-select-harness.md +14 -5
  62. package/ai/reference/bf-select-option.md +2 -0
  63. package/ai/reference/bf-size.md +3 -0
  64. package/ai/reference/bf_table_sort_select.md +23 -0
  65. package/ai/reference/provide-bf-labels.md +10 -3
  66. package/ai/reference/provide-bf-select.md +20 -0
  67. package/bin/bf-ui.js +1 -0
  68. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  69. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  70. package/fesm2022/bitforgehq-ui-testing.mjs +356 -10
  71. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  72. package/fesm2022/bitforgehq-ui.mjs +7531 -3989
  73. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  74. package/llms-full.txt +4151 -130
  75. package/llms.txt +31 -1
  76. package/package.json +1 -1
  77. package/theme.css +60 -0
  78. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  79. package/types/bitforgehq-ui-testing.d.ts +434 -6
  80. package/types/bitforgehq-ui.d.ts +1473 -182
@@ -0,0 +1,309 @@
1
+ # bf-image-viewer
2
+
3
+ Full-screen image viewer that zooms from a thumbnail and steps through a set
4
+
5
+ `BfImageViewerComponent` from `@bitforgehq/ui/overlay` · Overlays · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import {
11
+ Component,
12
+ ElementRef,
13
+ Injector,
14
+ type TemplateRef,
15
+ inject,
16
+ signal,
17
+ viewChild,
18
+ viewChildren,
19
+ } from '@angular/core';
20
+ import { BfActionDirective, BfButtonComponent, BfMediaCardComponent } from '@bitforgehq/ui';
21
+ import {
22
+ type BfImageViewerActionsContext,
23
+ type BfImageViewerImage,
24
+ BfImageViewerService,
25
+ } from '@bitforgehq/ui/overlay';
26
+
27
+ /**
28
+ * The bf-image-viewer overview.
29
+ *
30
+ * Live demo of `BfImageViewerService`: a grid of `bf-media-card` thumbnails opens the viewer on
31
+ * the clicked one, zooming out of its picture. The arrow keys or the side buttons step through the set, and closing zooms
32
+ * back into the thumbnail of the image shown. The "Use as cover" action closes the viewer and
33
+ * keeps the pick.
34
+ */
35
+ @Component({
36
+ selector: 'app-example-image-viewer-overview',
37
+ imports: [BfActionDirective, BfButtonComponent, BfMediaCardComponent],
38
+ template: `
39
+ <div class="flex flex-col gap-3">
40
+ <div class="grid grid-cols-2 gap-3 @md:grid-cols-5">
41
+ @for (image of images; track image.src; let i = $index) {
42
+ <bf-media-card #card aspect="1/1" [src]="image.src" [alt]="image.label">
43
+ <button bfAction type="button" class="text-label text-start" (click)="open(i)">
44
+ {{ image.caption ?? 'Untitled' }}
45
+ </button>
46
+ </bf-media-card>
47
+ }
48
+ </div>
49
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
50
+ </div>
51
+
52
+ <ng-template #actions let-image let-ref="ref">
53
+ <bf-button variant="primary" size="sm" (clicked)="useAsCover(image); ref.close()">
54
+ Use as cover
55
+ </bf-button>
56
+ </ng-template>
57
+ `,
58
+ })
59
+ export class ImageViewerOverviewExampleComponent {
60
+ private readonly viewer = inject(BfImageViewerService);
61
+ private readonly injector = inject(Injector);
62
+
63
+ protected readonly images: readonly BfImageViewerImage[] = [
64
+ { src: 'media/skyline.svg', label: 'Neon skyline at dusk', caption: 'Starfall, concept 1' },
65
+ { src: 'media/forest.svg', label: 'Pine forest under a pale sun', caption: 'Rootbound' },
66
+ { src: 'media/harbour.svg', label: 'Harbour at sunset', caption: 'Tidewalk' },
67
+ { src: 'media/desert.svg', label: 'Dunes under an orange sun' },
68
+ { src: 'media/glacier.svg', label: 'Glacier under a small sun', caption: 'Coldlight' },
69
+ ];
70
+ protected readonly status = signal('No cover picked.');
71
+
72
+ private readonly cards = viewChildren<BfMediaCardComponent, ElementRef<HTMLElement>>('card', {
73
+ read: ElementRef,
74
+ });
75
+ private readonly actions =
76
+ viewChild.required<TemplateRef<BfImageViewerActionsContext>>('actions');
77
+
78
+ protected open(index: number): void {
79
+ this.viewer.open({
80
+ images: this.images,
81
+ index,
82
+ // Any image's thumbnail, so closing zooms into the one shown.
83
+ origin: (i) => this.cards()[i]?.nativeElement.querySelector('img'),
84
+ actions: this.actions(),
85
+ injector: this.injector,
86
+ });
87
+ }
88
+
89
+ protected useAsCover(image: BfImageViewerImage): void {
90
+ this.status.set(`Cover: ${image.label}.`);
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Overview
96
+
97
+ The full-screen image viewer (lightbox) `BfImageViewerService.open()` renders: one image large
98
+ over a dark scrim, with a counter, previous and next buttons, a close button, an optional
99
+ caption and actions. It opens with a zoom out of the clicked thumbnail and steps with a
100
+ cross-fade. Apps do not place it in templates; open it with the service.
101
+
102
+ A full-screen image viewer (a lightbox) from `@bitforgehq/ui/overlay` (needs `@angular/cdk`, no Capacitor). `BfImageViewerService.open()` shows one image large over a dark scrim and steps through a set: previous and next buttons, the arrow keys, a counter ("2 of 5"), the caption and optional actions. It zooms out of the thumbnail that was clicked and back into it, and returns a `BfImageViewerRef`.
103
+
104
+ ## When to use
105
+
106
+ Use `BfImageViewerService` to look at a picture larger than the page shows it, such as a card's cover, a gallery or the results of an image generator, and to step through a set of them. Use [BfDialogService](https://bitforge-ui.web.app/api/bf-dialog-service) instead for a picture that comes with a form or a decision, and [bf-media-card](https://bitforge-ui.web.app/components/media-card) for the thumbnails themselves.
107
+
108
+ ## Basic usage
109
+
110
+ Call `open()` with the `images` (each a `src`, a `label`, which becomes the image's `alt`, and an optional `caption`) and the `index` to show first. Pass the clicked thumbnail as `origin`, or a function that returns the thumbnail of any image (`(index) => thumbnails[index]`), so closing after a few steps zooms into the right one. Actions under the image (a download link, a "Use this one" button) come from an `actions` template, whose context is `{ $implicit: image, index, ref }`; inject `BfImageViewerRef` or use `ref` to close or step from it.
111
+
112
+ ```ts
113
+ import {
114
+ Component,
115
+ ElementRef,
116
+ Injector,
117
+ type TemplateRef,
118
+ inject,
119
+ signal,
120
+ viewChild,
121
+ viewChildren,
122
+ } from '@angular/core';
123
+ import { BfActionDirective, BfButtonComponent, BfMediaCardComponent } from '@bitforgehq/ui';
124
+ import {
125
+ type BfImageViewerActionsContext,
126
+ type BfImageViewerImage,
127
+ BfImageViewerService,
128
+ } from '@bitforgehq/ui/overlay';
129
+
130
+ /**
131
+ * The bf-image-viewer overview.
132
+ *
133
+ * Live demo of `BfImageViewerService`: a grid of `bf-media-card` thumbnails opens the viewer on
134
+ * the clicked one, zooming out of its picture. The arrow keys or the side buttons step through the set, and closing zooms
135
+ * back into the thumbnail of the image shown. The "Use as cover" action closes the viewer and
136
+ * keeps the pick.
137
+ */
138
+ @Component({
139
+ selector: 'app-example-image-viewer-overview',
140
+ imports: [BfActionDirective, BfButtonComponent, BfMediaCardComponent],
141
+ template: `
142
+ <div class="flex flex-col gap-3">
143
+ <div class="grid grid-cols-2 gap-3 @md:grid-cols-5">
144
+ @for (image of images; track image.src; let i = $index) {
145
+ <bf-media-card #card aspect="1/1" [src]="image.src" [alt]="image.label">
146
+ <button bfAction type="button" class="text-label text-start" (click)="open(i)">
147
+ {{ image.caption ?? 'Untitled' }}
148
+ </button>
149
+ </bf-media-card>
150
+ }
151
+ </div>
152
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
153
+ </div>
154
+
155
+ <ng-template #actions let-image let-ref="ref">
156
+ <bf-button variant="primary" size="sm" (clicked)="useAsCover(image); ref.close()">
157
+ Use as cover
158
+ </bf-button>
159
+ </ng-template>
160
+ `,
161
+ })
162
+ export class ImageViewerOverviewExampleComponent {
163
+ private readonly viewer = inject(BfImageViewerService);
164
+ private readonly injector = inject(Injector);
165
+
166
+ protected readonly images: readonly BfImageViewerImage[] = [
167
+ { src: 'media/skyline.svg', label: 'Neon skyline at dusk', caption: 'Starfall, concept 1' },
168
+ { src: 'media/forest.svg', label: 'Pine forest under a pale sun', caption: 'Rootbound' },
169
+ { src: 'media/harbour.svg', label: 'Harbour at sunset', caption: 'Tidewalk' },
170
+ { src: 'media/desert.svg', label: 'Dunes under an orange sun' },
171
+ { src: 'media/glacier.svg', label: 'Glacier under a small sun', caption: 'Coldlight' },
172
+ ];
173
+ protected readonly status = signal('No cover picked.');
174
+
175
+ private readonly cards = viewChildren<BfMediaCardComponent, ElementRef<HTMLElement>>('card', {
176
+ read: ElementRef,
177
+ });
178
+ private readonly actions =
179
+ viewChild.required<TemplateRef<BfImageViewerActionsContext>>('actions');
180
+
181
+ protected open(index: number): void {
182
+ this.viewer.open({
183
+ images: this.images,
184
+ index,
185
+ // Any image's thumbnail, so closing zooms into the one shown.
186
+ origin: (i) => this.cards()[i]?.nativeElement.querySelector('img'),
187
+ actions: this.actions(),
188
+ injector: this.injector,
189
+ });
190
+ }
191
+
192
+ protected useAsCover(image: BfImageViewerImage): void {
193
+ this.status.set(`Cover: ${image.label}.`);
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Stepping and the ref
199
+
200
+ The ref's `index` is a signal of the shown image. `next()` and `previous()` wrap around at both ends, `goTo(index)` jumps (a negative index counts from the end), and `close()` closes; `afterClosed` resolves once the viewer has left the page, and `closed()` turns true. A viewer of one image shows no counter and no step buttons.
201
+
202
+ ```ts
203
+ import { Component, Injector, inject, signal } from '@angular/core';
204
+ import { BfButtonComponent } from '@bitforgehq/ui';
205
+ import { BfImageViewerService } from '@bitforgehq/ui/overlay';
206
+
207
+ /**
208
+ * One image, no thumbnail.
209
+ *
210
+ * Without an `origin` the viewer fades in and out; with a single image it shows no counter and
211
+ * no step buttons. `afterClosed` resolves once it has gone.
212
+ */
213
+ @Component({
214
+ selector: 'app-example-image-viewer-single',
215
+ imports: [BfButtonComponent],
216
+ template: `
217
+ <div class="flex flex-col items-start gap-3">
218
+ <bf-button variant="tonal" (clicked)="open()">View the key art</bf-button>
219
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
220
+ </div>
221
+ `,
222
+ })
223
+ export class ImageViewerSingleExampleComponent {
224
+ private readonly viewer = inject(BfImageViewerService);
225
+ private readonly injector = inject(Injector);
226
+ protected readonly status = signal('');
227
+
228
+ protected async open(): Promise<void> {
229
+ const ref = this.viewer.open({
230
+ images: [
231
+ { src: 'media/glacier.svg', label: 'Glacier under a small sun', caption: 'Key art' },
232
+ ],
233
+ injector: this.injector,
234
+ });
235
+ this.status.set('Viewer open.');
236
+ await ref.afterClosed;
237
+ this.status.set('Viewer closed.');
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## Motion
243
+
244
+ The viewer opens with a zoom: the image's frame starts on the thumbnail's box, cropped as the thumbnail crops it, and grows to its place in 260 ms on the overlay curve (`--ease-ios`), while the scrim and the controls fade in. With no `origin`, or one that is off screen, it fades in instead. Closing zooms back into the thumbnail of the shown image when it is still on screen, else fades out. Stepping cross-fades the images. Under `prefers-reduced-motion` the viewer only fades, in 150 ms, and the images appear without a fade.
245
+
246
+ ## Loading and failing
247
+
248
+ While a large image loads, the viewer shows its thumbnail (the `origin` image, when there is one) with a spinner over it, and the frame already has the thumbnail's aspect ratio; the image fades in over it when it arrives (`bfFadeIn`). An image that fails to load shows the `imageLoadFailed` label ("The image could not be loaded") in its place.
249
+
250
+ ## Desktop
251
+
252
+ Built for desktop and touch alike: the image fills the space between the top bar (counter and close) and the caption row, at its own aspect ratio. The previous and next buttons sit on the image's sides and the close button at the top end, each a `tonal` `bf-icon-button` that follows `provideBfDefaults()`; their hover tint and focus ring are the button's. A click on the scrim or beside the image closes the viewer; a click on the image, the caption or the actions does not. The page behind it is inert and does not scroll.
253
+
254
+ ## Theming
255
+
256
+ - The viewer is a `.dark` sub-tree, so its text and buttons take their dark-mode tokens in both colour schemes; the scrim is `bg-scrim/90`.
257
+ - Counter `text-label`, caption `text-body`, failure message `text-body text-text-secondary`.
258
+ - Buttons: `tonal` `bf-icon-button`s with lucide `ChevronLeft`, `ChevronRight` (mirrored under `dir="rtl"`) and `X`; the spinner is a `lg` `bf-spinner`.
259
+ - Motion: `OVERLAY_DURATION` (260 ms) and `OVERLAY_EASING` from the overlay core, `OVERLAY_REDUCED_DURATION` (150 ms) under reduced motion; the images fade on `duration-slow`.
260
+
261
+ ## Accessibility
262
+
263
+ The viewer is a modal `role="dialog"` named by `ariaLabel` or the `imageViewer` label ("Image viewer"), with `aria-modal="true"`. On open, focus moves to the close button and is trapped in the viewer; on close it returns to the element that opened it. The page behind it is `inert`. Every image has its `label` as `alt`, the step buttons are named by `imageViewerPrevious` and `imageViewerNext`, and after each step a polite live region announces the position and the label ("3 of 5, Harbour at sunset"); the counter itself is the `imageViewerPosition` label. The service is a root singleton: pass `injector: inject(Injector)` so a feature-level `provideBfLabels()` names its parts.
264
+
265
+ ### Keyboard
266
+
267
+ | Key | Action |
268
+ | ------------------------------------------------ | ----------------------------------------------------------- |
269
+ | <kbd>ArrowRight</kbd> / <kbd>ArrowLeft</kbd> | Next / previous image, wrapping (swapped under `dir="rtl"`) |
270
+ | <kbd>Home</kbd> / <kbd>End</kbd> | First / last image |
271
+ | <kbd>Escape</kbd> | Closes the viewer |
272
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Next / previous control, wrapping inside the viewer |
273
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Activates the focused button |
274
+
275
+ ## Notes
276
+
277
+ ### Do
278
+
279
+ - Give every image a `label` that says what it shows; it is all a screen reader gets of the picture.
280
+ - Pass `origin` (a function for a gallery), so the image visibly comes from and returns to its thumbnail.
281
+ - Keep actions to one or two buttons that act on the shown image.
282
+
283
+ ### Don't
284
+
285
+ - Don't open the viewer for a picture that already fills the page.
286
+ - Don't put a form in the actions; open a dialog for that.
287
+
288
+ ## Migration notes
289
+
290
+ ### 0.5.0
291
+
292
+ New: `BfImageViewerService` in `@bitforgehq/ui/overlay`, a full-screen image viewer (lightbox).
293
+ `open({ images: [{ src, label, caption? }], index?, origin?, actions?, ariaLabel?,
294
+ closeOnNavigation?, injector? })` (new types `BfImageViewerOptions`, `BfImageViewerImage`,
295
+ `BfImageViewerOrigin`, `BfImageViewerActionsContext`) renders `bf-image-viewer` over a dark scrim
296
+ and returns a `BfImageViewerRef` (`index()`, `next()`, `previous()`, `goTo()`, `close()`,
297
+ `afterClosed`, `closed()`). Previous and next buttons, the arrow keys (mirrored under rtl), Home
298
+ and End step through the set, wrapping at both ends; a counter shows "2 of 5" and a polite live
299
+ region announces each step. It is a modal dialog: focus moves to its close button and is trapped,
300
+ `Escape`, the close button and a click on the scrim close it, the page behind is `inert`, and
301
+ focus returns to the opener. The `actions` template (context `{ $implicit: image, index, ref }`)
302
+ sits beside the caption. It zooms out of `origin` (the clicked thumbnail, or a function returning
303
+ any image's thumbnail) with a 260 ms FLIP on the overlay curve and zooms back into it on close
304
+ when it is on screen, else fades; images cross-fade on a step; reduced motion only fades. While a
305
+ large image loads it shows the thumbnail and a spinner, then fades the image in. New labels
306
+ `imageViewer`, `imageViewerPrevious`, `imageViewerNext` and `imageViewerPosition`.
307
+ `@bitforgehq/ui/testing`: new `BfImageViewerHarness` (`getIndex()`, `getCount()`,
308
+ `getCounterText()`, `getSrc()`, `getAlt()`, `getCaption()`, `next()`, `previous()`,
309
+ `pressKey()`, `close()`, `loadImage()`, `failImage()`, `isLoaded()`, `hasFailed()`).