@bitforgehq/ui 0.4.0 → 0.5.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +303 -0
  2. package/README.md +12 -12
  3. package/ai/MANIFEST.index.json +65 -2
  4. package/ai/MANIFEST.json +2221 -737
  5. package/ai/REFERENCE.index.json +161 -0
  6. package/ai/REFERENCE.json +1336 -27
  7. package/ai/UI_GUIDE.md +61 -20
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/chat-composer.md +357 -0
  10. package/ai/components/chat-message.md +251 -0
  11. package/ai/components/chat-thread.md +521 -0
  12. package/ai/components/checkbox.md +7 -0
  13. package/ai/components/combobox.md +12 -0
  14. package/ai/components/date-input.md +9 -0
  15. package/ai/components/datetime-input.md +2 -0
  16. package/ai/components/field.md +10 -0
  17. package/ai/components/file-input.md +498 -0
  18. package/ai/components/header.md +1 -1
  19. package/ai/components/image-input.md +368 -0
  20. package/ai/components/image-viewer.md +309 -0
  21. package/ai/components/media-card.md +478 -0
  22. package/ai/components/menu-button.md +2 -0
  23. package/ai/components/menu.md +10 -0
  24. package/ai/components/multi-combobox.md +7 -0
  25. package/ai/components/nav-group.md +10 -3
  26. package/ai/components/nav-item.md +11 -2
  27. package/ai/components/number-input.md +2 -7
  28. package/ai/components/popover.md +13 -0
  29. package/ai/components/radio-group.md +6 -0
  30. package/ai/components/segmented.md +14 -2
  31. package/ai/components/select.md +100 -42
  32. package/ai/components/sidebar-toggle.md +16 -9
  33. package/ai/components/sidebar.md +41 -13
  34. package/ai/components/tab-group.md +18 -2
  35. package/ai/components/table.md +17 -2
  36. package/ai/components/theme-menu-button.md +1 -0
  37. package/ai/components/time-input.md +2 -0
  38. package/ai/components/toolbar.md +7 -18
  39. package/ai/reference/bf-chat-composer-harness.md +64 -0
  40. package/ai/reference/bf-chat-message-role.md +27 -0
  41. package/ai/reference/bf-chat-message-status.md +28 -0
  42. package/ai/reference/bf-fade-in-directive.md +18 -0
  43. package/ai/reference/bf-file-input-harness.md +67 -0
  44. package/ai/reference/bf-file-input-variant.md +27 -0
  45. package/ai/reference/bf-image-input-harness.md +67 -0
  46. package/ai/reference/bf-image-input-shape.md +27 -0
  47. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  48. package/ai/reference/bf-image-viewer-harness.md +87 -0
  49. package/ai/reference/bf-image-viewer-image.md +25 -0
  50. package/ai/reference/bf-image-viewer-options.md +33 -0
  51. package/ai/reference/bf-image-viewer-origin.md +23 -0
  52. package/ai/reference/bf-image-viewer-ref.md +55 -0
  53. package/ai/reference/bf-image-viewer-service.md +32 -0
  54. package/ai/reference/bf-labels.md +105 -75
  55. package/ai/reference/bf-media-card-aspect.md +28 -0
  56. package/ai/reference/bf-media-card-harness.md +66 -0
  57. package/ai/reference/bf-motion-list-directive.md +22 -0
  58. package/ai/reference/bf-prose-directive.md +16 -0
  59. package/ai/reference/bf-rejected-file-reason.md +28 -0
  60. package/ai/reference/bf-rejected-file.md +28 -0
  61. package/ai/reference/bf-select-harness.md +14 -5
  62. package/ai/reference/bf-select-option.md +2 -0
  63. package/ai/reference/bf-size.md +3 -0
  64. package/ai/reference/bf_table_sort_select.md +23 -0
  65. package/ai/reference/provide-bf-labels.md +10 -3
  66. package/ai/reference/provide-bf-select.md +20 -0
  67. package/bin/bf-ui.js +1 -0
  68. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  69. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  70. package/fesm2022/bitforgehq-ui-testing.mjs +356 -10
  71. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  72. package/fesm2022/bitforgehq-ui.mjs +7531 -3989
  73. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  74. package/llms-full.txt +4151 -130
  75. package/llms.txt +31 -1
  76. package/package.json +1 -1
  77. package/theme.css +60 -0
  78. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  79. package/types/bitforgehq-ui-testing.d.ts +434 -6
  80. package/types/bitforgehq-ui.d.ts +1473 -182
@@ -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
@@ -41,11 +41,13 @@ interface NavLink {
41
41
  template: `
42
42
  <div class="border-border bg-bg rounded-card flex h-72 overflow-hidden border">
43
43
  <bf-sidebar label="Toggle demo" [(collapsed)]="collapsed">
44
- <div bfSlot="header" class="flex items-center justify-between gap-2">
45
- @if (!collapsed()) {
46
- <span class="text-heading text-text ps-3">Atlas</span>
47
- }
48
- <bf-sidebar-toggle>
44
+ <!-- The brand fades while it narrows to nothing, so the toggle slides with the edge. -->
45
+ <div bfSlot="header" class="flex items-center">
46
+ <span
47
+ class="text-heading text-text ease-md min-w-0 flex-1 truncate ps-3 pe-2 transition-[opacity,padding] duration-(--bf-duration-layout) in-data-collapsed:ps-0 in-data-collapsed:pe-0 in-data-collapsed:opacity-0 motion-reduce:transition-none"
48
+ >Atlas</span
49
+ >
50
+ <bf-sidebar-toggle class="shrink-0">
49
51
  <bf-icon bfSlot="collapse-icon" [icon]="panelClose" />
50
52
  <bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
51
53
  </bf-sidebar-toggle>
@@ -134,7 +136,7 @@ interface NavLink {
134
136
  * The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
135
137
  * `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
136
138
  * buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
137
- * line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
139
+ * line closes and fades out. The theme binds a local signal so the docs site keeps its theme; an app binds
138
140
  * `BfColorSchemeService.scheme`.
139
141
  */
140
142
  @Component({
@@ -176,8 +178,13 @@ interface NavLink {
176
178
  <bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
177
179
  </bf-sidebar-toggle>
178
180
  </div>
179
- <p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
180
- Version 0.3.2
181
+ <!-- The version line closes and fades on the sidebar's timing (a grid row from 1fr to
182
+ 0fr), so the footer shrinks smoothly instead of jumping. -->
183
+ <p
184
+ bfSlot="footer"
185
+ class="text-caption text-text-muted ease-md grid grid-rows-[1fr] ps-3 transition-[grid-template-rows,opacity] duration-(--bf-duration-layout) in-data-collapsed:grid-rows-[0fr] in-data-collapsed:opacity-0 motion-reduce:transition-none"
186
+ >
187
+ <span class="min-h-0 min-w-0 truncate">Version 0.3.2</span>
181
188
  </p>
182
189
  </bf-sidebar>
183
190
  <div class="min-w-0 flex-1 p-4">
@@ -195,7 +202,7 @@ export class SidebarFooterExampleComponent {
195
202
 
196
203
  /** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
197
204
  protected readonly rowClasses = computed(() =>
198
- this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
205
+ this.collapsed() ? 'flex flex-col items-start gap-1' : 'flex items-center justify-between',
199
206
  );
200
207
  protected readonly themeIcon = computed(
201
208
  () => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
@@ -41,7 +41,8 @@ interface NavLink {
41
41
  * Live demo of `bf-sidebar`: a brand and a collapse toggle in the header, two `bf-nav-group`s of
42
42
  * `a[bfNavItem]` router links (this page is the current one, one item has a count badge) and a
43
43
  * footer link. The toggle binds `[(collapsed)]`; collapsed, the items show their icon only and
44
- * name themselves in a tooltip. The result line reports the state.
44
+ * name themselves in a tooltip. The width, the labels and the brand animate together (instantly
45
+ * under reduced motion). The result line reports the state.
45
46
  */
46
47
  @Component({
47
48
  selector: 'app-example-sidebar-overview',
@@ -57,11 +58,15 @@ interface NavLink {
57
58
  template: `
58
59
  <div class="border-border bg-bg rounded-card flex h-96 overflow-hidden border">
59
60
  <bf-sidebar label="Docs" [(collapsed)]="collapsed">
60
- <div bfSlot="header" class="flex items-center justify-between gap-2">
61
- @if (!collapsed()) {
62
- <span class="text-heading text-text ps-3">Atlas</span>
63
- }
61
+ <!-- The brand stays in the DOM and fades while it narrows to nothing, on the sidebar's
62
+ own timing, so the toggle slides with the edge instead of jumping. -->
63
+ <div bfSlot="header" class="flex items-center">
64
+ <span
65
+ class="text-heading text-text ease-md min-w-0 flex-1 truncate ps-3 pe-2 transition-[opacity,padding] duration-(--bf-duration-layout) in-data-collapsed:ps-0 in-data-collapsed:pe-0 in-data-collapsed:opacity-0 motion-reduce:transition-none"
66
+ >Atlas</span
67
+ >
64
68
  <bf-icon-button
69
+ class="shrink-0"
65
70
  variant="text"
66
71
  [label]="collapsed() ? 'Expand sidebar' : 'Collapse sidebar'"
67
72
  (clicked)="collapsed.set(!collapsed())"
@@ -157,7 +162,13 @@ Put `bfNavItem` on your own `<a routerLink routerLinkActive ariaCurrentWhenActiv
157
162
 
158
163
  ## Collapsing to an icon rail
159
164
 
160
- `collapsed` is a model. Put a [`bf-sidebar-toggle`](https://bitforge-ui.web.app/components/sidebar-toggle) (`@bitforgehq/ui/overlay`) in the header or footer: it flips the model, names itself by what the next press does ("Collapse sidebar", "Expand sidebar") and takes its two icons as slots, since a widget never imports one. Bind `[(collapsed)]` to keep the state in your own signal, or bind it to your own button outside the sidebar. Collapsed, every item shows only its icon, centred in a square; its label stays in the DOM as screen-reader text, so its accessible name does not change, and becomes the link's `title`, the tooltip a pointer user sees on hover. A count badge becomes a small dot that is still announced, and the group headings are hidden visually but still name their groups. The width follows the rail (`w-auto`); expanded the sidebar is `w-60` (240 px).
165
+ `collapsed` is a model. Put a [`bf-sidebar-toggle`](https://bitforge-ui.web.app/components/sidebar-toggle) (`@bitforgehq/ui/overlay`) in the header or footer: it flips the model, names itself by what the next press does ("Collapse sidebar", "Expand sidebar") and takes its two icons as slots, since a widget never imports one. Bind `[(collapsed)]` to keep the state in your own signal, or bind it to your own button outside the sidebar. Collapsed, every item shows only its icon, centred in a square; its label stays in the DOM as screen-reader text, so its accessible name does not change, and becomes the link's `title`, the tooltip a pointer user sees on hover. A count badge becomes a small dot that is still announced, and the group headings are hidden visually but still name their groups. Expanded the sidebar is `w-60` (240 px); the rail is one item square of the app's `controlSize` plus the 12 px padding on each side: 56, 64 or 72 px for `sm`, `md` and `lg`, and 68 px under a coarse pointer for `sm` and `md`, where items are 44 px wide.
166
+
167
+ ## Motion
168
+
169
+ Collapsing and expanding animate on the layout timing (`--bf-duration-layout`, 200 ms, on `ease-md`): the width moves, the item labels and group headings fade while the row narrows to its icon, the icons stay where they are, and the count pill shrinks away while its dot grows in. Content beside the sidebar, such as the [`bf-app-frame`](https://bitforge-ui.web.app/components/app-frame) content column, moves with it. Under `prefers-reduced-motion: reduce` the sidebar switches at once, and the first render never animates, so a collapsed state restored by `provideBfSidebarState()` is simply there on load.
170
+
171
+ Your own `header` and `footer` content can follow on the same timing: the host carries `data-collapsed` while collapsed, so Tailwind's `in-data-collapsed:` variant styles anything inside it. Keep the content in the DOM and let the sidebar clip it rather than removing it with `@if`, which makes it jump: fade a brand name with `transition-opacity duration-(--bf-duration-layout) ease-md in-data-collapsed:opacity-0 motion-reduce:transition-none`, and close a block (an account card, a version line) with a grid row, `grid grid-rows-[1fr] in-data-collapsed:grid-rows-[0fr]` around a `min-h-0 overflow-hidden` child, transitioning `grid-template-rows`. The overview and footer examples do both.
161
172
 
162
173
  ## Remember the collapsed sidebar
163
174
 
@@ -218,7 +229,7 @@ export class SidebarRememberedExampleComponent {
218
229
 
219
230
  ## Footer
220
231
 
221
- The footer slot takes several elements, stacked with a 4 px gap: a Settings link as an `a[bfNavItem]`, a row holding an icon-only text [`bf-theme-menu-button`](https://bitforge-ui.web.app/components/theme-menu-button) and the `bf-sidebar-toggle`, and an optional version line in `text-caption text-text-muted`. Expanded, the row puts the two buttons at either end (`flex items-center justify-between`). As an icon rail every footer element is centred: switch the row to a column (`flex flex-col items-center gap-1`) so both buttons fit the rail, and hide the version line (`[class.hidden]="collapsed()"`), which has no icon to shrink to. Open the theme menu upwards (`placement="above-start"`), since the footer sits at the bottom of the screen.
232
+ The footer slot takes several elements, stacked with a 4 px gap: a Settings link as an `a[bfNavItem]`, a row holding an icon-only text [`bf-theme-menu-button`](https://bitforge-ui.web.app/components/theme-menu-button) and the `bf-sidebar-toggle`, and an optional version line in `text-caption text-text-muted`. Expanded, the row puts the two buttons at either end (`flex items-center justify-between`). As an icon rail switch the row to a column (`flex flex-col items-start gap-1`) so both buttons fit the rail; aligned to the start, the theme button does not move while the width animates. Close the version line, which has no icon to shrink to, with a grid row (`in-data-collapsed:grid-rows-[0fr]`, see Motion) so the footer shrinks smoothly. Open the theme menu upwards (`placement="above-start"`), since the footer sits at the bottom of the screen.
222
233
 
223
234
  ```ts
224
235
  import { Component, computed, signal } from '@angular/core';
@@ -250,7 +261,7 @@ interface NavLink {
250
261
  * The footer slot holds a Settings link (`a[bfNavItem]`), a row with an icon-only
251
262
  * `bf-theme-menu-button` and the `bf-sidebar-toggle`, and a version line. Expanded, the two
252
263
  * buttons sit at either end of the row; as an icon rail the row stacks vertically and the version
253
- * line hides. The theme binds a local signal so the docs site keeps its theme; an app binds
264
+ * line closes and fades out. The theme binds a local signal so the docs site keeps its theme; an app binds
254
265
  * `BfColorSchemeService.scheme`.
255
266
  */
256
267
  @Component({
@@ -292,8 +303,13 @@ interface NavLink {
292
303
  <bf-icon bfSlot="expand-icon" [icon]="panelOpen" />
293
304
  </bf-sidebar-toggle>
294
305
  </div>
295
- <p bfSlot="footer" class="text-caption text-text-muted ps-3" [class.hidden]="collapsed()">
296
- Version 0.3.2
306
+ <!-- The version line closes and fades on the sidebar's timing (a grid row from 1fr to
307
+ 0fr), so the footer shrinks smoothly instead of jumping. -->
308
+ <p
309
+ bfSlot="footer"
310
+ class="text-caption text-text-muted ease-md grid grid-rows-[1fr] ps-3 transition-[grid-template-rows,opacity] duration-(--bf-duration-layout) in-data-collapsed:grid-rows-[0fr] in-data-collapsed:opacity-0 motion-reduce:transition-none"
311
+ >
312
+ <span class="min-h-0 min-w-0 truncate">Version 0.3.2</span>
297
313
  </p>
298
314
  </bf-sidebar>
299
315
  <div class="min-w-0 flex-1 p-4">
@@ -311,7 +327,7 @@ export class SidebarFooterExampleComponent {
311
327
 
312
328
  /** Expanded: one row, the buttons at either end. Collapsed: a column in the rail. */
313
329
  protected readonly rowClasses = computed(() =>
314
- this.collapsed() ? 'flex flex-col items-center gap-1' : 'flex items-center justify-between',
330
+ this.collapsed() ? 'flex flex-col items-start gap-1' : 'flex items-center justify-between',
315
331
  );
316
332
  protected readonly themeIcon = computed(
317
333
  () => ({ system: SunMoon, light: Sun, dark: Moon })[this.scheme()],
@@ -481,12 +497,13 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
481
497
  ## Theming
482
498
 
483
499
  - Surface: `surface` with a `border` line on its inline-end edge (`border-e`); 12 px padding and 24 px between the header, the groups and the footer.
484
- - Width: `w-60` (240 px) expanded, the icon rail's own width collapsed.
500
+ - Width: `w-60` (240 px) expanded; collapsed, a rail of one `controlSize` square plus padding (`w-14`, `w-16` or `w-18`; `w-17` under a coarse pointer for `sm` and `md`).
501
+ - Motion: `--bf-duration-layout` and `--ease-md` for the width, the labels, the headings and the badge.
485
502
  - The items' and the groups' tokens are on the [nav item](https://bitforge-ui.web.app/components/nav-item#theming) and [nav group](https://bitforge-ui.web.app/components/nav-group#theming) pages.
486
503
 
487
504
  ## Accessibility
488
505
 
489
- `bf-sidebar` renders a native `<nav>` landmark named by `label`, else `provideBfLabels()`'s `navigation` ("Main"); give each landmark on a page a different name. The header, the destinations and the footer all sit inside it, in that DOM order. The current page is the link the router marks with `aria-current="page"`; the tonal fill is never the only cue under forced colours, where an outline marks it. Collapsing changes only what is visible: every item keeps its label as screen-reader text and each group its name, so the accessible names stay the same. `bf-sidebar-toggle` is a native button named by what it does ("Collapse sidebar"), with `aria-expanded` and `aria-controls` on the `nav`; your own toggle should do the same, and sit in the sidebar or right before it.
506
+ `bf-sidebar` renders a native `<nav>` landmark named by `label`, else `provideBfLabels()`'s `navigation` ("Main"); give each landmark on a page a different name. The header, the destinations and the footer all sit inside it, in that DOM order. The current page is the link the router marks with `aria-current="page"`; the tonal fill is never the only cue under forced colours, where an outline marks it. Collapsing changes only what is visible, and it animates only without `prefers-reduced-motion: reduce`: every item keeps its label as screen-reader text and each group its name, so the accessible names stay the same. `bf-sidebar-toggle` is a native button named by what it does ("Collapse sidebar"), with `aria-expanded` and `aria-controls` on the `nav`; your own toggle should do the same, and sit in the sidebar or right before it.
490
507
 
491
508
  ### Keyboard
492
509
 
@@ -515,14 +532,25 @@ The sidebar is a desktop pattern. Its items take a `surface-muted` fill and the
515
532
  - Mark the current page with `routerLinkActive` and `ariaCurrentWhenActive="page"` on every link.
516
533
  - Drive the sidebar, the small-screen menu and the shell's tab bar from one array of destinations.
517
534
  - Remember the collapsed state with `BfSidebarStateService` and `provideBfSidebarState()`.
535
+ - Let header and footer content fade or close with `in-data-collapsed:` on `--bf-duration-layout`, so it moves with the sidebar.
518
536
 
519
537
  ### Don't
520
538
 
521
539
  - Don't put actions (Create, Log out) among the destinations; a sidebar is for places, so put an action in the header or footer as a button.
522
540
  - Don't use `bf-sidebar` inside the Capacitor shell; use `bf-tabs` with a `bf-tab-bar` there.
541
+ - Don't remove header or footer content with `@if` on the collapsed state; it disappears before the width has moved.
523
542
 
524
543
  ## Migration notes
525
544
 
545
+ ### 0.5.0
546
+
547
+ Collapsing and expanding now animate: the width moves over `--bf-duration-layout` on `ease-md`
548
+ from `w-60` to a fixed rail (see Migration 4), and the content beside it (the `bf-app-frame`
549
+ column) moves with it. Under `prefers-reduced-motion: reduce` it still switches at once. The first
550
+ render never animates, so a collapsed state restored by `provideBfSidebarState()` does not slide
551
+ on load. The host carries `data-collapsed` as before; header and footer content can follow on the
552
+ same timing with `in-data-collapsed:` (new Motion section in the docs).
553
+
526
554
  ### 0.3.2
527
555
 
528
556
  New `BfSidebarStateService` (a `collapsed` signal) and `provideBfSidebarState()` in