@bitforgehq/ui 0.3.3 → 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.
- package/CHANGELOG.md +394 -0
- package/README.md +14 -14
- package/ai/MANIFEST.index.json +101 -2
- package/ai/MANIFEST.json +2630 -746
- package/ai/REFERENCE.index.json +196 -0
- package/ai/REFERENCE.json +1625 -140
- package/ai/UI_GUIDE.md +83 -21
- package/ai/components/app-frame.md +1 -1
- package/ai/components/badge.md +39 -2
- package/ai/components/chat-composer.md +357 -0
- package/ai/components/chat-message.md +251 -0
- package/ai/components/chat-thread.md +521 -0
- package/ai/components/checkbox.md +7 -0
- package/ai/components/combobox.md +12 -0
- package/ai/components/date-input.md +9 -0
- package/ai/components/datetime-input.md +2 -0
- package/ai/components/field.md +10 -0
- package/ai/components/file-input.md +498 -0
- package/ai/components/header.md +1 -1
- package/ai/components/image-input.md +368 -0
- package/ai/components/image-viewer.md +309 -0
- package/ai/components/list-item.md +105 -9
- package/ai/components/media-card.md +478 -0
- package/ai/components/menu-button.md +2 -0
- package/ai/components/menu.md +10 -0
- package/ai/components/multi-combobox.md +7 -0
- package/ai/components/nav-group.md +10 -3
- package/ai/components/nav-item.md +11 -2
- package/ai/components/number-input.md +2 -7
- package/ai/components/popover.md +13 -0
- package/ai/components/radio-group.md +6 -0
- package/ai/components/segmented.md +14 -2
- package/ai/components/select.md +100 -42
- package/ai/components/sidebar-toggle.md +16 -9
- package/ai/components/sidebar.md +41 -13
- package/ai/components/step.md +112 -0
- package/ai/components/steps.md +124 -0
- package/ai/components/tab-group.md +18 -2
- package/ai/components/table.md +115 -4
- package/ai/components/theme-menu-button.md +1 -0
- package/ai/components/time-input.md +2 -0
- package/ai/components/timeline-event.md +144 -0
- package/ai/components/timeline.md +201 -0
- package/ai/components/toggle.md +11 -1
- package/ai/components/toolbar.md +7 -18
- package/ai/reference/bf-action-directive.md +3 -1
- package/ai/reference/bf-badge-emphasis.md +27 -0
- package/ai/reference/bf-chat-composer-harness.md +64 -0
- package/ai/reference/bf-chat-message-role.md +27 -0
- package/ai/reference/bf-chat-message-status.md +28 -0
- package/ai/reference/bf-fade-in-directive.md +18 -0
- package/ai/reference/bf-file-input-harness.md +67 -0
- package/ai/reference/bf-file-input-variant.md +27 -0
- package/ai/reference/bf-image-input-harness.md +67 -0
- package/ai/reference/bf-image-input-shape.md +27 -0
- package/ai/reference/bf-image-viewer-actions-context.md +29 -0
- package/ai/reference/bf-image-viewer-harness.md +87 -0
- package/ai/reference/bf-image-viewer-image.md +25 -0
- package/ai/reference/bf-image-viewer-options.md +33 -0
- package/ai/reference/bf-image-viewer-origin.md +23 -0
- package/ai/reference/bf-image-viewer-ref.md +55 -0
- package/ai/reference/bf-image-viewer-service.md +32 -0
- package/ai/reference/bf-labels.md +105 -74
- package/ai/reference/bf-media-card-aspect.md +28 -0
- package/ai/reference/bf-media-card-harness.md +66 -0
- package/ai/reference/bf-motion-list-directive.md +22 -0
- package/ai/reference/bf-prose-directive.md +16 -0
- package/ai/reference/bf-rejected-file-reason.md +28 -0
- package/ai/reference/bf-rejected-file.md +28 -0
- package/ai/reference/bf-select-harness.md +14 -5
- package/ai/reference/bf-select-option.md +2 -0
- package/ai/reference/bf-size.md +3 -0
- package/ai/reference/bf-step-heading-level.md +28 -0
- package/ai/reference/bf-table-align.md +1 -1
- package/ai/reference/bf-table-column-align.md +33 -0
- package/ai/reference/bf-table-column.md +7 -7
- package/ai/reference/bf-table-harness.md +12 -0
- package/ai/reference/bf-timeline-tone.md +31 -0
- package/ai/reference/bf-toggle-harness.md +55 -0
- package/ai/reference/bf_table_sort_select.md +23 -0
- package/ai/reference/provide-bf-labels.md +10 -3
- package/ai/reference/provide-bf-select.md +20 -0
- package/bin/bf-ui.js +1 -0
- package/dark-ink.css +124 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +406 -11
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +11149 -6957
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +5219 -141
- package/llms.txt +40 -1
- package/package.json +2 -1
- package/theme.css +84 -0
- package/types/bitforgehq-ui-overlay.d.ts +684 -7
- package/types/bitforgehq-ui-testing.d.ts +510 -7
- package/types/bitforgehq-ui.d.ts +1757 -186
|
@@ -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
|
package/ai/components/select.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# bf-select
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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)`.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
mapped back by position, so the model always gets the option's real
|
|
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.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
390
|
-
|
|
|
391
|
-
| <kbd>
|
|
392
|
-
| <kbd>
|
|
393
|
-
| <kbd>
|
|
394
|
-
| <kbd>
|
|
395
|
-
| <kbd>
|
|
396
|
-
| <kbd>
|
|
397
|
-
| <kbd>
|
|
398
|
-
| <kbd>
|
|
399
|
-
|
|
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` | `''` |
|
|
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` | `''` |
|
|
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
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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-
|
|
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()],
|
package/ai/components/sidebar.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
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-
|
|
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,
|
|
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
|