@bitforgehq/ui 0.4.0 → 0.5.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/README.md +12 -12
  3. package/ai/MANIFEST.index.json +74 -2
  4. package/ai/MANIFEST.json +3507 -1763
  5. package/ai/REFERENCE.index.json +189 -0
  6. package/ai/REFERENCE.json +1617 -139
  7. package/ai/UI_GUIDE.md +70 -20
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/badge.md +41 -1
  10. package/ai/components/chat-composer.md +357 -0
  11. package/ai/components/chat-message.md +251 -0
  12. package/ai/components/chat-thread.md +521 -0
  13. package/ai/components/checkbox.md +7 -0
  14. package/ai/components/combobox.md +12 -0
  15. package/ai/components/date-input.md +9 -0
  16. package/ai/components/datetime-input.md +2 -0
  17. package/ai/components/field.md +10 -0
  18. package/ai/components/file-input.md +498 -0
  19. package/ai/components/header.md +1 -1
  20. package/ai/components/image-input.md +368 -0
  21. package/ai/components/image-tile.md +452 -0
  22. package/ai/components/image-viewer.md +309 -0
  23. package/ai/components/media-card.md +478 -0
  24. package/ai/components/menu-button.md +67 -9
  25. package/ai/components/menu.md +10 -0
  26. package/ai/components/multi-combobox.md +7 -0
  27. package/ai/components/nav-group.md +10 -3
  28. package/ai/components/nav-item.md +11 -2
  29. package/ai/components/number-input.md +2 -7
  30. package/ai/components/pagination.md +59 -11
  31. package/ai/components/popover.md +13 -0
  32. package/ai/components/radio-group.md +6 -0
  33. package/ai/components/segmented.md +14 -2
  34. package/ai/components/select.md +100 -42
  35. package/ai/components/sidebar-toggle.md +16 -9
  36. package/ai/components/sidebar.md +41 -13
  37. package/ai/components/tab-group.md +18 -2
  38. package/ai/components/table.md +17 -2
  39. package/ai/components/theme-menu-button.md +1 -0
  40. package/ai/components/time-input.md +2 -0
  41. package/ai/components/toolbar.md +7 -18
  42. package/ai/reference/bf-chat-composer-harness.md +64 -0
  43. package/ai/reference/bf-chat-message-role.md +27 -0
  44. package/ai/reference/bf-chat-message-status.md +28 -0
  45. package/ai/reference/bf-fade-in-directive.md +18 -0
  46. package/ai/reference/bf-file-input-harness.md +67 -0
  47. package/ai/reference/bf-file-input-variant.md +27 -0
  48. package/ai/reference/bf-image-input-harness.md +67 -0
  49. package/ai/reference/bf-image-input-shape.md +27 -0
  50. package/ai/reference/bf-image-tile-aspect.md +28 -0
  51. package/ai/reference/bf-image-tile-caption-placement.md +27 -0
  52. package/ai/reference/bf-image-tile-harness.md +70 -0
  53. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  54. package/ai/reference/bf-image-viewer-harness.md +87 -0
  55. package/ai/reference/bf-image-viewer-image.md +25 -0
  56. package/ai/reference/bf-image-viewer-options.md +33 -0
  57. package/ai/reference/bf-image-viewer-origin.md +23 -0
  58. package/ai/reference/bf-image-viewer-ref.md +55 -0
  59. package/ai/reference/bf-image-viewer-service.md +32 -0
  60. package/ai/reference/bf-labels.md +107 -75
  61. package/ai/reference/bf-media-card-aspect.md +28 -0
  62. package/ai/reference/bf-media-card-harness.md +66 -0
  63. package/ai/reference/bf-motion-list-directive.md +22 -0
  64. package/ai/reference/bf-pagination-position-label.md +22 -0
  65. package/ai/reference/bf-prose-directive.md +16 -0
  66. package/ai/reference/bf-rejected-file-reason.md +28 -0
  67. package/ai/reference/bf-rejected-file.md +28 -0
  68. package/ai/reference/bf-select-harness.md +14 -5
  69. package/ai/reference/bf-select-option.md +2 -0
  70. package/ai/reference/bf-size.md +3 -0
  71. package/ai/reference/bf_table_sort_select.md +23 -0
  72. package/ai/reference/provide-bf-labels.md +11 -3
  73. package/ai/reference/provide-bf-select.md +20 -0
  74. package/bin/bf-ui.js +1 -0
  75. package/fesm2022/bitforgehq-ui-overlay.mjs +2660 -594
  76. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  77. package/fesm2022/bitforgehq-ui-testing.mjs +414 -10
  78. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  79. package/fesm2022/bitforgehq-ui.mjs +8056 -3993
  80. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  81. package/llms-full.txt +4923 -132
  82. package/llms.txt +36 -1
  83. package/package.json +1 -1
  84. package/theme.css +60 -0
  85. package/types/bitforgehq-ui-overlay.d.ts +695 -7
  86. package/types/bitforgehq-ui-testing.d.ts +517 -6
  87. package/types/bitforgehq-ui.d.ts +1699 -181
@@ -72,7 +72,7 @@ Put the group's links in its default slot and its name in `label`. Groups stack
72
72
 
73
73
  ## In a collapsed sidebar
74
74
 
75
- While the sidebar is collapsed to an icon rail the heading is hidden visually (`sr-only`), so only the icons show, and it still names the group for screen readers.
75
+ While the sidebar is collapsed to an icon rail the heading's row closes and the text fades out (on `--bf-duration-layout`, at once under reduced motion), so only the icons show, and it still names the group for screen readers. The heading stays on one line and truncates, so its height never changes while the sidebar's width moves.
76
76
 
77
77
  ```ts
78
78
  import { Component } from '@angular/core';
@@ -84,7 +84,7 @@ import { Layers, LayoutList, PanelTop, SquareStack } from 'lucide';
84
84
  /**
85
85
  * Groups in a collapsed sidebar.
86
86
  *
87
- * The headings are hidden visually and still name their groups for screen readers; the items
87
+ * The headings close and fade out (at once under reduced motion) and still name their groups for screen readers; the items
88
88
  * show their icons only.
89
89
  */
90
90
  @Component({
@@ -146,7 +146,7 @@ The group itself is static; its links carry the hover, pressed and focus styles
146
146
 
147
147
  ## Accessibility
148
148
 
149
- The host has `role="group"` and `aria-labelledby` pointing at its heading, so a screen reader announces the group's name when focus enters it ("Workspace, group"). The heading is text, not a heading element, so it does not add to the page outline. Collapsed, the heading is `sr-only` and keeps naming the group.
149
+ The host has `role="group"` and `aria-labelledby` pointing at its heading, so a screen reader announces the group's name when focus enters it ("Workspace, group"). The heading is text, not a heading element, so it does not add to the page outline. Collapsed, the heading is closed to no height and transparent, but stays in the accessibility tree and keeps naming the group.
150
150
 
151
151
  ## Inputs
152
152
 
@@ -172,6 +172,13 @@ The host has `role="group"` and `aria-labelledby` pointing at its heading, so a
172
172
 
173
173
  ## Migration notes
174
174
 
175
+ ### 0.5.0
176
+
177
+ Collapsed, the heading's row closes and its text fades out on `--bf-duration-layout` instead of
178
+ switching to `sr-only`; it stays in the accessibility tree and still names the group. The heading
179
+ stays on one line and truncates (it wrapped before), so its height does not change while the
180
+ width moves.
181
+
175
182
  ### 0.3.1
176
183
 
177
184
  New `bf-nav-group`: a titled group of sidebar links, `role="group"` named by its small heading,
@@ -109,7 +109,7 @@ The router sets `aria-current="page"` on the current page's link (`routerLinkAct
109
109
 
110
110
  ## Collapsed
111
111
 
112
- In a collapsed `bf-sidebar` the item shows only its icon in a square of its height step. The label stays in the DOM as screen-reader text, so the link's name does not change, and becomes the link's `title` (the tooltip on hover, kept up to date when the label changes); your own `title` comes back when the sidebar expands. The badge becomes a small dot on the icon's corner and is still announced. Give every item an icon, or it shows nothing while collapsed. For a styled tooltip that also shows on keyboard focus, add `bfTooltip` from `@bitforgehq/ui/overlay` with the label's text while collapsed (an empty text while expanded shows none) and `bfTooltipPlacement="end"` (see [bf-tooltip](https://bitforge-ui.web.app/components/tooltip)).
112
+ In a collapsed `bf-sidebar` the item shows only its icon in a square of its height step: the row keeps its layout, its start padding centres a 20 px icon, and the label fades out while the sidebar narrows, so the icon never moves. The label stays in the DOM, so the link's name does not change, and becomes the link's `title` (the tooltip on hover, kept up to date when the label changes); your own `title` comes back when the sidebar expands. The badge pill shrinks away while a small dot grows on the icon's corner, on the same timing, and the count is still announced. Give every item an icon, or it shows nothing while collapsed. For a styled tooltip that also shows on keyboard focus, add `bfTooltip` from `@bitforgehq/ui/overlay` with the label's text while collapsed (an empty text while expanded shows none) and `bfTooltipPlacement="end"` (see [bf-tooltip](https://bitforge-ui.web.app/components/tooltip)).
113
113
 
114
114
  ## Sizes
115
115
 
@@ -161,11 +161,12 @@ Hover gives a `surface-muted` fill with the `text` colour and the pointer; press
161
161
  - Current page (`aria-current="page"`): `primary-soft` with `primary-strong`, hover `primary-soft-hover`, pressed `primary-soft-pressed`; under forced colours a `LinkText` outline.
162
162
  - Shape: `rounded-control`; label `font-bold`, `text-label-xs` / `text-label-sm` / `text-label` by size.
163
163
  - Badge: `primary` with `on-primary`, `text-micro`; collapsed, a `primary` dot (`CanvasText` under forced colours).
164
+ - Motion: in a sidebar, the padding, the label and the badge move on `--bf-duration-layout` with `ease-md` (none under reduced motion); hover and press colours fade over 150 ms.
164
165
  - Focus: `ring` (4 px).
165
166
 
166
167
  ## Accessibility
167
168
 
168
- The item is the consumer's native `<a>`: exposed as a link, reached with Tab and followed with Enter, its name being its label (plus the badge announcement). The current page carries `aria-current="page"`, which screen readers announce; the fill is not the only cue, as an outline marks it under forced colours. The icon slot is decorative (a `bf-icon` is `aria-hidden` by default). Collapsed, the label is visually hidden but still the name, and `title` adds a pointer tooltip only; the badge dot is `aria-hidden`, its count announced through the screen-reader text. In Safari, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise).
169
+ The item is the consumer's native `<a>`: exposed as a link, reached with Tab and followed with Enter, its name being its label (plus the badge announcement). The current page carries `aria-current="page"`, which screen readers announce; the fill is not the only cue, as an outline marks it under forced colours. The icon slot is decorative (a `bf-icon` is `aria-hidden` by default). Collapsed, the label is transparent and clipped but still the name, and `title` adds a pointer tooltip only; the badge dot is `aria-hidden`, its count announced through the screen-reader text. In Safari, Tab reaches links only when the user turns on "Press Tab to highlight each item" (Option+Tab otherwise).
169
170
 
170
171
  ### Keyboard
171
172
 
@@ -200,6 +201,14 @@ The item is the consumer's native `<a>`: exposed as a link, reached with Tab and
200
201
 
201
202
  ## Migration notes
202
203
 
204
+ ### 0.5.0
205
+
206
+ In a collapsing sidebar the item no longer switches to a centred square: the row keeps its layout
207
+ and its start padding moves so the icon ends centred in the rail without moving, while the label
208
+ fades out (it stays the accessible name; it is no longer `sr-only`). The count pill shrinks away
209
+ while the dot (new element, `[data-badge-dot]`) grows in on the same timing. Each item clips
210
+ what does not fit the rail. No animation under reduced motion or in a top nav.
211
+
203
212
  ### 0.3.1
204
213
 
205
214
  New `a[bfNavItem]`: one destination of a `bf-sidebar` or `bf-top-nav` on the app's own
@@ -165,13 +165,8 @@ The control works in Reactive Forms through Angular's Signal Forms interop (no `
165
165
  ```ts
166
166
  import { Component } from '@angular/core';
167
167
  import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
168
- import {
169
- BfButtonComponent,
170
- BfCheckboxComponent,
171
- BfInputComponent,
172
- BfSelectComponent,
173
- type BfSelectOption,
174
- } from '@bitforgehq/ui';
168
+ import { BfButtonComponent, BfCheckboxComponent, BfInputComponent } from '@bitforgehq/ui';
169
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
175
170
  import { BfNumberInputComponent } from '@bitforgehq/ui/number-input';
176
171
 
177
172
  /**
@@ -42,7 +42,9 @@ export class PaginationOverviewExampleComponent {
42
42
  Page navigation for a long list or table: Previous and Next buttons around the page numbers,
43
43
  the current one marked, the hidden runs shown as an ellipsis. `compact` shows "Page 3 of 12"
44
44
  between the arrows instead, for narrow screens. The page count comes from `pageCount`, or from
45
- `total` and `pageSize`; the consumer shows the rows of `page`.
45
+ `total` and `pageSize`; the consumer shows the rows of `page`. A compact pagination with its own
46
+ `positionLabel`, `previousLabel` and `nextLabel` steps through anything numbered, such as the
47
+ versions of a model ("‹ v3 of 7 ›").
46
48
 
47
49
  Pagination splits a long list or table into pages and lets the user move between them: Previous and Next buttons around the page numbers, the current page marked, the first and the last page always in reach, and an ellipsis for each run of pages in between. `bf-pagination` holds only the page number. You bind `[(page)]` and show that page's rows, sliced on the client or fetched from the server. It works the same in both shells: in a desktop web app and in the Capacitor shell.
48
50
 
@@ -89,6 +91,41 @@ export class PaginationCompactExampleComponent {
89
91
  }
90
92
  ```
91
93
 
94
+ ## Stepping through versions
95
+
96
+ A compact pagination also steps through anything else that is numbered, such as the versions of a model: "‹ v3 of 7 ›". `positionLabel` takes a `BfPaginationPositionLabel`, a function from the page and the count to the position text (`` (page, count) => `v${page} of ${count}` ``); unset, the text comes from `pageOf`. `previousLabel` and `nextLabel` name the arrows ("Previous version") where "Previous page" would be wrong, and `label` names the navigation. Use it instead of a [bf-segmented](https://bitforge-ui.web.app/components/segmented) control once there are more than four versions; the arrows are disabled at the ends, as on any pagination.
97
+
98
+ ```ts
99
+ import { Component, signal } from '@angular/core';
100
+ import { BfPaginationComponent, type BfPaginationPositionLabel } from '@bitforgehq/ui';
101
+
102
+ /**
103
+ * A compact pager that steps through the versions of a model, "‹ v3 of 7 ›".
104
+ *
105
+ * `positionLabel` writes the position text; `previousLabel` and `nextLabel` name the arrows.
106
+ */
107
+ @Component({
108
+ selector: 'app-example-pagination-versions',
109
+ imports: [BfPaginationComponent],
110
+ template: `
111
+ <bf-pagination
112
+ compact
113
+ size="sm"
114
+ label="Versions"
115
+ previousLabel="Previous version"
116
+ nextLabel="Next version"
117
+ [positionLabel]="versionOf"
118
+ [pageCount]="7"
119
+ [(page)]="version"
120
+ />
121
+ `,
122
+ })
123
+ export class PaginationVersionsExampleComponent {
124
+ readonly version = signal(3);
125
+ protected readonly versionOf: BfPaginationPositionLabel = (page, count) => `v${page} of ${count}`;
126
+ }
127
+ ```
128
+
92
129
  ## With a table
93
130
 
94
131
  Put the pagination under the table it pages, aligned to the inline end or centred, and show only the current page's rows: slice them in a `computed()` from `page`, or fetch the page from the server in a `httpResource()` keyed on it. When the user sorts or filters, set `page` back to 1. The [table](https://bitforge-ui.web.app/components/table) page has a paged example.
@@ -123,16 +160,19 @@ At the first and the last page the arrow is disabled with `aria-disabled="true"`
123
160
 
124
161
  ## Inputs
125
162
 
126
- | Name | Type | Default | Description |
127
- | ----------- | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
- | `pageCount` | `number` | `0` | The number of pages; wins over `total` and `pageSize` when above 0. |
129
- | `total` | `number` | `0` | The number of items, used with `pageSize` while `pageCount` is 0. |
130
- | `pageSize` | `number` | `10` | Items per page, used with `total` while `pageCount` is 0. |
131
- | `siblings` | `number` | `1` | How many pages to show on each side of the current one. |
132
- | `compact` | `boolean` | `false` | Shows "Page 3 of 12" between the arrows instead of the page buttons, for narrow screens. |
133
- | `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). |
134
- | `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `pagination` ("Pagination"). |
135
- | `page` | `number` | `1` | The current page, 1-based; two-way bindable. The buttons set it; show the rows of this page. Two-way: `[(page)]`. |
163
+ | Name | Type | Default | Description |
164
+ | --------------- | ----------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `pageCount` | `number` | `0` | The number of pages; wins over `total` and `pageSize` when above 0. |
166
+ | `total` | `number` | `0` | The number of items, used with `pageSize` while `pageCount` is 0. |
167
+ | `pageSize` | `number` | `10` | Items per page, used with `total` while `pageCount` is 0. |
168
+ | `siblings` | `number` | `1` | How many pages to show on each side of the current one. |
169
+ | `compact` | `boolean` | `false` | Shows "Page 3 of 12" between the arrows instead of the page buttons, for narrow screens. |
170
+ | `positionLabel` | `BfPaginationPositionLabel \| undefined` | `undefined` | Writes the compact position text from the page and the count, such as "v3 of 7"; unset uses `provideBfLabels()`'s `pageOf` ("Page 3 of 12"). Only used with `compact`. |
171
+ | `previousLabel` | `string` | `''` | Accessible name of the previous button, such as "Previous version"; empty falls back to `provideBfLabels()`'s `previousPage` ("Previous page"). |
172
+ | `nextLabel` | `string` | `''` | Accessible name of the next button, such as "Next version"; empty falls back to `provideBfLabels()`'s `nextPage` ("Next page"). |
173
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Button height step: sm 32, md 40, lg 48 px (44 px hit area under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
174
+ | `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `pagination` ("Pagination"). |
175
+ | `page` | `number` | `1` | The current page, 1-based; two-way bindable. The buttons set it; show the rows of this page. Two-way: `[(page)]`. |
136
176
 
137
177
  ## Methods
138
178
 
@@ -159,6 +199,14 @@ Goes to a page: sets `page` when it is another page within 1…count; else does
159
199
 
160
200
  ## Migration notes
161
201
 
202
+ ### 0.5.0
203
+
204
+ New `positionLabel` input (new type `BfPaginationPositionLabel`, `(page, count) => string`) that
205
+ writes a compact pagination's position text, such as "v3 of 7"; unset it is the `pageOf` label as
206
+ before. New `previousLabel` and `nextLabel` inputs name the arrows ("Previous version"); empty
207
+ they fall back to `previousPage` and `nextPage`. Together they make a compact pager for stepping
208
+ through versions.
209
+
162
210
  ### 0.3.1
163
211
 
164
212
  New `bf-pagination` in `@bitforgehq/ui`, page navigation for both shells: a `nav` (named by
@@ -207,6 +207,9 @@ in the Capacitor shell a `bf-sheet` suits phone screens better.
207
207
  `Shift+Tab` before the first control closes it onto the trigger.
208
208
  - `Escape` goes to the popover first: inside a dialog, the first `Escape` closes the popover and
209
209
  the next one the dialog.
210
+ - Motion: the panel grows out of the trigger as it opens (a fade, a slight scale and a 4 px slide,
211
+ 180 ms) and shrinks back as it closes (130 ms). Focus and `aria-expanded` change at once; the
212
+ closing panel is inert while it fades. Under `prefers-reduced-motion` it only fades.
210
213
 
211
214
  ### Keyboard
212
215
 
@@ -238,6 +241,16 @@ in the Capacitor shell a `bf-sheet` suits phone screens better.
238
241
 
239
242
  ## Migration notes
240
243
 
244
+ ### 0.5.0
245
+
246
+ The panel animates open and closed. It grows out of the trigger (a fade, a scale from 96 % and a
247
+ 4 px slide away from the trigger, 180 ms, ease-out; transform origin on the trigger's corner,
248
+ following a flip above or below) and shrinks back on close (130 ms). Focus, `aria-expanded` and
249
+ `bfPopoverClosed` still change at once; the closing panel stays in the overlay container, inert
250
+ and click-through, until its exit has played, and opening again during the exit replaces it. Under
251
+ `prefers-reduced-motion` it only fades. Where the Web Animations API is missing or stubbed
252
+ (jsdom), the panel is still removed at once.
253
+
241
254
  ### 0.3.1
242
255
 
243
256
  New `[bfPopoverTriggerFor]` (`BfPopoverTriggerDirective`, exported as `bfPopoverTrigger`) and the
@@ -302,6 +302,7 @@ Hovering an unselected, enabled option darkens its ring (`text-muted`) with the
302
302
  - Pressed (an option held down): its ring border turns `text`; not while disabled or read-only.
303
303
  - Disabled: ring border `surface-sunken`, background `surface-muted`; option label `text-disabled`, description `text-muted`.
304
304
  - Read-only: ring background `surface-muted`, no hover, default cursor.
305
+ - Motion: the dot grows in and shrinks out over 150 ms on `ease-md` (a fade only under reduced motion); an error message fades in (`animate-message-in`).
305
306
  - Legend: `text-muted`. Error text: `danger`. Focus ring: `ring`.
306
307
 
307
308
  The legend, the options and the error text sit 6 px apart; each ring sits 10 px from its label.
@@ -374,6 +375,11 @@ Focuses the checked radio, else the first enabled one; Signal Forms calls it fro
374
375
 
375
376
  ## Migration notes
376
377
 
378
+ ### 0.5.0
379
+
380
+ The selected option's dot grows in and the previous one shrinks away (150 ms on `ease-md`);
381
+ under reduced motion they only fade. An error message fades in when it appears.
382
+
377
383
  ### 0.3.0
378
384
 
379
385
  New exported `BfRadioGroupOrientation` (`'vertical' | 'horizontal'`), the type of `orientation`
@@ -104,6 +104,8 @@ export class SegmentedBasicExampleComponent {
104
104
  `value` is `null` while no segment is selected; the first enabled segment then owns the tab stop.
105
105
  `(changed)` emits the chosen value after a user change.
106
106
 
107
+ The selected pill slides to the newly chosen segment, changing width as it goes (200 ms, `--bf-duration-layout`); it lands in place at once on the first render and after a resize, and always under `prefers-reduced-motion`.
108
+
107
109
  ## Typed values
108
110
 
109
111
  The control is generic over the option value: numbers, ids and objects bind directly.
@@ -330,7 +332,7 @@ Hovering an inactive segment darkens its label to `text`, and the active one tak
330
332
  ## Theming
331
333
 
332
334
  - Track: `surface-sunken`.
333
- - Active segment: `surface` / `text` with the `shadow-segment` elevation and a 1.5 px inset `primary-strong` outline (≥ 3:1 on `surface` and the track, so the selection does not rest on the fill), hover `surface-muted` (under `forced-colors`, `Highlight` / `HighlightText` with a `CanvasText` outline). Inactive: `text-muted`, hover `text`. Pressed (editable only): `surface-pressed` with `text` (the selected segment keeps `Highlight` under `forced-colors`).
335
+ - Active segment: `surface` / `text` with the `shadow-segment` elevation and a 1.5 px inset `primary-strong` outline (≥ 3:1 on `surface` and the track, so the selection does not rest on the fill), hover `surface-muted` (under `forced-colors`, `Highlight` / `HighlightText` with a `CanvasText` outline). Inactive: `text-muted`, hover `text`. Pressed (editable only): `surface-pressed` with `text` (the selected segment keeps `Highlight` under `forced-colors`). The pill slides over `--bf-duration-layout` with the `ease-md` curve.
334
336
  - Read-only: track `surface-muted`, no hover, default cursor. Error: the track gets a 1.5 px `danger-border` ring and the message below is `danger`.
335
337
  - Focus ring: `ring`, drawn past a 2 px `surface` offset so it sits on `surface` (≥ 3:1) rather than on the track. Disabled segments: `text-disabled` with `cursor-not-allowed`; an unselected one takes the `surface-sunken` fill, the selected one keeps its `surface` pill without the shadow, outlined in `text-disabled` (never opacity).
336
338
  - Shape (track and segments): `rounded-control` (`--radius-control`, `9999px` by default).
@@ -339,7 +341,7 @@ Segments are at least 24/32/40 px tall inside the track's 4 px padding. Under a
339
341
 
340
342
  ## Accessibility
341
343
 
342
- `role="radiogroup"` with `aria-label` from `label` (else `ariaLabel`, or `aria-labelledby` from `ariaLabelledby`); each segment is a `<button role="radio">` with `aria-checked` mirroring selection. Only the selected (or first enabled) segment sits in the tab order — the rest carry `tabindex="-1"` — and ArrowRight/Down, ArrowLeft/Up, Home and End move the selection and focus like a native radio group, matching platform expectations. `disabled` segments are skipped by keyboard navigation and get the native attribute. The group carries `aria-required`, and once touched and invalid `aria-invalid="true"` with `aria-describedby` and `aria-errormessage` pointing at the error text. Read-only: `aria-readonly="true"` on the group; the tab stop stays focusable and selection by pointer or keyboard is ignored. `focus()` focuses the tab-stop segment; Signal Forms calls it from `focusBoundControl()`. Under forced colours (Windows High Contrast) the selected segment opts out of the adjustment (`forced-color-adjust: none`) and paints `Highlight`/`HighlightText` itself with a `CanvasText` selected outline, focus outline and focus ring (on a `Canvas` offset) and no shadow, so it paints system colours only and so no text backplate hides its label; a disabled selected segment's outline is `GrayText` instead; the others repaint in `ButtonText`.
344
+ `role="radiogroup"` with `aria-label` from `label` (else `ariaLabel`, or `aria-labelledby` from `ariaLabelledby`); each segment is a `<button role="radio">` with `aria-checked` mirroring selection. Only the selected (or first enabled) segment sits in the tab order — the rest carry `tabindex="-1"` — and ArrowRight/Down, ArrowLeft/Up, Home and End move the selection and focus like a native radio group, matching platform expectations. `disabled` segments are skipped by keyboard navigation and get the native attribute. The group carries `aria-required`, and once touched and invalid `aria-invalid="true"` with `aria-describedby` and `aria-errormessage` pointing at the error text. Read-only: `aria-readonly="true"` on the group; the tab stop stays focusable and selection by pointer or keyboard is ignored. The sliding pill is one decorative `aria-hidden` element of the track. `focus()` focuses the tab-stop segment; Signal Forms calls it from `focusBoundControl()`. Under forced colours (Windows High Contrast) the selected segment opts out of the adjustment (`forced-color-adjust: none`) and paints `Highlight`/`HighlightText` itself with a `CanvasText` selected outline, focus outline and focus ring (on a `Canvas` offset) and no shadow, so it paints system colours only and so no text backplate hides its label; a disabled selected segment's outline is `GrayText` instead; the others repaint in `ButtonText`.
343
345
 
344
346
  ### Keyboard
345
347
 
@@ -422,6 +424,16 @@ missing, or it is already selected.
422
424
 
423
425
  ## Migration notes
424
426
 
427
+ ### 0.5.0
428
+
429
+ The selected segment's pill (surface fill, `shadow-segment`, `primary-strong` outline,
430
+ `Highlight` under forced colours) is now one indicator element of the track that slides to the
431
+ newly chosen segment, changing width as it goes (`--bf-duration-layout`, `ease-md`). The first
432
+ placement and a resize land at once, and so does everything under `prefers-reduced-motion`. The
433
+ selected segment's hover and pressed fills move with it. Until it has been measured (jsdom,
434
+ server render, a hidden control) the segment keeps painting its own pill. `bf-theme-switch`
435
+ slides the same way. The indicator is `aria-hidden`; roles, keyboard and focus are unchanged.
436
+
425
437
  ### 0.3.0
426
438
 
427
439
  **Breaking:** typed values. `BfSegmentedComponent<T = string>` is generic over the option value
@@ -1,14 +1,15 @@
1
1
  # bf-select
2
2
 
3
- Native single-choice dropdown styled like the text field, with Signal Forms binding
3
+ Single-choice dropdown with a styled list, native under touch, with Signal Forms binding
4
4
 
5
- `BfSelectComponent` from `@bitforgehq/ui` · Forms · since 0.1.0
5
+ `BfSelectComponent` from `@bitforgehq/ui/overlay` · Forms · since 0.1.0
6
6
 
7
7
  ## Example
8
8
 
9
9
  ```ts
10
10
  import { Component, computed, signal } from '@angular/core';
11
- import { BfButtonComponent, BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
11
+ import { BfButtonComponent } from '@bitforgehq/ui';
12
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
12
13
 
13
14
  /**
14
15
  * The bf-select overview.
@@ -91,11 +92,16 @@ export class SelectOverviewExampleComponent {
91
92
 
92
93
  ## Overview
93
94
 
94
- Native single-choice dropdown styled like the text field, with a placeholder option and
95
- Signal Forms binding. Generic over the option value (a string by default); `null` means nothing
96
- is selected.
95
+ Single-choice dropdown styled like the text field, with a placeholder and Signal Forms binding.
96
+ Under a mouse it opens a styled list (the combobox's) that follows the select-only combobox
97
+ pattern: the arrow keys, `Home`, `End`, `PageUp`/`PageDown` and typeahead move through it,
98
+ `Enter`, `Space` or `Tab` pick. Under a coarse pointer (touch) it renders the platform's native
99
+ select instead. Generic over the option value (a string by default); `null` means nothing is
100
+ selected. Moved from `@bitforgehq/ui` to `@bitforgehq/ui/overlay` in 0.5.0.
97
101
 
98
- A single-choice dropdown built on the native `<select>`, so it opens the platform picker on phones and stays keyboard-accessible everywhere. Styled like the text field with a CSS chevron.
102
+ A single-choice dropdown styled like the text field, with a CSS chevron. Under a mouse it opens a styled list, the same panel as [bf-combobox](https://bitforge-ui.web.app/components/combobox) without the search: rounded, floating, with a check mark on the chosen option, exactly as wide as the field. Under a coarse pointer (`matchMedia('(pointer: coarse)')`, a phone or a Capacitor app) it renders the native `<select>` with the same label, help and error text instead, so a phone opens its own picker. Closed, both look the same.
103
+
104
+ Import `BfSelectComponent` from `@bitforgehq/ui/overlay` (it needs `@angular/cdk` for the list: `bun add @angular/cdk rxjs`). The list opens in the CDK overlay container, so it is never clipped by a scrolling parent and works inside a `BfDialogService` dialog. `BfSelectOption` is exported from both `@bitforgehq/ui` and `@bitforgehq/ui/overlay`. Before 0.5.0 the select lived in `@bitforgehq/ui`; see [Migration notes](#migration-notes).
99
105
 
100
106
  ## When to use
101
107
 
@@ -105,7 +111,7 @@ Use `bf-select` to choose one value from a list too long to show at once: a coun
105
111
 
106
112
  ```ts
107
113
  import { Component, signal } from '@angular/core';
108
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
114
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
109
115
 
110
116
  /** A select of projects with a placeholder until one is picked. */
111
117
  @Component({
@@ -143,7 +149,7 @@ string conversion. `BfSelectOption` defaults to `string`; type it for anything e
143
149
 
144
150
  ```ts
145
151
  import { Component, signal } from '@angular/core';
146
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
152
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
147
153
 
148
154
  /** A select of numeric team ids: the bound value is a `number | null`. */
149
155
  @Component({
@@ -171,7 +177,7 @@ instance (reloaded from a server), pass `compareWith`:
171
177
  ```ts
172
178
  import { Component, signal } from '@angular/core';
173
179
  import { FormField, form } from '@angular/forms/signals';
174
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
180
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
175
181
 
176
182
  interface Team {
177
183
  id: number;
@@ -202,10 +208,12 @@ export class SelectCompareWithExampleComponent {
202
208
  }
203
209
  ```
204
210
 
205
- `compareWith` is called as `compareWith(optionValue, value)`. A string, number or boolean option
206
- value is also the native `<option value>` (`"42"` for `42`), so a native form post and browser
207
- tests (`selectOption('42')`) see it; an object option's native value is its index. A choice is
208
- mapped back by position, so the model always gets the option's real value. An option whose value is `null` is not supported: `null` means nothing is selected, so such an
211
+ `compareWith` is called as `compareWith(optionValue, value)`. With `name` set, a form post carries
212
+ the chosen option's key: a string, number or boolean option value as text (`"42"` for `42`), an
213
+ object option's index. Under a mouse a hidden input carries it; under touch it is the native
214
+ `<option value>`. A choice is mapped back by position, so the model always gets the option's real
215
+ value. In tests, pick options by label with `BfSelectHarness` from `@bitforgehq/ui/testing`, which
216
+ drives both modes. An option whose value is `null` is not supported: `null` means nothing is selected, so such an
209
217
  option never shows as selected.
210
218
 
211
219
  Migrating from 0.2: "nothing selected" is `null` (it was `''`). Type bound signals and fields
@@ -217,7 +225,7 @@ Migrating from 0.2: "nothing selected" is `null` (it was `''`). Type bound signa
217
225
  ```ts
218
226
  import { Component, signal } from '@angular/core';
219
227
  import { FormField, form } from '@angular/forms/signals';
220
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
228
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
221
229
 
222
230
  /** A select bound to a Signal Forms field. */
223
231
  @Component({
@@ -240,15 +248,14 @@ Reactive Forms, bind a `FormControl<T | null>` through `[formControl]` or `formC
240
248
 
241
249
  ## Read-only
242
250
 
243
- `readonly` shows the chosen option on the muted surface and blocks changes. A mouse press focuses
244
- the select without opening the picker, and the picker keys (arrows, Alt+Arrow, Enter, Space, F4,
245
- type-ahead) do nothing. On touch screens the platform picker can still open, because the browser
246
- gives no press to cancel; a choice made there snaps back to the bound value, so the value never
247
- changes. The select stays in the tab order. Signal Forms binds it from `readonly()` rules.
251
+ `readonly` shows the chosen option on the muted surface and blocks changes. The field stays in the
252
+ tab order, but a click or a key never opens the list. Under touch, the platform picker can still
253
+ open, because the browser gives no press to cancel; a choice made there snaps back to the bound
254
+ value, so the value never changes. Signal Forms binds it from `readonly()` rules.
248
255
 
249
256
  ```ts
250
257
  import { Component } from '@angular/core';
251
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
258
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
252
259
 
253
260
  /** A read-only select. */
254
261
  @Component({
@@ -273,7 +280,7 @@ of the two. Both are ignored while `label` is set.
273
280
 
274
281
  ```ts
275
282
  import { Component, signal } from '@angular/core';
276
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
283
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
277
284
 
278
285
  /** A small toolbar select named by `ariaLabel`. */
279
286
  @Component({
@@ -298,7 +305,7 @@ export class SelectUnlabelledExampleComponent {
298
305
 
299
306
  ```ts
300
307
  import { Component, signal } from '@angular/core';
301
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
308
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
302
309
 
303
310
  /** The sm, md and lg select sizes. */
304
311
  @Component({
@@ -329,7 +336,8 @@ Once the control is touched it shows the first error as text: the error's own `m
329
336
  ```ts
330
337
  import { Component, signal } from '@angular/core';
331
338
  import { FormField, FormRoot, form, required } from '@angular/forms/signals';
332
- import { BfButtonComponent, BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
339
+ import { BfButtonComponent } from '@bitforgehq/ui';
340
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
333
341
 
334
342
  /**
335
343
  * A required select with the default message.
@@ -367,36 +375,61 @@ export class SelectValidationExampleComponent {
367
375
  }
368
376
  ```
369
377
 
378
+ ## In a table
379
+
380
+ A stacked `bf-table` with sortable columns shows a "Sort by" select above its rows. Add `provideBfSelect()` from `@bitforgehq/ui/overlay` to the app's providers (`ApplicationConfig.providers`) so that select is the styled `bf-select` too; without it, the table, which lives in the cdk-free `@bitforgehq/ui`, falls back to the native one. `provideBfSelect()` imports `bf-select` (and the CDK overlay it uses) into the bundle that holds the app config, usually the initial one; when the stacked tables sit behind lazy routes, add it to the `providers` of a route in a lazily loaded routes file instead, or provide `{ provide: BF_TABLE_SORT_SELECT, useValue: BfSelectComponent }` on the component that holds the table, as this site's stacked table example does.
381
+
382
+ ## Migration notes
383
+
384
+ ### 0.5.0
385
+
386
+ - `bf-select` moved from `@bitforgehq/ui` to `@bitforgehq/ui/overlay`: change `import { BfSelectComponent } from '@bitforgehq/ui'` to `import { BfSelectComponent } from '@bitforgehq/ui/overlay'`. The selector, inputs, outputs and Signal Forms binding are unchanged.
387
+ - The overlay entry needs `@angular/cdk` (and `rxjs`): `bun add @angular/cdk rxjs`.
388
+ - `BfSelectOption` stays importable from `@bitforgehq/ui`, and is also exported from `@bitforgehq/ui/overlay`.
389
+ - Add `provideBfSelect()` to the app's providers so a stacked `bf-table`'s "Sort by" control is the styled select (see [In a table](#in-a-table)); without it, that control stays native.
390
+ - Under a mouse the control is no longer a `<select>`: `checkValidity()` and the browser's own "required" popup are not part of the styled mode. Enforce `required` through Signal Forms (`required()`) or your own check; `aria-required` still announces it. A form post keeps working through a hidden input when `name` is set.
391
+ - Tests that set a `<select>`'s value directly break under a mouse; pick by label with `BfSelectHarness` (`selectOption()`, `getValueLabel()`, the new `isOpen()`), which drives both modes alike: `getOptionLabels()` reads a read-only or disabled select's options too, and `selectOption()` on a read-only select changes nothing.
392
+
370
393
  ## Desktop
371
394
 
372
- Hover darkens the border (`text-muted`, `danger` in the error state) with the pointer cursor; a read-only select keeps the default cursor. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The `primary` border and the focus ring show on every focus, as on the text fields. The picker is the browser's own list, so it matches the operating system. Fields default to `md` (40 px); `provideBfDefaults({ fieldSize: 'sm' })` gives 32 px selects for toolbars and filter bars ([density guide](https://bitforge-ui.web.app/density)).
395
+ Hover darkens the border (`text-muted`, `danger` in the error state) with the pointer cursor; a read-only select keeps the default cursor. Hover styles apply only on devices that can hover, so a tap never leaves one stuck. The `primary` border and the focus ring show on every focus, as on the text fields. A click on the field opens the list below it (above it when there is no room below), with the chosen option active; the list follows the field while it is open, and a click outside closes it. The pointer moving over an option makes it the active one. Options are 32, 36 or 44 px tall with the field's size. Fields default to `md` (40 px); `provideBfDefaults({ fieldSize: 'sm' })` gives 32 px selects for toolbars and filter bars ([density guide](https://bitforge-ui.web.app/density)).
373
396
 
374
397
  ## Theming
375
398
 
376
399
  - Border: `border-strong` (the control boundary, ≥ 3:1), switching to `danger-border` on error; focus: `primary` border with a `ring` ring.
377
400
  - Hover (pointer cursor): border `text-muted`, or `danger` in the error state.
378
401
  - Disabled: `surface-sunken` border, `surface-muted` background, `text-disabled` text. Read-only: `surface-muted` background, default cursor. Placeholder text: `text-muted` (≥ 4.5:1). Chevron: `text-subtle`.
402
+ - List: `bg-surface`, a 1 px `border-border`, `rounded-card`, `shadow-floating`; the active option `surface-muted`, the chosen option `primary-text` with a check mark, a disabled one `text-disabled`. In forced colours the active option takes the system highlight.
379
403
  - Label, help and error text come from the `bf-field` wrapper (see its Theming section).
380
404
 
381
405
  ## Accessibility
382
406
 
383
- Native `<select>`, so it opens the platform's own picker on phones and stays fully keyboard-operable. `bf-field` renders the `<label for>`; the select itself wires `aria-describedby` to the help paragraph and the error paragraph under it (both while shown, help first) and, while it is invalid, `aria-errormessage` to the error paragraph; `aria-invalid` is set once touched and invalid (an explicit `errorMessage` shows immediately). The `placeholder`, when set, is a disabled option so it can be shown but never chosen. `aria-label` and `aria-labelledby` carry `ariaLabel` and `ariaLabelledby` only while there is no visible `label`. Read-only: `aria-readonly="true"`, the select stays focusable, a mouse press focuses it without opening the picker, the picker keys (arrows, Alt+Arrow, Enter, Space, F4, type-ahead) are blocked, and a change that still gets through (the platform picker on touch screens) is reverted, so the value never changes. `focus()` focuses the native select; Signal Forms calls it from `focusBoundControl()`.
407
+ Under a mouse the field follows the [select-only combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/) pattern: it is a focusable `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded`, and while open `aria-controls` (the listbox) and `aria-activedescendant` (the active option). Focus never leaves the field. The list is a `listbox` named like the field, its options `role="option"` with `aria-selected` and `aria-disabled`. `required` is announced with `aria-required`.
408
+
409
+ Under touch it is a native `<select>`, so it opens the platform's own picker and stays fully keyboard-operable. The `placeholder`, when set, is a disabled option so it can be shown but never chosen.
410
+
411
+ In both modes `bf-field` renders the label; the control wires `aria-describedby` to the help paragraph and the error paragraph under it (both while shown, help first) and, while it is invalid, `aria-errormessage` to the error paragraph; `aria-invalid` is set once touched and invalid (an explicit `errorMessage` shows immediately). With a visible `label` the label names the control alone: the native select through its `<label for>`, the styled field (a div, which a `<label for>` cannot name) through `aria-labelledby` pointing at the label's id, as in the APG example. `ariaLabel` and `ariaLabelledby` become `aria-label` and `aria-labelledby` only while there is no visible `label`. A hidden input carrying `name` posts nothing while the select is disabled. Read-only: `aria-readonly="true"`, the control stays focusable and the value never changes. Disabled: `aria-disabled="true"` (the native `disabled` under touch) and out of the tab order. `focus()` focuses the field, or the native select under touch; Signal Forms calls it from `focusBoundControl()`.
412
+
413
+ Motion (styled mode): the list grows out of the field as it opens (a fade, a slight scale and a 4 px slide, 180 ms) and fades back as it closes (130 ms). The field's state changes at once and the closing list is inert. Under `prefers-reduced-motion` the list only fades.
384
414
 
385
415
  ### Keyboard
386
416
 
387
- The select is a native `<select>`, so these are the browser's keys and differ slightly between operating systems. While `readonly`, the picker and type-ahead keys are blocked; <kbd>Tab</kbd> and <kbd>Escape</kbd> still work.
417
+ Under a mouse (the styled list). While `readonly`, no key opens the list or changes the value; <kbd>Tab</kbd> still moves on.
388
418
 
389
- | Key | Action |
390
- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
391
- | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves focus to and from the select; a `readonly` one stays in the tab order |
392
- | <kbd>Space</kbd> | Opens the picker |
393
- | <kbd>Alt</kbd>+<kbd>ArrowDown</kbd> / <kbd>F4</kbd> | Opens the picker on Windows and Linux |
394
- | <kbd>ArrowUp</kbd> / <kbd>ArrowDown</kbd> | Closed: chooses the previous / next option on Windows and Linux (<kbd>ArrowLeft</kbd> / <kbd>ArrowRight</kbd> too), opens the picker on macOS; open: moves the highlight |
395
- | <kbd>Home</kbd> / <kbd>End</kbd> | Closed on Windows and Linux: chooses the first / last option; open: highlights the first / last option |
396
- | <kbd>PageUp</kbd> / <kbd>PageDown</kbd> | Moves several options up / down (to the first / last in a short list) |
397
- | <kbd>Enter</kbd> | In the open picker: chooses the highlighted option |
398
- | <kbd>Escape</kbd> | Closes the open picker without changing the value |
399
- | <kbd>A</kbd>–<kbd>Z</kbd>, <kbd>0</kbd>–<kbd>9</kbd> | Type-ahead: moves to the next option whose label starts with the typed text |
419
+ | Key | Where | Action |
420
+ | ------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
421
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> / <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Field | Opens the list on the chosen option (else the first enabled one); <kbd>Alt</kbd>+<kbd>ArrowDown</kbd> too |
422
+ | <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Open list | Next / previous enabled option, wrapping at the ends |
423
+ | <kbd>Home</kbd> / <kbd>End</kbd> | Open list | First / last enabled option |
424
+ | <kbd>PageUp</kbd> / <kbd>PageDown</kbd> | Open list | Moves ten enabled options up / down, stopping at the first / last |
425
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Open list | Picks the active option and closes the list |
426
+ | <kbd>Tab</kbd> | Open list | Picks the active option, closes the list and moves focus on |
427
+ | <kbd>Escape</kbd> | Open list | Closes the list without changing the value (a dialog underneath stays open) |
428
+ | <kbd>A</kbd>–<kbd>Z</kbd>, <kbd>0</kbd>–<kbd>9</kbd> | Field | Typeahead: opens the list and makes the next option whose label starts with the typed text active (case and accents ignored) |
429
+
430
+ Typed keys within half a second form one search ("New Y"); the same letter typed again cycles through the options starting with it. Typeahead and the arrows only move the active option: the value changes on <kbd>Enter</kbd>, <kbd>Space</kbd>, <kbd>Tab</kbd> or a click, so `changed` fires once per pick. The pointer moving over an option makes it the active one, so a following <kbd>Tab</kbd> picks it, as in the combobox.
431
+
432
+ Under touch (the native select) the keys are the browser's own and differ slightly between operating systems: <kbd>Space</kbd> (or <kbd>Alt</kbd>+<kbd>ArrowDown</kbd> / <kbd>F4</kbd> on Windows and Linux) opens the picker, the arrows, <kbd>Home</kbd> / <kbd>End</kbd> and typeahead choose, <kbd>Escape</kbd> closes it.
400
433
 
401
434
  ## Inputs
402
435
 
@@ -408,7 +441,7 @@ The select is a native `<select>`, so these are the browser's keys and differ sl
408
441
  | `label` | `string` | `''` | Visible label above the control. |
409
442
  | `ariaLabel` | `string` | `''` | Accessible name when no visible label is rendered (e.g. a table filter). Ignored when `label` is set. |
410
443
  | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the field when no visible label is rendered. Ignored when `label` is set. |
411
- | `placeholder` | `string` | `''` | Disabled first option shown while nothing is selected. |
444
+ | `placeholder` | `string` | `''` | Muted text shown while nothing is selected. |
412
445
  | `helpText` | `string` | `''` | Helper text under the control. |
413
446
  | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
414
447
  | `disabled` | `boolean` | `false` | Disables the control. Set by `[formField]`; do not bind it yourself. |
@@ -417,7 +450,7 @@ The select is a native `<select>`, so these are the browser's keys and differ sl
417
450
  | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
418
451
  | `touched` | `boolean` | `false` | Whether the user has left the field. Set by `[formField]`; do not bind it yourself. |
419
452
  | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms. Set by `[formField]`; do not bind it yourself. |
420
- | `name` | `string` | `''` | Native name attribute. Set by `[formField]`; do not bind it yourself. |
453
+ | `name` | `string` | `''` | Form name: of a hidden input carrying the selected option's key, or of the native select. Set by `[formField]`; do not bind it yourself. |
421
454
  | `id` | `string` | `generated` | Control id; generated when not set. |
422
455
  | `value` | `T \| null` | `null` | The selected option's value, `null` while nothing is selected; two-way bindable and bound by `[formField]`. Two-way: `[(value)]`. |
423
456
 
@@ -434,7 +467,8 @@ The select is a native `<select>`, so these are the browser's keys and differ sl
434
467
 
435
468
  ### `focus(options?: FocusOptions): void`
436
469
 
437
- Focuses the native select; Signal Forms calls it from `focusBoundControl()`.
470
+ Focuses the field, or the native select under a coarse pointer; Signal Forms calls it from
471
+ `focusBoundControl()`.
438
472
 
439
473
  | Parameter | Type | Description |
440
474
  | ---------- | -------------- | --------------------------------------------------------------------- |
@@ -445,7 +479,8 @@ Focuses the native select; Signal Forms calls it from `focusBoundControl()`.
445
479
  ### Do
446
480
 
447
481
  - Provide a `placeholder` when no option is a sensible default, and make the field required.
448
- - Keep option labels short; long labels are truncated by the native control.
482
+ - Keep option labels short; a long label is truncated in the field.
483
+ - Add `provideBfSelect()` once when a page has a stacked, sortable `bf-table`: in the app config, or on the lazy route that holds the table, so the styled select and the CDK overlay stay out of the initial bundle.
449
484
 
450
485
  ### Don't
451
486
 
@@ -453,6 +488,29 @@ Focuses the native select; Signal Forms calls it from `focusBoundControl()`.
453
488
 
454
489
  ## Migration notes
455
490
 
491
+ ### 0.5.0
492
+
493
+ **Breaking:** `bf-select` moved from `@bitforgehq/ui` to `@bitforgehq/ui/overlay` (see
494
+ Migration). Under a fine pointer it now opens a styled list, the combobox's panel without the
495
+ search, exactly as wide as the field: a focusable `role="combobox"` field following the APG
496
+ select-only combobox pattern (Enter, Space, the arrows and Alt+ArrowDown open; the arrows,
497
+ Home/End and PageUp/PageDown move; Enter, Space, Tab or a click pick; Escape closes without
498
+ closing a dialog underneath). Typeahead (500 ms buffer, case and accents ignored) moves the active
499
+ option, open or closed, and never commits on its own. Under a coarse pointer
500
+ (`matchMedia('(pointer: coarse)')`, followed live) it renders the native `<select>` as before;
501
+ on the server it renders the styled field. With `name` set, a hidden input carries the chosen
502
+ option's key, so plain form posts keep working (none while disabled). With a visible `label` the
503
+ field is named by `aria-labelledby` pointing at the label (a `<label for>` cannot name a div);
504
+ `ariaLabel` and `ariaLabelledby` still apply only without one. Under forced colours a disabled
505
+ field shows `GrayText`. `@bitforgehq/ui/testing`: `BfSelectHarness` drives both modes by label,
506
+ and has a new `isOpen()`; in both modes `getOptionLabels()` reads a read-only or disabled
507
+ select's options, and `selectOption()` on a read-only select changes nothing.
508
+
509
+ The styled list animates open and closed with the popover's motion (180 ms in, 130 ms out). The
510
+ field's `aria-expanded` and focus change at once; the closing list is inert and keeps the options
511
+ it showed until it is gone. Under `prefers-reduced-motion` it only fades. The native select under
512
+ touch is unchanged.
513
+
456
514
  ### 0.3.0
457
515
 
458
516
  **Breaking:** typed values. `BfSelectComponent<T = string>` is generic over the option value