@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/llms-full.txt CHANGED
@@ -147,10 +147,10 @@ Render library components only in the browser, for example in a `@defer` block o
147
147
  route. Support will be decided before 1.0.
148
148
 
149
149
  The one server-side check is a render smoke, not a support test: the library repository's
150
- `tools/ssr-smoke.ts` renders 49 fixtures with `renderApplication()` (every root widget, `bf-icon`,
150
+ `tools/ssr-smoke.ts` renders 50 fixtures with `renderApplication()` (every root widget, `bf-icon`,
151
151
  `bf-number-input`, `bf-select` from `/overlay`, a `/charts` line chart, a `/mobile` page with
152
152
  `provideBfShell()`, `bf-page`, `bf-header` and `bf-content`, `bf-tabs` with `bf-stack-outlet` under
153
- the router, and an `/overlay` dialog), and all of them render without an error. 31 of them render
153
+ the router, and an `/overlay` dialog), and all of them render without an error. 32 of them render
154
154
  identically twice. The other 18 number their ids differently on every render, from a module-level
155
155
  counter, so they could not hydrate: `bf-app-frame`, `bf-calendar`, `bf-checkbox`, `bf-file-input`, `bf-image-input`, `bf-input`,
156
156
  `bf-radio-group`, `bf-segmented`, `bf-select`, `bf-sidebar`, `bf-tab-group`, `bf-table`,
@@ -254,7 +254,7 @@ you can guess a binding without looking it up:
254
254
  spinner, tab bar, progress or icon); every component that renders a native input, select or
255
255
  textarea (checkbox and toggle included) also takes `ariaLabel` and `ariaLabelledby` for when no
256
256
  visible `label` is shown; a built-in sub-control's name is `<part>Label` (`dismissLabel`,
257
- `closeLabel`, `actionLabel`); `alt` only on `bf-avatar` and `bf-media-card`.
257
+ `closeLabel`, `actionLabel`); `alt` only on `bf-avatar`, `bf-media-card` and `bf-image-tile`.
258
258
  - Types: every union or object type of an input, model, output, `Bf…Options`/`Bf…Config` property
259
259
  or an interface they name is a named type exported from the same entry
260
260
  (`BfToggleLabelPosition`, `BfRefresherRefreshEvent`, `BfDialogRole`, …), so type your own state
@@ -416,6 +416,11 @@ protected readonly colorScheme = inject(BfColorSchemeService);
416
416
  when it fails; retry with a changed `src`. Never project your own `<img>` for the main picture.
417
417
  Clickable like `bf-card` (a `bfAction` in the title); `selectable` with `[(selected)]` and
418
418
  `label` for a pick-several grid.
419
+ - **Thumbnail grids:** `bf-image-tile` (`[src]`, `alt`, `aspect`, `captionPlacement="overlay"`
420
+ for the title on a gradient over the picture), with a `bfAction` link or button in its title
421
+ heading, `bfSlot="meta"` for a date or count, `bfSlot="badge"` for a `bf-badge` and
422
+ `[selected]` for the current one. Put tiles straight into a CSS grid: they stretch to the row
423
+ and keep two title lines, so a row lines up; give every tile a meta line or none.
419
424
  - **Rows on a card:** `<bf-list [inset]="false" divided class="-mx-card">`, flush to the card's
420
425
  edges with lines between the rows.
421
426
  - **Theme switch:** `<bf-theme-switch [(value)]="colorScheme.scheme" />` (or
@@ -427,6 +432,9 @@ protected readonly colorScheme = inject(BfColorSchemeService);
427
432
  - **Paging and loading:** `<bf-pagination [(page)]="page" [total]="count" [pageSize]="20">` under a
428
433
  `bf-table` (the app slices its rows); `bf-skeleton` (`shape` text/rect/circle, `lines`) as the
429
434
  placeholder while data loads, inside a region with `aria-busy="true"`.
435
+ - **Stepping through versions:** a compact `bf-pagination` with `[pageCount]`,
436
+ `[positionLabel]="(page, count) => …"` ("v3 of 7") and `previousLabel`/`nextLabel`; past four
437
+ options it replaces a `bf-segmented`.
430
438
  - **Dates and times:** `<bf-calendar [(value)]="date">` for an inline month view;
431
439
  `bf-date-input`, `bf-time-input` and `bf-datetime-input` (`/overlay`) for a field: typed parts
432
440
  in locale order plus a calendar or time-list popover (`picker="none"` for typed only, `native`
@@ -1027,7 +1035,7 @@ describe('bf-button in a consumer app (Vitest)', () => {
1027
1035
 
1028
1036
  ### 6. Drive a form widget through its harness
1029
1037
 
1030
- The entry ships CDK harnesses for the widgets whose markup is not obvious to drive: `BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfMenuButtonHarness`, `BfDesktopToastHarness`, `BfTableHarness`, `BfToggleHarness`, `BfPaginationHarness` `BfTabGroupHarness`, `BfCalendarHarness`, `BfFileInputHarness`, `BfImageInputHarness`, `BfMediaCardHarness`, `BfChatComposerHarness` and `BfChartHarness`. `with({ label })` finds a widget by its label. Load an in-page widget with `TestbedHarnessEnvironment.loader(fixture)`. jsdom has no `URL.createObjectURL`, so after `addFile()` `BfImageInputHarness.hasPreview()` stays false: assert on the bound value, or stub `URL.createObjectURL` to read the preview.
1038
+ The entry ships CDK harnesses for the widgets whose markup is not obvious to drive: `BfSelectHarness`, `BfRadioGroupHarness`, `BfSegmentedHarness`, `BfDialogHarness`, `BfMenuHarness`, `BfMenuItemHarness`, `BfMenuButtonHarness`, `BfDesktopToastHarness`, `BfTableHarness`, `BfToggleHarness`, `BfPaginationHarness` `BfTabGroupHarness`, `BfCalendarHarness`, `BfFileInputHarness`, `BfImageInputHarness`, `BfMediaCardHarness`, `BfImageTileHarness`, `BfChatComposerHarness` and `BfChartHarness`. `with({ label })` finds a widget by its label. Load an in-page widget with `TestbedHarnessEnvironment.loader(fixture)`. jsdom has no `URL.createObjectURL`, so after `addFile()` `BfImageInputHarness.hasPreview()` stays false: assert on the bound value, or stub `URL.createObjectURL` to read the preview.
1031
1039
 
1032
1040
  `select-harness.spec.ts`:
1033
1041
 
@@ -1097,7 +1105,7 @@ describe('BfSelectHarness in a consumer app (Vitest)', () => {
1097
1105
 
1098
1106
  ### 7. Drive a dialog from the document root
1099
1107
 
1100
- Dialogs, menus and toasts render in the CDK overlay container at the end of `<body>`, outside the fixture. Load their harnesses with `TestbedHarnessEnvironment.documentRootLoader(fixture)`; `BfImageViewerHarness` drives the image viewer the same way. jsdom never loads images: `BfMediaCardHarness` and `BfImageViewerHarness` fire the `load` and `error` events with `loadImage()` and `failImage()`.
1108
+ Dialogs, menus and toasts render in the CDK overlay container at the end of `<body>`, outside the fixture. Load their harnesses with `TestbedHarnessEnvironment.documentRootLoader(fixture)`; `BfImageViewerHarness` drives the image viewer the same way. jsdom never loads images: `BfMediaCardHarness`, `BfImageTileHarness` and `BfImageViewerHarness` fire the `load` and `error` events with `loadImage()` and `failImage()`.
1101
1109
 
1102
1110
  `dialog-harness.spec.ts`:
1103
1111
 
@@ -1289,6 +1297,7 @@ notes.
1289
1297
  | `div[bfDescriptionItem]` | `@bitforgehq/ui` | One label and value pair of a description list, label as term, content as value |
1290
1298
  | `bf-description-list` | `@bitforgehq/ui` | Native description list of label-value pairs, as divided rows or a tile grid |
1291
1299
  | `bf-donut-chart` | `@bitforgehq/ui/charts` | Donut chart of the parts of a whole, with a legend, tooltip and centre slot |
1300
+ | `bf-image-tile` | `@bitforgehq/ui` | Small card for thumbnail grids: a picture and title that act as one target |
1292
1301
  | `bf-line-chart` | `@bitforgehq/ui/charts` | Line chart of series over categories, with tooltip, legend and data table |
1293
1302
  | `bf-list` | `@bitforgehq/ui` | Container for list rows, inset by default with rounded corners on a warm background |
1294
1303
  | `bf-list-item` | `@bitforgehq/ui` | List row with leading slot, title, subtitle, trailing slot and an optional chevron |
@@ -3917,6 +3926,37 @@ import { BfBadgeComponent } from '@bitforgehq/ui';
3917
3926
  export class BadgeStrongExampleComponent {}
3918
3927
  ```
3919
3928
 
3929
+ ## Icon
3930
+
3931
+ An icon in the `icon-start` slot sits before the text, at 12 px whatever the `bf-icon`'s own `size`, 4 px from the text, and the start padding tightens to match. Use it to repeat what the text says at a glance, a check on "Reference" or a cube on "3 versions", never instead of the text. A count pill does not show it.
3932
+
3933
+ ```ts
3934
+ import { Component } from '@angular/core';
3935
+ import { BfBadgeComponent } from '@bitforgehq/ui';
3936
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
3937
+ import { Box, Check, Clock } from 'lucide';
3938
+
3939
+ /** Badges with a leading icon: the icon repeats the meaning, the text still says it. */
3940
+ @Component({
3941
+ selector: 'app-example-badge-icon',
3942
+ imports: [BfBadgeComponent, BfIconComponent],
3943
+ template: `
3944
+ <div class="flex flex-wrap items-center gap-2">
3945
+ <bf-badge variant="success"><bf-icon bfSlot="icon-start" [icon]="check" />Reference</bf-badge>
3946
+ <bf-badge variant="info"><bf-icon bfSlot="icon-start" [icon]="box" />3 versions</bf-badge>
3947
+ <bf-badge variant="warning" emphasis="strong"
3948
+ ><bf-icon bfSlot="icon-start" [icon]="clock" />Queued</bf-badge
3949
+ >
3950
+ </div>
3951
+ `,
3952
+ })
3953
+ export class BadgeIconExampleComponent {
3954
+ protected readonly check = Check;
3955
+ protected readonly box = Box;
3956
+ protected readonly clock = Clock;
3957
+ }
3958
+ ```
3959
+
3920
3960
  ## Count pill
3921
3961
 
3922
3962
  Setting `count` switches to the compact primary pill. Values above 99 render as "99+"; zero renders as "0".
@@ -3980,11 +4020,12 @@ Consumes tokens by variant, background then text:
3980
4020
  - `info`: `info-soft` / `info` (≥ 4.5:1 in both modes; it is not derived from the primary, so a custom brand colour leaves it unchanged)
3981
4021
  - Count pill (`count` set): `primary` / `on-primary`, ignoring `variant`
3982
4022
  - Strong (`emphasis="strong"`, label and count pill): `<variant>-strong` / `on-<variant>-strong` (`neutral-strong`, `success-strong`, `warning-strong`, `danger-strong`, `info-strong`, each with its `on-` text token; ≥ 4.5:1 in both modes and the fill ≥ 3:1 on `bg`, `surface` and `surface-sunken`); `primary` uses `primary` / `on-primary`. An app can retune `--color-danger-strong` and `--color-on-danger-strong` (and the others) in `@theme` and `.dark` like any status token
4023
+ - Icon (`icon-start`): drawn in the text colour (`currentColor`), so it follows the variant and stays visible in forced colours
3983
4024
  - Shape (label and count pill): `rounded-control` (`--radius-control`, `9999px` by default).
3984
4025
 
3985
4026
  ## Accessibility
3986
4027
 
3987
- A badge is a plain `<span>` with no role or ARIA attributes; its text is read as part of the surrounding content, so pair it with a label or context that says what it means rather than relying on color alone. A count pill with `countLabel` carries that context itself: a visually hidden (`sr-only`) span after the number, read as one phrase with it ("99+ unread messages"). A strong badge is louder, not more meaningful: its text ("Critical", "Kritisch") still says the state. In forced colours (Windows High Contrast) the fills give way to the system colours, so a strong badge draws a `CanvasText` outline and still reads as a filled pill next to a soft one.
4028
+ A badge is a plain `<span>` with no role or ARIA attributes; a `bf-icon` in `icon-start` is decorative (`aria-hidden`) unless it has a `label`, so the text alone carries the meaning; its text is read as part of the surrounding content, so pair it with a label or context that says what it means rather than relying on color alone. A count pill with `countLabel` carries that context itself: a visually hidden (`sr-only`) span after the number, read as one phrase with it ("99+ unread messages"). A strong badge is louder, not more meaningful: its text ("Critical", "Kritisch") still says the state. In forced colours (Windows High Contrast) the fills give way to the system colours, so a strong badge draws a `CanvasText` outline and still reads as a filled pill next to a soft one.
3988
4029
 
3989
4030
  ## Inputs
3990
4031
 
@@ -3997,6 +4038,7 @@ A badge is a plain `<span>` with no role or ARIA attributes; its text is read as
3997
4038
 
3998
4039
  ## Slots
3999
4040
 
4041
+ - `icon-start`: An icon before the text, drawn at 12 px; not shown on a count pill.
4000
4042
  - `default`: The badge text, shown when no `count` is set.
4001
4043
 
4002
4044
  ## Notes
@@ -4016,6 +4058,13 @@ A badge is a plain `<span>` with no role or ARIA attributes; its text is read as
4016
4058
 
4017
4059
  ## Migration notes
4018
4060
 
4061
+ ### 0.5.0
4062
+
4063
+ New `icon-start` slot: `<bf-icon bfSlot="icon-start" … />` before the label text, drawn at 12 px
4064
+ whatever the icon's `size`, 4 px from the text, with the start padding tightened to 8 px. It
4065
+ takes the text colour, so it follows the variant and forced colours. A count pill does not show
4066
+ it.
4067
+
4019
4068
  ### 0.4.0
4020
4069
 
4021
4070
  New `emphasis` input (`BfBadgeEmphasis`: `soft`, the default, or `strong`). `strong` is a solid
@@ -14449,6 +14498,461 @@ only picks; uploading stays with the app. New label keys `imageInputChoose`, `im
14449
14498
 
14450
14499
  ---
14451
14500
 
14501
+ # bf-image-tile
14502
+
14503
+ Small card for thumbnail grids: a picture and title that act as one target
14504
+
14505
+ `BfImageTileComponent` from `@bitforgehq/ui` · Data display · since 0.5.0
14506
+
14507
+ ## Example
14508
+
14509
+ ```ts
14510
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
14511
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
14512
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
14513
+ import { Box, ImagePlus } from 'lucide';
14514
+
14515
+ /** One model in the grid, as the app tracks it. */
14516
+ interface Model {
14517
+ readonly id: string;
14518
+ readonly name: string;
14519
+ readonly src: string;
14520
+ readonly alt: string;
14521
+ readonly versions: number;
14522
+ readonly updated: string;
14523
+ }
14524
+
14525
+ /** How long the simulated render of the last model takes. */
14526
+ const RENDER_MS = 3000;
14527
+
14528
+ /**
14529
+ * The bf-image-tile overview.
14530
+ *
14531
+ * A grid of 3D models, one picture each: a click selects a tile (the ring, and "Selected" read
14532
+ * before its title), a badge counts the versions, the last picture is still rendering and fades
14533
+ * in when it arrives, and a model without a render yet shows the placeholder. The titles differ
14534
+ * in length, yet every tile in a row is as tall.
14535
+ */
14536
+ @Component({
14537
+ selector: 'app-example-image-tile-overview',
14538
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
14539
+ template: `
14540
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
14541
+ @for (model of models(); track model.id) {
14542
+ <bf-image-tile
14543
+ [src]="model.src"
14544
+ [alt]="model.alt"
14545
+ [loading]="model.id === 'lantern' && rendering()"
14546
+ [selected]="model.id === current()"
14547
+ >
14548
+ <h2>
14549
+ <button bfAction type="button" (click)="current.set(model.id)">{{ model.name }}</button>
14550
+ </h2>
14551
+ <span bfSlot="meta">{{ model.updated }}</span>
14552
+ @if (model.versions > 1) {
14553
+ <bf-badge bfSlot="badge" variant="info"
14554
+ ><bf-icon bfSlot="icon-start" [icon]="box" />{{ model.versions }} versions</bf-badge
14555
+ >
14556
+ }
14557
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
14558
+ </bf-image-tile>
14559
+ }
14560
+ </div>
14561
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">
14562
+ Selected: {{ currentName() }}
14563
+ </p>
14564
+ `,
14565
+ })
14566
+ export class ImageTileOverviewExampleComponent {
14567
+ protected readonly box = Box;
14568
+ protected readonly imagePlus = ImagePlus;
14569
+ protected readonly current = signal('ship');
14570
+ protected readonly rendering = signal(true);
14571
+ protected readonly models = signal<Model[]>([
14572
+ {
14573
+ id: 'ship',
14574
+ name: 'Harbour ship',
14575
+ src: 'media/harbour.svg',
14576
+ alt: 'A ship in a harbour',
14577
+ versions: 3,
14578
+ updated: 'Today',
14579
+ },
14580
+ {
14581
+ id: 'tower',
14582
+ name: 'Glacier watchtower with a long rope bridge',
14583
+ src: 'media/glacier.svg',
14584
+ alt: 'A tower on a glacier',
14585
+ versions: 1,
14586
+ updated: 'Yesterday',
14587
+ },
14588
+ { id: 'well', name: 'Desert well', src: '', alt: '', versions: 1, updated: 'Last week' },
14589
+ {
14590
+ id: 'lantern',
14591
+ name: 'Forest lantern',
14592
+ src: '',
14593
+ alt: 'A lantern in a pine forest',
14594
+ versions: 2,
14595
+ updated: 'Rendering…',
14596
+ },
14597
+ ]);
14598
+
14599
+ constructor() {
14600
+ const timer = setTimeout(() => {
14601
+ this.models.update((models) =>
14602
+ models.map((m) =>
14603
+ m.id === 'lantern' ? { ...m, src: 'media/forest.svg', updated: 'Just now' } : m,
14604
+ ),
14605
+ );
14606
+ this.rendering.set(false);
14607
+ }, RENDER_MS);
14608
+ inject(DestroyRef).onDestroy(() => clearTimeout(timer));
14609
+ }
14610
+
14611
+ protected currentName(): string {
14612
+ return this.models().find((m) => m.id === this.current())?.name ?? '';
14613
+ }
14614
+ }
14615
+ ```
14616
+
14617
+ ## Overview
14618
+
14619
+ Small card for a grid of thumbnails: mostly picture, at a fixed aspect ratio (square by
14620
+ default), with a title and an optional meta line below it or, with `captionPlacement="overlay"`,
14621
+ on a soft gradient over its bottom. Without a picture yet it shows a placeholder (an icon and a
14622
+ text); while one loads a shimmering skeleton; the picture fades in when it arrives; a failed load
14623
+ shows a short message. Make the whole tile one link or button with a `bfAction` in its title, as
14624
+ on `bf-card`: it then lifts on hover, zooms the picture slightly and shows the focus ring.
14625
+ `selected` draws a ring. The title clamps to two lines and always takes two, so tiles in a grid
14626
+ row are as tall whatever their titles.
14627
+
14628
+ 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.
14629
+
14630
+ ## When to use
14631
+
14632
+ 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).
14633
+
14634
+ ## Basic usage
14635
+
14636
+ 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.
14637
+
14638
+ ```ts
14639
+ import { Component, DestroyRef, inject, signal } from '@angular/core';
14640
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
14641
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
14642
+ import { Box, ImagePlus } from 'lucide';
14643
+
14644
+ /** One model in the grid, as the app tracks it. */
14645
+ interface Model {
14646
+ readonly id: string;
14647
+ readonly name: string;
14648
+ readonly src: string;
14649
+ readonly alt: string;
14650
+ readonly versions: number;
14651
+ readonly updated: string;
14652
+ }
14653
+
14654
+ /** How long the simulated render of the last model takes. */
14655
+ const RENDER_MS = 3000;
14656
+
14657
+ /**
14658
+ * The bf-image-tile overview.
14659
+ *
14660
+ * A grid of 3D models, one picture each: a click selects a tile (the ring, and "Selected" read
14661
+ * before its title), a badge counts the versions, the last picture is still rendering and fades
14662
+ * in when it arrives, and a model without a render yet shows the placeholder. The titles differ
14663
+ * in length, yet every tile in a row is as tall.
14664
+ */
14665
+ @Component({
14666
+ selector: 'app-example-image-tile-overview',
14667
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
14668
+ template: `
14669
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
14670
+ @for (model of models(); track model.id) {
14671
+ <bf-image-tile
14672
+ [src]="model.src"
14673
+ [alt]="model.alt"
14674
+ [loading]="model.id === 'lantern' && rendering()"
14675
+ [selected]="model.id === current()"
14676
+ >
14677
+ <h2>
14678
+ <button bfAction type="button" (click)="current.set(model.id)">{{ model.name }}</button>
14679
+ </h2>
14680
+ <span bfSlot="meta">{{ model.updated }}</span>
14681
+ @if (model.versions > 1) {
14682
+ <bf-badge bfSlot="badge" variant="info"
14683
+ ><bf-icon bfSlot="icon-start" [icon]="box" />{{ model.versions }} versions</bf-badge
14684
+ >
14685
+ }
14686
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
14687
+ </bf-image-tile>
14688
+ }
14689
+ </div>
14690
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">
14691
+ Selected: {{ currentName() }}
14692
+ </p>
14693
+ `,
14694
+ })
14695
+ export class ImageTileOverviewExampleComponent {
14696
+ protected readonly box = Box;
14697
+ protected readonly imagePlus = ImagePlus;
14698
+ protected readonly current = signal('ship');
14699
+ protected readonly rendering = signal(true);
14700
+ protected readonly models = signal<Model[]>([
14701
+ {
14702
+ id: 'ship',
14703
+ name: 'Harbour ship',
14704
+ src: 'media/harbour.svg',
14705
+ alt: 'A ship in a harbour',
14706
+ versions: 3,
14707
+ updated: 'Today',
14708
+ },
14709
+ {
14710
+ id: 'tower',
14711
+ name: 'Glacier watchtower with a long rope bridge',
14712
+ src: 'media/glacier.svg',
14713
+ alt: 'A tower on a glacier',
14714
+ versions: 1,
14715
+ updated: 'Yesterday',
14716
+ },
14717
+ { id: 'well', name: 'Desert well', src: '', alt: '', versions: 1, updated: 'Last week' },
14718
+ {
14719
+ id: 'lantern',
14720
+ name: 'Forest lantern',
14721
+ src: '',
14722
+ alt: 'A lantern in a pine forest',
14723
+ versions: 2,
14724
+ updated: 'Rendering…',
14725
+ },
14726
+ ]);
14727
+
14728
+ constructor() {
14729
+ const timer = setTimeout(() => {
14730
+ this.models.update((models) =>
14731
+ models.map((m) =>
14732
+ m.id === 'lantern' ? { ...m, src: 'media/forest.svg', updated: 'Just now' } : m,
14733
+ ),
14734
+ );
14735
+ this.rendering.set(false);
14736
+ }, RENDER_MS);
14737
+ inject(DestroyRef).onDestroy(() => clearTimeout(timer));
14738
+ }
14739
+
14740
+ protected currentName(): string {
14741
+ return this.models().find((m) => m.id === this.current())?.name ?? '';
14742
+ }
14743
+ }
14744
+ ```
14745
+
14746
+ ## States
14747
+
14748
+ 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.
14749
+
14750
+ ```ts
14751
+ import { Component } from '@angular/core';
14752
+ import { BfImageTileComponent } from '@bitforgehq/ui';
14753
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
14754
+ import { ImagePlus } from 'lucide';
14755
+
14756
+ /**
14757
+ * The four states of a tile side by side.
14758
+ *
14759
+ * Without `src` the placeholder shows (an icon and "No image yet"); `loading` shows the skeleton;
14760
+ * a `src` fades in when it loads; `failed` (or a load error) shows a short message.
14761
+ */
14762
+ @Component({
14763
+ selector: 'app-example-image-tile-states',
14764
+ imports: [BfIconComponent, BfImageTileComponent],
14765
+ template: `
14766
+ <div class="grid grid-cols-2 gap-4 @md:grid-cols-4">
14767
+ <bf-image-tile>
14768
+ <h2>Placeholder</h2>
14769
+ <bf-icon bfSlot="placeholder-icon" [icon]="imagePlus" />
14770
+ </bf-image-tile>
14771
+ <bf-image-tile loading><h2>Loading</h2></bf-image-tile>
14772
+ <bf-image-tile src="media/forest.svg" alt="A pine forest under a pale sun">
14773
+ <h2>Loaded</h2>
14774
+ </bf-image-tile>
14775
+ <bf-image-tile failed failedLabel="Render failed"><h2>Failed</h2></bf-image-tile>
14776
+ </div>
14777
+ `,
14778
+ })
14779
+ export class ImageTileStatesExampleComponent {
14780
+ protected readonly imagePlus = ImagePlus;
14781
+ }
14782
+ ```
14783
+
14784
+ ## Caption over the picture
14785
+
14786
+ `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.
14787
+
14788
+ ```ts
14789
+ import { Component } from '@angular/core';
14790
+ import { BfActionDirective, BfBadgeComponent, BfImageTileComponent } from '@bitforgehq/ui';
14791
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
14792
+ import { Check } from 'lucide';
14793
+
14794
+ /** One reference picture on the board. */
14795
+ interface Reference {
14796
+ readonly id: string;
14797
+ readonly title: string;
14798
+ readonly source: string;
14799
+ readonly src: string;
14800
+ readonly reference: boolean;
14801
+ }
14802
+
14803
+ /**
14804
+ * A board of reference pictures with the caption over the picture.
14805
+ *
14806
+ * `captionPlacement="overlay"` lays the title and meta line on a soft gradient over the bottom of
14807
+ * a 4:3 picture; each tile is a link, and a check badge marks the chosen references.
14808
+ */
14809
+ @Component({
14810
+ selector: 'app-example-image-tile-overlay',
14811
+ imports: [BfActionDirective, BfBadgeComponent, BfIconComponent, BfImageTileComponent],
14812
+ template: `
14813
+ <div class="grid grid-cols-1 gap-4 @sm:grid-cols-2 @lg:grid-cols-3">
14814
+ @for (item of references; track item.id) {
14815
+ <bf-image-tile [src]="item.src" alt="" aspect="4/3" captionPlacement="overlay">
14816
+ <h2>
14817
+ <a bfAction [href]="'#' + item.id">{{ item.title }}</a>
14818
+ </h2>
14819
+ <span bfSlot="meta">{{ item.source }}</span>
14820
+ @if (item.reference) {
14821
+ <bf-badge bfSlot="badge" variant="success" emphasis="strong"
14822
+ ><bf-icon bfSlot="icon-start" [icon]="check" />Reference</bf-badge
14823
+ >
14824
+ }
14825
+ </bf-image-tile>
14826
+ }
14827
+ </div>
14828
+ `,
14829
+ })
14830
+ export class ImageTileOverlayExampleComponent {
14831
+ protected readonly check = Check;
14832
+ protected readonly references: Reference[] = [
14833
+ {
14834
+ id: 'skyline',
14835
+ title: 'Skyline at dusk',
14836
+ source: 'Moodboard',
14837
+ src: 'media/skyline.svg',
14838
+ reference: true,
14839
+ },
14840
+ {
14841
+ id: 'desert',
14842
+ title: 'Dunes in the late afternoon, warm light from the west',
14843
+ source: 'Upload',
14844
+ src: 'media/desert.svg',
14845
+ reference: false,
14846
+ },
14847
+ {
14848
+ id: 'glacier',
14849
+ title: 'Glacier',
14850
+ source: 'Moodboard',
14851
+ src: 'media/glacier.svg',
14852
+ reference: true,
14853
+ },
14854
+ ];
14855
+ }
14856
+ ```
14857
+
14858
+ ## Grids of tiles
14859
+
14860
+ 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.
14861
+
14862
+ ## Desktop
14863
+
14864
+ 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.
14865
+
14866
+ ## Theming
14867
+
14868
+ - Tile: `bg-surface`, `text-text`, `rounded-card`, `shadow-raised` (`shadow-floating` on hover).
14869
+ - Picture area: `bg-surface-sunken`; the skeleton is a `bf-skeleton` rect; the placeholder and failure texts are `text-caption text-text-muted`.
14870
+ - 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.
14871
+ - Selection: a 2 px `primary` outline 2 px outside the tile.
14872
+ - Forced colours: a 1 px `CanvasText` border keeps each tile's edge; the selection ring is `Highlight`.
14873
+ - Focus: `ring-4 ring-ring` around the tile.
14874
+ - 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.
14875
+
14876
+ ## Accessibility
14877
+
14878
+ 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.
14879
+
14880
+ ### Keyboard
14881
+
14882
+ | Key | Action |
14883
+ | ---------------- | ---------------------------------------------------------- |
14884
+ | <kbd>Tab</kbd> | Moves to the tile's `bfAction` link or button |
14885
+ | <kbd>Enter</kbd> | Follows a `bfAction` link or activates a `bfAction` button |
14886
+ | <kbd>Space</kbd> | Activates a `bfAction` button; a link ignores it |
14887
+
14888
+ ## Inputs
14889
+
14890
+ | Name | Type | Default | Description |
14891
+ | --- | --- | --- | --- |
14892
+ | `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. |
14893
+ | `alt` | `string` | `''` | Alternative text of the picture; empty marks it decorative (the title names the tile). |
14894
+ | `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`. |
14895
+ | `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. |
14896
+ | `loading` | `boolean` | `false` | Shows the skeleton while there is no `src` yet (a picture that is still being made). |
14897
+ | `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. |
14898
+ | `selected` | `boolean` | `false` | Marks the tile as the selected one: a ring around it, and "Selected" read before its title. |
14899
+ | `placeholderLabel` | `string` | `''` | Placeholder text while there is no picture; empty uses `provideBfLabels()`'s `imageTileEmpty` ("No image yet"). |
14900
+ | `failedLabel` | `string` | `''` | Failure message; empty uses `provideBfLabels()`'s `imageLoadFailed`. |
14901
+
14902
+ ## Outputs
14903
+
14904
+ | Name | Payload | Description |
14905
+ | --- | --- | --- |
14906
+ | `loaded` | `void` | The picture of `src` has loaded. |
14907
+ | `errored` | `void` | The picture of `src` failed to load (the tile shows the failure message). |
14908
+
14909
+ ## Slots
14910
+
14911
+ - `placeholder-icon`: The placeholder's icon, shown with its text while there is no picture.
14912
+ - `badge`: A `bf-badge` in the picture's top start corner.
14913
+ - `default`: The title: a heading holding the tile's `bfAction` link or button.
14914
+ - `meta`: A short line under the title, such as a date or a count.
14915
+
14916
+ ## Notes
14917
+
14918
+ ### Do
14919
+
14920
+ - Use a heading for the title, at the level that fits the page, with the `bfAction` inside it.
14921
+ - Set `loading` while a picture is being made, so the tile holds its place.
14922
+ - Keep the meta line short: a date, a count, a source.
14923
+
14924
+ ### Don't
14925
+
14926
+ - 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.
14927
+ - Don't project your own `<img>` for the picture; pass `src`, so the tile handles loading, fading and failing.
14928
+ - Don't rely on the ring alone to mark the selected tile in your own text: the tile already tells assistive technology.
14929
+
14930
+ ## Migration notes
14931
+
14932
+ ### 0.5.0
14933
+
14934
+ New: `bf-image-tile`, a small card for grids of thumbnails (3D models with one render each, a
14935
+ board of reference pictures). It is mostly picture, at a fixed `aspect` (new type
14936
+ `BfImageTileAspect`: `'1/1'` (default), `'4/3'`, `'16/9'`, `'3/4'`), with the title (default slot,
14937
+ a heading holding a `bfAction` link or button) and an optional `bfSlot="meta"` line below the
14938
+ picture, or with `captionPlacement="overlay"` (new type `BfImageTileCaptionPlacement`: `'below'`
14939
+ (default), `'overlay'`) on a soft `scrim` gradient over its bottom. States share
14940
+ `bf-media-card`'s logic: without `src` a placeholder (`bfSlot="placeholder-icon"` and
14941
+ `placeholderLabel`, default: the new label `imageTileEmpty`, "No image yet"); `loading`, and a
14942
+ `src` that is loading, show a `bf-skeleton` shimmer (`aria-busy`); the picture fades in over
14943
+ 260 ms; a load error or `failed` shows `failedLabel` (default `imageLoadFailed`). `loaded` and
14944
+ `errored` report the picture's events. A `bfAction` makes the whole tile one link or button: the
14945
+ pointer, a 2 px lift to `shadow-floating` and a 5 % zoom of the picture on hover (no movement
14946
+ under reduced motion), and the focus ring. `selected` draws a 2 px primary ring and starts the
14947
+ action's name with the new label `imageTileSelected` ("Selected, Harbour ship"). `bfSlot="badge"`
14948
+ puts a `bf-badge` in the picture's top start corner. The title clamps to two lines and always
14949
+ takes two, so tiles in a grid row are as tall. `@bitforgehq/ui/testing`: new
14950
+ `BfImageTileHarness` (`getTitle()`, `getMeta()`, `getState()`, `getSrc()`, `getAlt()`,
14951
+ `getPlaceholderText()`, `getFailedMessage()`, `isSelected()`, `isClickable()`, `click()`,
14952
+ `loadImage()`, `failImage()`).
14953
+
14954
+ ---
14955
+
14452
14956
  # bf-image-viewer
14453
14957
 
14454
14958
  Full-screen image viewer that zooms from a thumbnail and steps through a set
@@ -17576,6 +18080,55 @@ export class MenuButtonSizesExampleComponent {
17576
18080
  }
17577
18081
  ```
17578
18082
 
18083
+ ## Full width
18084
+
18085
+ `fullWidth` stretches the host and its button to the width of their container, as on
18086
+ `bf-button`: in a grid of actions, a "More" menu button then fills its cell like the buttons
18087
+ beside it. The label and caret stay centred. It does nothing with `iconOnly`, which stays a
18088
+ square.
18089
+
18090
+ ```ts
18091
+ import { Component, signal } from '@angular/core';
18092
+ import { BfButtonComponent } from '@bitforgehq/ui';
18093
+ import {
18094
+ BfMenuButtonComponent,
18095
+ BfMenuComponent,
18096
+ BfMenuItemDirective,
18097
+ } from '@bitforgehq/ui/overlay';
18098
+
18099
+ /**
18100
+ * A two-column grid of actions on a card, where a menu button fills its cell like the buttons
18101
+ * beside it.
18102
+ *
18103
+ * `fullWidth` stretches the host and its button, as on `bf-button`.
18104
+ */
18105
+ @Component({
18106
+ selector: 'app-example-menu-button-full-width',
18107
+ imports: [BfButtonComponent, BfMenuButtonComponent, BfMenuComponent, BfMenuItemDirective],
18108
+ template: `
18109
+ <div class="grid max-w-sm grid-cols-2 gap-2">
18110
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Opened.')">Open</bf-button>
18111
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Duplicated.')"
18112
+ >Duplicate</bf-button
18113
+ >
18114
+ <bf-button variant="outline" fullWidth (clicked)="result.set('Shared.')">Share</bf-button>
18115
+ <bf-menu-button [menu]="more" fullWidth>More</bf-menu-button>
18116
+ </div>
18117
+ <p aria-live="polite" class="text-caption text-text-muted mt-3">{{ result() }}</p>
18118
+
18119
+ <ng-template #more>
18120
+ <bf-menu>
18121
+ <button bfMenuItem (triggered)="result.set('Renamed.')">Rename</button>
18122
+ <button bfMenuItem (triggered)="result.set('Archived.')">Archive</button>
18123
+ </bf-menu>
18124
+ </ng-template>
18125
+ `,
18126
+ })
18127
+ export class MenuButtonFullWidthExampleComponent {
18128
+ protected readonly result = signal('');
18129
+ }
18130
+ ```
18131
+
17579
18132
  ## Desktop
17580
18133
 
17581
18134
  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)).
@@ -17623,6 +18176,7 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
17623
18176
  | `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. |
17624
18177
  | `label` | `string` | `''` | Accessible name (`aria-label`). Required with `iconOnly`; with a visible label leave it empty or start it with the visible text. |
17625
18178
  | `iconOnly` | `boolean` | `false` | Square button holding only the projected icon, like `bf-icon-button`; set `label`. |
18179
+ | `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`. |
17626
18180
  | `disabled` | `boolean` | `false` | Disables the button; the menu cannot open. |
17627
18181
 
17628
18182
  ## Outputs
@@ -17651,6 +18205,12 @@ The keys inside the open menu are on the [bf-menu](https://bitforge-ui.web.app/c
17651
18205
 
17652
18206
  ## Migration notes
17653
18207
 
18208
+ ### 0.5.0
18209
+
18210
+ New `fullWidth` input, as on `bf-button`: the host and its button stretch to the width of their
18211
+ container (label and caret stay centred), so app CSS no longer needs to reach into the inner
18212
+ `<button>`. Ignored with `iconOnly`.
18213
+
17654
18214
  ### 0.3.0
17655
18215
 
17656
18216
  New `bf-menu-button` in `@bitforgehq/ui/overlay`: a native `<button>` that opens a `bf-menu`
@@ -19968,7 +20528,9 @@ export class PaginationOverviewExampleComponent {
19968
20528
  Page navigation for a long list or table: Previous and Next buttons around the page numbers,
19969
20529
  the current one marked, the hidden runs shown as an ellipsis. `compact` shows "Page 3 of 12"
19970
20530
  between the arrows instead, for narrow screens. The page count comes from `pageCount`, or from
19971
- `total` and `pageSize`; the consumer shows the rows of `page`.
20531
+ `total` and `pageSize`; the consumer shows the rows of `page`. A compact pagination with its own
20532
+ `positionLabel`, `previousLabel` and `nextLabel` steps through anything numbered, such as the
20533
+ versions of a model ("‹ v3 of 7 ›").
19972
20534
 
19973
20535
  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.
19974
20536
 
@@ -20015,6 +20577,41 @@ export class PaginationCompactExampleComponent {
20015
20577
  }
20016
20578
  ```
20017
20579
 
20580
+ ## Stepping through versions
20581
+
20582
+ 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.
20583
+
20584
+ ```ts
20585
+ import { Component, signal } from '@angular/core';
20586
+ import { BfPaginationComponent, type BfPaginationPositionLabel } from '@bitforgehq/ui';
20587
+
20588
+ /**
20589
+ * A compact pager that steps through the versions of a model, "‹ v3 of 7 ›".
20590
+ *
20591
+ * `positionLabel` writes the position text; `previousLabel` and `nextLabel` name the arrows.
20592
+ */
20593
+ @Component({
20594
+ selector: 'app-example-pagination-versions',
20595
+ imports: [BfPaginationComponent],
20596
+ template: `
20597
+ <bf-pagination
20598
+ compact
20599
+ size="sm"
20600
+ label="Versions"
20601
+ previousLabel="Previous version"
20602
+ nextLabel="Next version"
20603
+ [positionLabel]="versionOf"
20604
+ [pageCount]="7"
20605
+ [(page)]="version"
20606
+ />
20607
+ `,
20608
+ })
20609
+ export class PaginationVersionsExampleComponent {
20610
+ readonly version = signal(3);
20611
+ protected readonly versionOf: BfPaginationPositionLabel = (page, count) => `v${page} of ${count}`;
20612
+ }
20613
+ ```
20614
+
20018
20615
  ## With a table
20019
20616
 
20020
20617
  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.
@@ -20056,6 +20653,9 @@ At the first and the last page the arrow is disabled with `aria-disabled="true"`
20056
20653
  | `pageSize` | `number` | `10` | Items per page, used with `total` while `pageCount` is 0. |
20057
20654
  | `siblings` | `number` | `1` | How many pages to show on each side of the current one. |
20058
20655
  | `compact` | `boolean` | `false` | Shows "Page 3 of 12" between the arrows instead of the page buttons, for narrow screens. |
20656
+ | `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`. |
20657
+ | `previousLabel` | `string` | `''` | Accessible name of the previous button, such as "Previous version"; empty falls back to `provideBfLabels()`'s `previousPage` ("Previous page"). |
20658
+ | `nextLabel` | `string` | `''` | Accessible name of the next button, such as "Next version"; empty falls back to `provideBfLabels()`'s `nextPage` ("Next page"). |
20059
20659
  | `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). |
20060
20660
  | `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `pagination` ("Pagination"). |
20061
20661
  | `page` | `number` | `1` | The current page, 1-based; two-way bindable. The buttons set it; show the rows of this page. Two-way: `[(page)]`. |
@@ -20085,6 +20685,14 @@ Goes to a page: sets `page` when it is another page within 1…count; else does
20085
20685
 
20086
20686
  ## Migration notes
20087
20687
 
20688
+ ### 0.5.0
20689
+
20690
+ New `positionLabel` input (new type `BfPaginationPositionLabel`, `(page, count) => string`) that
20691
+ writes a compact pagination's position text, such as "v3 of 7"; unset it is the `pageOf` label as
20692
+ before. New `previousLabel` and `nextLabel` inputs name the arrows ("Previous version"); empty
20693
+ they fall back to `previousPage` and `nextPage`. Together they make a compact pager for stepping
20694
+ through versions.
20695
+
20088
20696
  ### 0.3.1
20089
20697
 
20090
20698
  New `bf-pagination` in `@bitforgehq/ui`, page navigation for both shells: a `nav` (named by
@@ -33254,6 +33862,67 @@ type BfImageInputShape = 'round' | 'square'
33254
33862
 
33255
33863
  ---
33256
33864
 
33865
+ # BfImageTileAspect
33866
+
33867
+ The four aspect ratios of an image tile's picture, square by default
33868
+
33869
+ Type from `@bitforgehq/ui` · since 0.5.0
33870
+
33871
+ The aspect ratio of a `bf-image-tile`'s picture: square (default), 4:3, 16:9 or 3:4.
33872
+
33873
+ ```ts
33874
+ import { type BfImageTileAspect } from '@bitforgehq/ui';
33875
+ ```
33876
+
33877
+ ## Signature
33878
+
33879
+ ```ts
33880
+ type BfImageTileAspect = '1/1' | '4/3' | '16/9' | '3/4'
33881
+ ```
33882
+
33883
+ ## Values
33884
+
33885
+ - `1/1`
33886
+ - `4/3`
33887
+ - `16/9`
33888
+ - `3/4`
33889
+
33890
+ ## Used by
33891
+
33892
+ - [bf-image-tile](../components/image-tile.md)
33893
+
33894
+ ---
33895
+
33896
+ # BfImageTileCaptionPlacement
33897
+
33898
+ Where an image tile's caption sits, below the picture or over it
33899
+
33900
+ Type from `@bitforgehq/ui` · since 0.5.0
33901
+
33902
+ Where a `bf-image-tile`'s title and meta line sit: `below` the picture (default), or `overlay`,
33903
+ on a soft gradient over the bottom of the picture.
33904
+
33905
+ ```ts
33906
+ import { type BfImageTileCaptionPlacement } from '@bitforgehq/ui';
33907
+ ```
33908
+
33909
+ ## Signature
33910
+
33911
+ ```ts
33912
+ type BfImageTileCaptionPlacement = 'below' | 'overlay'
33913
+ ```
33914
+
33915
+ ## Values
33916
+
33917
+ - `below`
33918
+ - `overlay`
33919
+
33920
+ ## Used by
33921
+
33922
+ - [bf-image-tile](../components/image-tile.md)
33923
+
33924
+ ---
33925
+
33257
33926
  # BfInputAutocapitalize
33258
33927
 
33259
33928
  A text field's capitalisation setting, a mode or empty to leave it off
@@ -33561,7 +34230,9 @@ import { type BfLabels } from '@bitforgehq/ui';
33561
34230
  | `chatSend` | `string` | `'Send'` | bf-chat-composer's send button. |
33562
34231
  | `chatStop` | `string` | `'Stop'` | bf-chat-composer's stop button, shown in place of send while a reply runs. |
33563
34232
  | `mediaCardSelect` | `string` | `'Select'` | bf-media-card's select button, read before the card's name ("Select Starfall"). |
33564
- | `imageLoadFailed` | `string` | `'The image could not be loaded'` | bf-media-card's and bf-image-viewer's message for an image that failed to load. |
34233
+ | `imageLoadFailed` | `string` | `'The image could not be loaded'` | bf-media-card's, bf-image-tile's and bf-image-viewer's message for an image that failed to load. |
34234
+ | `imageTileEmpty` | `string` | `'No image yet'` | bf-image-tile's placeholder text while it has no image yet. |
34235
+ | `imageTileSelected` | `string` | `'Selected'` | bf-image-tile's selected state, read before the tile's title ("Selected, Dragon"). |
33565
34236
  | `imageViewer` | `string` | `'Image viewer'` | bf-image-viewer's name. |
33566
34237
  | `imageViewerPrevious` | `string` | `'Previous image'` | bf-image-viewer's button that shows the previous image. |
33567
34238
  | `imageViewerNext` | `string` | `'Next image'` | bf-image-viewer's button that shows the next image. |
@@ -33694,6 +34365,31 @@ type BfPageHeaderHeadingLevel = 1 | 2
33694
34365
 
33695
34366
  ---
33696
34367
 
34368
+ # BfPaginationPositionLabel
34369
+
34370
+ Function that writes a compact pager's position text, such as "v3 of 7"
34371
+
34372
+ Type from `@bitforgehq/ui` · since 0.5.0
34373
+
34374
+ Writes a compact `bf-pagination`'s position text from the current page and the page count, such
34375
+ as "v3 of 7" for page 3 of 7.
34376
+
34377
+ ```ts
34378
+ import { type BfPaginationPositionLabel } from '@bitforgehq/ui';
34379
+ ```
34380
+
34381
+ ## Signature
34382
+
34383
+ ```ts
34384
+ type BfPaginationPositionLabel = (page: number, count: number) => string
34385
+ ```
34386
+
34387
+ ## Used by
34388
+
34389
+ - [bf-pagination](../components/pagination.md)
34390
+
34391
+ ---
34392
+
33697
34393
  # BfProgressSize
33698
34394
 
33699
34395
  The two track heights of a progress bar, thin and regular
@@ -34929,7 +35625,8 @@ every string. In a component's `providers` it covers that sub-tree's widgets and
34929
35625
  `fileRejectedType`, `fileRejectedSize`, `fileRejectedCount`,
34930
35626
  `imageInputChoose`, `imageInputUpload`, `imageInputReplace`, `imageInputRemove`,
34931
35627
  `chatThread`, `chatJumpToLatest`, `chatMessageStopped`, `chatMessageFailed`, `chatComposer`,
34932
- `chatSend`, `chatStop`, `mediaCardSelect`, `imageLoadFailed`, `validation`); the overlay
35628
+ `chatSend`, `chatStop`, `mediaCardSelect`, `imageLoadFailed`, `imageTileEmpty`,
35629
+ `imageTileSelected`, `validation`); the overlay
34933
35630
  services (`ok`, `cancel`, `actions`, `sheet`, `modal`, `close`, `expand`, `collapse`,
34934
35631
  `sheetPosition`, `dialog`, `notifications`, `imageViewer`, `imageViewerPrevious`,
34935
35632
  `imageViewerNext`, `imageViewerPosition`) are root singletons and pick it up only when the caller passes its injector:
@@ -41685,6 +42382,79 @@ The reason the last pick was refused, until the next pick or Remove.
41685
42382
 
41686
42383
  ---
41687
42384
 
42385
+ # BfImageTileHarness
42386
+
42387
+ Test harness for an image tile: its picture state, title, meta, selection and action
42388
+
42389
+ Class from `@bitforgehq/ui/testing` · since 0.5.0
42390
+
42391
+ Harness for `bf-image-tile`. jsdom never loads images, so `loadImage()` and `failImage()` fire
42392
+ the picture's `load` and `error` events the way a browser would; `getState()` then tells what
42393
+ the tile shows. `click()` clicks the tile's `bfAction` link or button.
42394
+
42395
+ ```ts
42396
+ import { BfImageTileHarness } from '@bitforgehq/ui/testing';
42397
+ ```
42398
+
42399
+ ## Signature
42400
+
42401
+ ```ts
42402
+ class BfImageTileHarness extends ComponentHarness
42403
+ ```
42404
+
42405
+ ## Methods
42406
+
42407
+ ### `getTitle(): Promise<string>`
42408
+
42409
+ The tile's title: the text of its default slot.
42410
+
42411
+ ### `getMeta(): Promise<string>`
42412
+
42413
+ The meta line under the title.
42414
+
42415
+ ### `getState(): Promise<'empty' | 'loading' | 'loaded' | 'failed'>`
42416
+
42417
+ What the picture area shows: `empty` (the placeholder), `loading` (the skeleton), `loaded`
42418
+ (the picture) or `failed` (the failure message).
42419
+
42420
+ ### `getSrc(): Promise<string>`
42421
+
42422
+ The URL of the picture.
42423
+
42424
+ ### `getAlt(): Promise<string>`
42425
+
42426
+ The alternative text of the picture.
42427
+
42428
+ ### `getPlaceholderText(): Promise<string>`
42429
+
42430
+ The placeholder text while there is no picture.
42431
+
42432
+ ### `getFailedMessage(): Promise<string>`
42433
+
42434
+ The failure message.
42435
+
42436
+ ### `isSelected(): Promise<boolean>`
42437
+
42438
+ Whether the tile is marked selected.
42439
+
42440
+ ### `isClickable(): Promise<boolean>`
42441
+
42442
+ Whether the tile is clickable (has a `bfAction` link or button).
42443
+
42444
+ ### `click(): Promise<void>`
42445
+
42446
+ Clicks the tile's `bfAction` link or button. Throws for a tile without one.
42447
+
42448
+ ### `loadImage(): Promise<void>`
42449
+
42450
+ Fires the picture's `load` event, as a browser does when it arrives. Throws without one.
42451
+
42452
+ ### `failImage(): Promise<void>`
42453
+
42454
+ Fires the picture's `error` event, as a browser does when it fails. Throws without one.
42455
+
42456
+ ---
42457
+
41688
42458
  # BfImageViewerHarness
41689
42459
 
41690
42460
  Test harness for an image viewer: its image, index, caption, steps and close