@bitforgehq/ui 0.3.0 → 0.3.1

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 (144) hide show
  1. package/CHANGELOG.md +482 -0
  2. package/README.md +17 -11
  3. package/ai/MANIFEST.index.json +288 -0
  4. package/ai/MANIFEST.json +8716 -3926
  5. package/ai/REFERENCE.index.json +413 -0
  6. package/ai/REFERENCE.json +4710 -2047
  7. package/ai/UI_GUIDE.md +185 -57
  8. package/ai/components/back-button.md +42 -2
  9. package/ai/components/badge.md +17 -7
  10. package/ai/components/bar-chart.md +188 -0
  11. package/ai/components/breadcrumb-item.md +121 -0
  12. package/ai/components/breadcrumb.md +277 -0
  13. package/ai/components/button.md +82 -15
  14. package/ai/components/calendar.md +300 -0
  15. package/ai/components/card.md +63 -2
  16. package/ai/components/chart-card.md +283 -0
  17. package/ai/components/combobox.md +411 -0
  18. package/ai/components/date-input.md +396 -0
  19. package/ai/components/datetime-input.md +215 -0
  20. package/ai/components/description-item.md +111 -0
  21. package/ai/components/description-list.md +160 -0
  22. package/ai/components/donut-chart.md +169 -0
  23. package/ai/components/header.md +67 -2
  24. package/ai/components/icon-button.md +74 -16
  25. package/ai/components/icon.md +63 -7
  26. package/ai/components/line-chart.md +234 -0
  27. package/ai/components/list.md +70 -6
  28. package/ai/components/menu.md +5 -0
  29. package/ai/components/multi-combobox.md +257 -0
  30. package/ai/components/nav-group.md +178 -0
  31. package/ai/components/nav-item.md +210 -0
  32. package/ai/components/page-header.md +237 -0
  33. package/ai/components/pagination.md +173 -0
  34. package/ai/components/popover.md +250 -0
  35. package/ai/components/side-panel.md +391 -0
  36. package/ai/components/sidebar.md +363 -0
  37. package/ai/components/skeleton.md +215 -0
  38. package/ai/components/sparkline.md +188 -0
  39. package/ai/components/stat.md +233 -0
  40. package/ai/components/tab-group.md +226 -0
  41. package/ai/components/tab-panel.md +163 -0
  42. package/ai/components/table.md +568 -0
  43. package/ai/components/tabs.md +42 -2
  44. package/ai/components/theme-menu-button.md +182 -0
  45. package/ai/components/theme-switch.md +170 -0
  46. package/ai/components/time-input.md +240 -0
  47. package/ai/components/toolbar.md +289 -0
  48. package/ai/components/tooltip.md +230 -0
  49. package/ai/components/top-nav.md +159 -0
  50. package/ai/reference/bf-badge-appearance.md +36 -0
  51. package/ai/reference/bf-badge-variant.md +1 -1
  52. package/ai/reference/bf-bar-chart-orientation.md +27 -0
  53. package/ai/reference/bf-button-appearance.md +36 -0
  54. package/ai/reference/bf-button-variant.md +1 -1
  55. package/ai/reference/bf-calendar-disabled-date.md +23 -0
  56. package/ai/reference/bf-calendar-harness.md +51 -0
  57. package/ai/reference/bf-calendar-weekday.md +31 -0
  58. package/ai/reference/bf-chart-card-heading-level.md +26 -0
  59. package/ai/reference/bf-chart-curve.md +28 -0
  60. package/ai/reference/bf-chart-harness.md +51 -0
  61. package/ai/reference/bf-chart-legend-position.md +30 -0
  62. package/ai/reference/bf-chart-series.md +23 -0
  63. package/ai/reference/bf-chart-value-format.md +23 -0
  64. package/ai/reference/bf-color-scheme-service.md +30 -0
  65. package/ai/reference/bf-color-scheme.md +31 -0
  66. package/ai/reference/bf-combobox-filter.md +27 -0
  67. package/ai/reference/bf-combobox-harness.md +89 -0
  68. package/ai/reference/bf-combobox-option.md +33 -0
  69. package/ai/reference/bf-compare-with.md +2 -0
  70. package/ai/reference/bf-date-input-harness.md +67 -0
  71. package/ai/reference/bf-date-input-picker.md +27 -0
  72. package/ai/reference/bf-date-time-input-harness.md +76 -0
  73. package/ai/reference/bf-date-time-input-picker.md +27 -0
  74. package/ai/reference/bf-description-list-layout.md +26 -0
  75. package/ai/reference/bf-donut-slice.md +22 -0
  76. package/ai/reference/bf-labels.md +67 -20
  77. package/ai/reference/bf-menu-button-variant.md +1 -0
  78. package/ai/reference/bf-menu-placement.md +1 -0
  79. package/ai/reference/bf-overlay-options.md +28 -27
  80. package/ai/reference/bf-overlay-placement.md +34 -0
  81. package/ai/reference/bf-overlay-position.md +1 -1
  82. package/ai/reference/bf-overlay-service.md +5 -3
  83. package/ai/reference/bf-page-header-heading-level.md +26 -0
  84. package/ai/reference/bf-pagination-harness.md +61 -0
  85. package/ai/reference/bf-platform-service.md +17 -15
  86. package/ai/reference/bf-popover-harness.md +42 -0
  87. package/ai/reference/bf-popover-placement.md +28 -0
  88. package/ai/reference/bf-popover-trigger-directive.md +56 -0
  89. package/ai/reference/bf-side-panel-config.md +35 -0
  90. package/ai/reference/bf-side-panel-harness.md +34 -0
  91. package/ai/reference/bf-side-panel-ref.md +44 -0
  92. package/ai/reference/bf-side-panel-service.md +43 -0
  93. package/ai/reference/bf-side-panel-template-context.md +32 -0
  94. package/ai/reference/bf-side-panel-width.md +28 -0
  95. package/ai/reference/bf-size.md +12 -0
  96. package/ai/reference/bf-skeleton-shape.md +28 -0
  97. package/ai/reference/bf-stat-trend-intent.md +27 -0
  98. package/ai/reference/bf-stat-trend.md +27 -0
  99. package/ai/reference/bf-stat-variant.md +26 -0
  100. package/ai/reference/bf-tab-group-harness.md +44 -0
  101. package/ai/reference/bf-table-align.md +26 -0
  102. package/ai/reference/bf-table-cell-context.md +24 -0
  103. package/ai/reference/bf-table-cell-directive.md +22 -0
  104. package/ai/reference/bf-table-column.md +28 -0
  105. package/ai/reference/bf-table-harness.md +77 -0
  106. package/ai/reference/bf-table-row-key.md +21 -0
  107. package/ai/reference/bf-table-sort-direction.md +26 -0
  108. package/ai/reference/bf-table-sort.md +27 -0
  109. package/ai/reference/bf-theme.md +5 -4
  110. package/ai/reference/bf-time-input-harness.md +67 -0
  111. package/ai/reference/bf-time-input-hour-cycle.md +27 -0
  112. package/ai/reference/bf-time-input-picker.md +27 -0
  113. package/ai/reference/bf-tooltip-directive.md +43 -0
  114. package/ai/reference/bf-tooltip-harness.md +43 -0
  115. package/ai/reference/bf-tooltip-placement.md +28 -0
  116. package/ai/reference/bf_color_scheme_storage.md +20 -0
  117. package/ai/reference/bf_side_panel_data.md +17 -0
  118. package/ai/reference/bf_theme_storage.md +3 -1
  119. package/ai/reference/provide-bf-color-scheme.md +20 -0
  120. package/ai/reference/provide-bf-labels.md +12 -4
  121. package/ai/reference/slide-in-from-end.md +24 -0
  122. package/ai/reference/slide-out-to-end.md +24 -0
  123. package/fesm2022/bitforgehq-ui-charts.mjs +3045 -0
  124. package/fesm2022/bitforgehq-ui-charts.mjs.map +1 -0
  125. package/fesm2022/bitforgehq-ui-icons.mjs +107 -3
  126. package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -1
  127. package/fesm2022/bitforgehq-ui-mobile.mjs +14 -59
  128. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  129. package/fesm2022/bitforgehq-ui-overlay.mjs +6436 -33
  130. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  131. package/fesm2022/bitforgehq-ui-testing.mjs +618 -1
  132. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  133. package/fesm2022/bitforgehq-ui.mjs +5092 -153
  134. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  135. package/llms-full.txt +25177 -14398
  136. package/llms.txt +95 -1
  137. package/package.json +7 -3
  138. package/theme.css +101 -0
  139. package/types/bitforgehq-ui-charts.d.ts +967 -0
  140. package/types/bitforgehq-ui-icons.d.ts +13 -1
  141. package/types/bitforgehq-ui-mobile.d.ts +25 -18
  142. package/types/bitforgehq-ui-overlay.d.ts +1844 -7
  143. package/types/bitforgehq-ui-testing.d.ts +748 -1
  144. package/types/bitforgehq-ui.d.ts +2136 -44
@@ -0,0 +1,289 @@
1
+ # bf-toolbar
2
+
3
+ Dense row of search, filters, view switch, result count and actions
4
+
5
+ `BfToolbarComponent` from `@bitforgehq/ui` · Actions · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import {
12
+ BfButtonComponent,
13
+ BfInputComponent,
14
+ BfSegmentedComponent,
15
+ BfSelectComponent,
16
+ BfToolbarComponent,
17
+ type BfSegmentOption,
18
+ type BfSelectOption,
19
+ } from '@bitforgehq/ui';
20
+
21
+ type ProjectStatus = 'active' | 'paused' | 'done';
22
+ type StatusFilter = ProjectStatus | 'all';
23
+ type View = 'list' | 'grid';
24
+
25
+ interface Project {
26
+ readonly name: string;
27
+ readonly status: ProjectStatus;
28
+ }
29
+
30
+ /**
31
+ * The bf-toolbar overview.
32
+ *
33
+ * Live demo of `bf-toolbar`: a search field and a status filter narrow a list of projects, a
34
+ * view switch lays it out as a list or a grid, the toolbar's status announces the count, and a
35
+ * primary action adds a project. Every control in the row is `sm` from the toolbar's density.
36
+ */
37
+ @Component({
38
+ selector: 'app-example-toolbar-overview',
39
+ imports: [
40
+ BfButtonComponent,
41
+ BfInputComponent,
42
+ BfSegmentedComponent,
43
+ BfSelectComponent,
44
+ BfToolbarComponent,
45
+ ],
46
+ template: `
47
+ <div class="flex flex-col gap-3">
48
+ <bf-toolbar label="Project filters" [status]="count()">
49
+ <bf-input
50
+ type="search"
51
+ ariaLabel="Search projects"
52
+ placeholder="Search projects"
53
+ class="w-full sm:w-56"
54
+ [(value)]="query"
55
+ />
56
+ <bf-select
57
+ ariaLabel="Status"
58
+ class="w-full sm:w-40"
59
+ [options]="statuses"
60
+ [(value)]="filter"
61
+ />
62
+ <bf-segmented bfSlot="view" label="View" [options]="views" [(value)]="view" />
63
+ <bf-button bfSlot="actions" (clicked)="add()">New project</bf-button>
64
+ </bf-toolbar>
65
+ <ul [class]="view() === 'grid' ? 'grid grid-cols-2 gap-2' : 'flex flex-col gap-1'">
66
+ @for (project of visible(); track project.name) {
67
+ <li class="bg-surface rounded-field text-body px-3 py-2">{{ project.name }}</li>
68
+ }
69
+ </ul>
70
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
71
+ </div>
72
+ `,
73
+ })
74
+ export class ToolbarOverviewExampleComponent {
75
+ protected readonly query = signal('');
76
+ protected readonly filter = signal<StatusFilter | null>('all');
77
+ protected readonly view = signal<View | null>('list');
78
+ protected readonly projects = signal<readonly Project[]>([
79
+ { name: 'Atlas rebrand', status: 'active' },
80
+ { name: 'Billing revamp', status: 'active' },
81
+ { name: 'Carbon audit', status: 'paused' },
82
+ { name: 'Docs refresh', status: 'done' },
83
+ { name: 'Onboarding flow', status: 'active' },
84
+ ]);
85
+
86
+ protected readonly statuses: BfSelectOption<StatusFilter>[] = [
87
+ { value: 'all', label: 'All statuses' },
88
+ { value: 'active', label: 'Active' },
89
+ { value: 'paused', label: 'Paused' },
90
+ { value: 'done', label: 'Done' },
91
+ ];
92
+ protected readonly views: BfSegmentOption<View>[] = [
93
+ { value: 'list', label: 'List' },
94
+ { value: 'grid', label: 'Grid' },
95
+ ];
96
+
97
+ protected readonly visible = computed(() => {
98
+ const query = this.query().trim().toLowerCase();
99
+ const filter = this.filter() ?? 'all';
100
+ return this.projects().filter(
101
+ (p) =>
102
+ (filter === 'all' || p.status === filter) &&
103
+ (!query || p.name.toLowerCase().includes(query)),
104
+ );
105
+ });
106
+
107
+ protected readonly count = computed(() => {
108
+ const n = this.visible().length;
109
+ return n === 1 ? '1 project' : `${n} projects`;
110
+ });
111
+
112
+ protected readonly status = computed(
113
+ () => `Showing ${this.visible().length} of ${this.projects().length} · ${this.view()} view`,
114
+ );
115
+
116
+ protected add(): void {
117
+ const next = this.projects().length + 1;
118
+ this.projects.update((list) => [...list, { name: `Project ${next}`, status: 'active' }]);
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## Overview
124
+
125
+ Row of search, filters, a view switch and trailing actions above a list or table. Every
126
+ control projected into it is `sm` unless it sets `size` itself.
127
+
128
+ A dense row above a list or table: the search field and filters, a view switch, a live result count and the trailing actions. `bf-toolbar` lays the row out, wraps it in a narrow column and makes every control in it `sm`; the controls themselves are ordinary `bf-input`, `bf-select`, `bf-chip-group`, `bf-segmented` and `bf-button`s.
129
+
130
+ ## When to use
131
+
132
+ Use `bf-toolbar` on a desktop page for the controls that narrow or act on the collection below it: search, filters, list or grid, "New". Use [bf-action-bar](https://bitforge-ui.web.app/components/action-bar) instead for actions on a selection while it lasts. In the Capacitor shell, put the main action in [bf-header](https://bitforge-ui.web.app/components/header) actions and move filters into a [bf-sheet](https://bitforge-ui.web.app/components/sheet) instead.
133
+
134
+ ## Layout
135
+
136
+ Project the search field and the filters as content, in the order they read. `bfSlot="view"` takes a view switch after them, and `bfSlot="actions"` the trailing actions, pushed to the inline end. Each field needs its own `ariaLabel` (or `label`), since the row shows no field labels. Bind `status` to the result count.
137
+
138
+ ## Status
139
+
140
+ `status` renders a polite live region (`role="status"`) after the view switch, in muted caption text. Keep it short and countable ("24 results", "No matches"); it is announced each time it changes, without moving focus. The region is always in the DOM, so the first count after an empty one is announced too; while it is empty it takes no space.
141
+
142
+ ```ts
143
+ import { Component, computed, signal } from '@angular/core';
144
+ import { BfChipComponent, BfChipGroupComponent, BfToolbarComponent } from '@bitforgehq/ui';
145
+
146
+ type Filter = 'all' | 'open' | 'done';
147
+
148
+ interface Task {
149
+ readonly title: string;
150
+ readonly done: boolean;
151
+ }
152
+
153
+ /**
154
+ * A result count in the status line.
155
+ *
156
+ * The toolbar's `status` is a polite live region: picking a filter changes the count, and a
157
+ * screen reader announces it without moving focus.
158
+ */
159
+ @Component({
160
+ selector: 'app-example-toolbar-status',
161
+ imports: [BfChipComponent, BfChipGroupComponent, BfToolbarComponent],
162
+ template: `
163
+ <bf-toolbar label="Task filters" [status]="count()">
164
+ <bf-chip-group label="Status" [(value)]="filter">
165
+ <bf-chip variant="ink" value="all">All</bf-chip>
166
+ <bf-chip variant="ink" value="open">Open</bf-chip>
167
+ <bf-chip variant="ink" value="done">Done</bf-chip>
168
+ </bf-chip-group>
169
+ </bf-toolbar>
170
+ `,
171
+ })
172
+ export class ToolbarStatusExampleComponent {
173
+ protected readonly filter = signal<Filter | null>('all');
174
+ private readonly tasks: readonly Task[] = [
175
+ { title: 'Draft the brief', done: true },
176
+ { title: 'Review the logo', done: false },
177
+ { title: 'Book the venue', done: false },
178
+ ];
179
+
180
+ protected readonly count = computed(() => {
181
+ const filter = this.filter() ?? 'all';
182
+ const n = this.tasks.filter((t) => filter === 'all' || t.done === (filter === 'done')).length;
183
+ return n === 1 ? '1 task' : `${n} tasks`;
184
+ });
185
+ }
186
+ ```
187
+
188
+ ## Wrapping
189
+
190
+ The row is a wrapping flex row with an 8 px gap between items and between rows. In a narrow column the filters wrap onto new rows, and the actions wrap as one group, kept together at the inline end. Give a search field a width (`class="w-full sm:w-56"`) so it fills a phone-wide row and stays compact on a wide one.
191
+
192
+ ```ts
193
+ import { Component } from '@angular/core';
194
+ import {
195
+ BfButtonComponent,
196
+ BfInputComponent,
197
+ BfSelectComponent,
198
+ BfToolbarComponent,
199
+ type BfSelectOption,
200
+ } from '@bitforgehq/ui';
201
+
202
+ /**
203
+ * A toolbar that wraps.
204
+ *
205
+ * In a narrow column the filters wrap onto new rows 8 px apart, and the actions move down
206
+ * together, still at the inline end.
207
+ */
208
+ @Component({
209
+ selector: 'app-example-toolbar-wrapping',
210
+ imports: [BfButtonComponent, BfInputComponent, BfSelectComponent, BfToolbarComponent],
211
+ template: `
212
+ <div class="max-w-sm">
213
+ <bf-toolbar label="Invoice filters" status="18 invoices">
214
+ <bf-input type="search" ariaLabel="Search invoices" placeholder="Search invoices" />
215
+ <bf-select ariaLabel="Customer" [options]="customers" value="all" />
216
+ <bf-select ariaLabel="Period" [options]="periods" value="month" />
217
+ <bf-button bfSlot="actions" variant="outline">Export</bf-button>
218
+ <bf-button bfSlot="actions">New invoice</bf-button>
219
+ </bf-toolbar>
220
+ </div>
221
+ `,
222
+ })
223
+ export class ToolbarWrappingExampleComponent {
224
+ protected readonly customers: BfSelectOption[] = [
225
+ { value: 'all', label: 'All customers' },
226
+ { value: 'acme', label: 'Acme' },
227
+ ];
228
+ protected readonly periods: BfSelectOption[] = [
229
+ { value: 'month', label: 'This month' },
230
+ { value: 'year', label: 'This year' },
231
+ ];
232
+ }
233
+ ```
234
+
235
+ ## Desktop
236
+
237
+ `bf-toolbar` is a desktop pattern. Every control and field projected into it resolves its unset `size` to `sm` (32 px tall), because its `providers` set `provideBfDefaults({ size: 'sm' })` for its content; a control that sets `size` itself keeps it, and nothing outside the toolbar changes ([density guide](https://bitforge-ui.web.app/density)). Hover, the pointer cursor and the `focus-visible` ring come from each control; the row adds none of its own. Under a fine pointer each control's visual box is its hit area; under a coarse pointer the `sm` touch extensions reach up to 7.5 px past the visual edge, which the 8 px gap keeps off the neighbouring row.
238
+
239
+ Place it directly under `bf-page-header` and above the `bf-table` or `bf-list` it filters, with the page's own vertical gap (`gap-4`) between them: the page header names the page and holds its page-level actions, the toolbar holds what narrows the collection, and the count in `status` describes the rows below.
240
+
241
+ ## Theming
242
+
243
+ The row has no surface, border or radius of its own; it sits on the page background, and each projected control themes itself.
244
+
245
+ - Status text: `text-caption` in `text-muted`.
246
+ - Layout: `gap-2` (8 px) between items and rows, in logical properties, so under `dir="rtl"` the row mirrors and the actions sit at the left end.
247
+
248
+ ## Accessibility
249
+
250
+ The host is `role="group"`, named by `aria-label` from `label` ("Project filters"). It is a group, not `role="toolbar"`: that role promises arrow-key movement between its controls with one tab stop, and the search field needs the arrow keys to move its caret, so each control keeps its own tab stop and its own keys. Without `label` the group is left unnamed rather than given a generic default: a page has one toolbar under a heading that already names the context, and a stock name such as "Toolbar" would add noise to every focus entry without telling the user anything. Pass a `label` when the page has more than one toolbar.
251
+
252
+ The `status` element is `role="status"` (an `aria-live="polite"` region), always rendered, so a changed count is read after the user's current input finishes. The toolbar sets no focus and handles no keys; <kbd>Tab</kbd> moves through the projected controls in DOM order (filters, view switch, actions).
253
+
254
+ ## Inputs
255
+
256
+ | Name | Type | Default | Description |
257
+ | -------- | -------- | ------- | -------------------------------------------------------------------------------------- |
258
+ | `label` | `string` | `''` | The group's accessible name, such as "Project filters"; unnamed when empty. |
259
+ | `status` | `string` | `''` | The result count or a short state, such as "24 results", announced politely on change. |
260
+
261
+ ## Slots
262
+
263
+ - `default`: The search field and the filters (bf-input, bf-select, bf-chip-group), in order.
264
+ - `view`: A view switch after the filters, such as a bf-segmented of list and grid.
265
+ - `actions`: The trailing actions, kept together at the inline end of the row.
266
+
267
+ ## Notes
268
+
269
+ ### Do
270
+
271
+ - Name each field with `ariaLabel` (or a `label`), and keep the order: search, filters, view, actions.
272
+ - Bind `status` to the number of rows the filters leave, and update it as they change.
273
+
274
+ ### Don't
275
+
276
+ - Don't put the page's title or page-level actions in it; they belong in `bf-page-header`.
277
+ - Don't use it in the Capacitor shell; use `bf-header` actions and a `bf-sheet` of filters.
278
+
279
+ ## Migration notes
280
+
281
+ ### 0.3.1
282
+
283
+ New `bf-toolbar` in `@bitforgehq/ui`: a wrapping `role="group"` row for the search field and
284
+ filters (default slot), a view switch (`bfSlot="view"`) and the trailing actions
285
+ (`bfSlot="actions"`, kept together at the inline end). `label` names the group; `status` renders
286
+ a polite `role="status"` live region for the result count. Its providers set
287
+ `provideBfDefaults({ size: 'sm' })`, so every projected control and field is `sm` unless it sets
288
+ `size` itself. Desktop only; in the Capacitor shell use `bf-header` actions and a `bf-sheet` of
289
+ filters.
@@ -0,0 +1,230 @@
1
+ # bf-tooltip
2
+
3
+ Short hint beside a control on hover or keyboard focus, for desktop
4
+
5
+ `BfTooltipComponent` from `@bitforgehq/ui/overlay` · Overlays · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, signal } from '@angular/core';
11
+ import { BfIconButtonComponent } from '@bitforgehq/ui';
12
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
13
+ import { BfTooltipDirective } from '@bitforgehq/ui/overlay';
14
+ import { Bold, Italic, Link, Underline } from 'lucide';
15
+
16
+ /**
17
+ * The bf-tooltip overview.
18
+ *
19
+ * Live demo of `bfTooltip`: a formatting toolbar of icon buttons, each named by its `label` and
20
+ * showing the same name, with its keyboard shortcut, in a tooltip on hover or keyboard focus. The
21
+ * result line reports each press.
22
+ */
23
+ @Component({
24
+ selector: 'app-example-tooltip-overview',
25
+ imports: [BfIconButtonComponent, BfIconComponent, BfTooltipDirective],
26
+ template: `
27
+ <div class="flex flex-col gap-3">
28
+ <div role="group" aria-label="Formatting" class="flex flex-wrap items-center gap-2">
29
+ <bf-icon-button
30
+ label="Bold"
31
+ variant="text"
32
+ bfTooltip="Bold (Ctrl+B)"
33
+ (clicked)="press('Bold')"
34
+ >
35
+ <bf-icon [icon]="bold" />
36
+ </bf-icon-button>
37
+ <bf-icon-button
38
+ label="Italic"
39
+ variant="text"
40
+ bfTooltip="Italic (Ctrl+I)"
41
+ (clicked)="press('Italic')"
42
+ >
43
+ <bf-icon [icon]="italic" />
44
+ </bf-icon-button>
45
+ <bf-icon-button
46
+ label="Underline"
47
+ variant="text"
48
+ bfTooltip="Underline (Ctrl+U)"
49
+ (clicked)="press('Underline')"
50
+ >
51
+ <bf-icon [icon]="underline" />
52
+ </bf-icon-button>
53
+ <bf-icon-button
54
+ label="Insert link"
55
+ variant="text"
56
+ bfTooltip="Insert link (Ctrl+K)"
57
+ (clicked)="press('Insert link')"
58
+ >
59
+ <bf-icon [icon]="link" />
60
+ </bf-icon-button>
61
+ </div>
62
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
63
+ </div>
64
+ `,
65
+ })
66
+ export class TooltipOverviewExampleComponent {
67
+ readonly bold = Bold;
68
+ readonly italic = Italic;
69
+ readonly underline = Underline;
70
+ readonly link = Link;
71
+ readonly status = signal('Hover or tab to a button for its tooltip.');
72
+
73
+ press(label: string): void {
74
+ this.status.set(`Pressed ${label}.`);
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Overview
80
+
81
+ The small dark pill of a tooltip (role `tooltip`). A `[bfTooltip]` directive renders it in the
82
+ CDK overlay beside its element while the pointer rests on it or it has keyboard focus, so you
83
+ rarely place it yourself; the directive is how you add a tooltip.
84
+
85
+ Short hints for desktop and web apps, from `@bitforgehq/ui/overlay` (needs `@angular/cdk`, no
86
+ Capacitor). Put `bfTooltip="Text"` on a control and a small dark `bf-tooltip` pill shows beside it
87
+ when a mouse rests on it for half a second, or at once when it gets keyboard focus. It hides when
88
+ the pointer leaves, on blur and on `Escape`. While it shows, the control is described by it
89
+ (`aria-describedby`), so a screen reader reads the hint after the control's name. The pill
90
+ renders in the CDK overlay container, so it works inside a `BfDialogService` dialog.
91
+
92
+ Import `BfTooltipDirective` from `@bitforgehq/ui/overlay`. You rarely place `bf-tooltip`
93
+ (`BfTooltipComponent`) yourself: the directive renders it.
94
+
95
+ ## When to use
96
+
97
+ Use a tooltip to repeat the name of an icon-only control (`bf-icon-button`, a collapsed sidebar
98
+ link) for mouse users, or to add a keyboard shortcut to it. A tooltip never holds information
99
+ found nowhere else: touch screens never show it, so the control's own name and the page must say
100
+ everything that matters. Use [bf-popover](https://bitforge-ui.web.app/components/popover) instead for content a user reads at
101
+ length or interacts with (links, fields, buttons), [bf-field](https://bitforge-ui.web.app/components/field) help text for
102
+ guidance on a form field, and in a Capacitor app the control's visible label.
103
+
104
+ ## Placement
105
+
106
+ `bfTooltipPlacement` puts the tooltip on the `top` (default), `bottom`, `start` or `end` side of
107
+ its element, centred along it, 8 px away. `start` and `end` follow the text direction, so they
108
+ swap sides in right-to-left pages. A tooltip without room on its side flips to the opposite side
109
+ first, then to the other two. The placement is read each time the tooltip shows.
110
+
111
+ ```ts
112
+ import { Component } from '@angular/core';
113
+ import { BfButtonComponent } from '@bitforgehq/ui';
114
+ import { BfTooltipDirective, type BfTooltipPlacement } from '@bitforgehq/ui/overlay';
115
+
116
+ /**
117
+ * Tooltips on each side of their button.
118
+ *
119
+ * `bfTooltipPlacement` puts the tooltip on the `top` (default), `bottom`, `start` or `end` side;
120
+ * `start` and `end` follow the text direction, and a tooltip without room flips to the other side.
121
+ */
122
+ @Component({
123
+ selector: 'app-example-tooltip-placement',
124
+ imports: [BfButtonComponent, BfTooltipDirective],
125
+ template: `
126
+ <div class="flex flex-wrap items-center justify-center gap-3 py-8">
127
+ @for (side of sides; track side) {
128
+ <bf-button
129
+ variant="outline"
130
+ size="sm"
131
+ [bfTooltip]="'Shown on the ' + side + ' side'"
132
+ [bfTooltipPlacement]="side"
133
+ >
134
+ {{ side }}
135
+ </bf-button>
136
+ }
137
+ </div>
138
+ `,
139
+ })
140
+ export class TooltipPlacementExampleComponent {
141
+ protected readonly sides: readonly BfTooltipPlacement[] = ['top', 'bottom', 'start', 'end'];
142
+ }
143
+ ```
144
+
145
+ ## Text
146
+
147
+ Keep the text to a few words on one line: the name of the control, and maybe its shortcut
148
+ ("Bold (Ctrl+B)"). Longer text wraps at 20 rem. A bound text that changes while the tooltip shows
149
+ ("Copy link", then "Copied") updates it in place; an empty text shows no tooltip.
150
+
151
+ On an icon-only `bf-icon-button` the tooltip repeats its `label`: the `label` stays the accessible
152
+ name, and the tooltip describes the button. A collapsed `bf-sidebar` link (`a[bfNavItem]`) shows
153
+ its name in the browser's `title` tooltip; add `bfTooltip` with the same text and
154
+ `bfTooltipPlacement="end"` for a styled one that also shows on keyboard focus. Bind it to an empty
155
+ text while the sidebar is expanded, where the label is visible.
156
+
157
+ ## Desktop
158
+
159
+ Built for a mouse and a keyboard. Hovering shows the tooltip after a 500 ms rest, so a pointer
160
+ that only passes over a toolbar shows nothing; moving the pointer from the control onto the
161
+ tooltip keeps it open (so it can be read under a magnifier). Keyboard focus (`focus-visible`)
162
+ shows it at once; a mouse click that focuses the control does not. The tooltip fades in over
163
+ 120 ms, without the fade when the system asks for reduced motion.
164
+
165
+ Under a coarse pointer (`(pointer: coarse)`, touch screens and the Capacitor shell) no tooltip
166
+ ever shows: there is no hover there, and a tooltip after a tap would sit under the finger. That
167
+ is why a tooltip never carries information found nowhere else.
168
+
169
+ ## Theming
170
+
171
+ - Pill: `bg-tooltip` with `text-on-tooltip` (14.9:1 in light, 13.8:1 in dark; the pill is dark
172
+ on a light page and light on a dark one), `rounded-field`, `shadow-raised`, `text-label-xs`,
173
+ 10 px by 4 px padding, at most 20 rem wide.
174
+ - Under forced colours (Windows High Contrast) the pill drops its fill and draws a 1 px border in
175
+ the text colour.
176
+
177
+ ## Accessibility
178
+
179
+ - The pill is `role="tooltip"` with an id. While it shows, the control (the host, or the first
180
+ control inside it, such as the button of a `bf-icon-button`) gets that id added to its
181
+ `aria-describedby`, next to any ids it already has; the id is removed when the tooltip hides.
182
+ - A tooltip is a description, never the name: an icon-only button keeps its `label` or
183
+ `aria-label`.
184
+ - WCAG 1.4.13 (content on hover or focus): the tooltip can be dismissed with `Escape` without
185
+ moving the pointer or focus, stays while the pointer is over it, and stays until the pointer
186
+ leaves, focus leaves or `Escape` is pressed.
187
+ - `Escape` goes to the tooltip first: inside a dialog or a popover, the first `Escape` hides the
188
+ tooltip and the next one closes the dialog.
189
+ - A disabled native button fires no pointer events, so its tooltip never shows; explain why a
190
+ control is disabled in the page instead.
191
+
192
+ ### Keyboard
193
+
194
+ | Key | Action |
195
+ | ------------------------------------------------ | -------------------------------------------------------- |
196
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Focusing the control shows its tooltip; leaving hides it |
197
+ | <kbd>Escape</kbd> | Hides the tooltip, focus stays where it is |
198
+
199
+ ## Inputs
200
+
201
+ | Name | Type | Default | Description |
202
+ | ------ | -------- | ------- | -------------------------------------------------------------------------- |
203
+ | `text` | `string` | `''` | The tooltip's text: a few words that repeat or explain the control's name. |
204
+
205
+ ## Notes
206
+
207
+ ### Do
208
+
209
+ - Repeat the name of icon-only controls, and add their keyboard shortcut.
210
+ - Keep the text to a few words.
211
+ - Put tooltips on controls that take focus, so keyboard users get them too.
212
+
213
+ ### Don't
214
+
215
+ - Don't put information only in a tooltip: touch screens never show it.
216
+ - Don't put links, buttons or other controls in a tooltip; use a popover.
217
+ - Don't use a tooltip as the accessible name of a control.
218
+ - Don't add a tooltip that repeats a visible text label word for word.
219
+
220
+ ## Migration notes
221
+
222
+ ### 0.3.1
223
+
224
+ New `[bfTooltip]` (`BfTooltipDirective`) and its `bf-tooltip` pill (`BfTooltipComponent`) in
225
+ `@bitforgehq/ui/overlay`: a short hint beside a control after a 500 ms hover or at once on keyboard
226
+ focus, hidden on leave, blur and `Escape`, and kept while the pointer moves onto it (WCAG 1.4.13).
227
+ `bfTooltipPlacement` (`BfTooltipPlacement`: `top` default, `bottom`, `start`, `end`, logical)
228
+ flips when it would overflow. While it shows, the control is described by it
229
+ (`aria-describedby`). Nothing shows under a coarse pointer. Fades in over 120 ms, not under
230
+ reduced motion. New `BfTooltipHarness` in `@bitforgehq/ui/testing`.
@@ -0,0 +1,159 @@
1
+ # bf-top-nav
2
+
3
+ Horizontal app navigation, a wrapping row of links for apps with few sections
4
+
5
+ `BfTopNavComponent` from `@bitforgehq/ui` · Navigation · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { RouterLink, RouterLinkActive } from '@angular/router';
12
+ import { BfNavItemComponent, BfTopNavComponent } from '@bitforgehq/ui';
13
+
14
+ /**
15
+ * The bf-top-nav overview.
16
+ *
17
+ * Live demo of `bf-top-nav`: a row of `a[bfNavItem]` router links for an app with few sections.
18
+ * This page is the current one; in a narrow container the row wraps onto more lines.
19
+ */
20
+ @Component({
21
+ selector: 'app-example-top-nav-overview',
22
+ imports: [RouterLink, RouterLinkActive, BfNavItemComponent, BfTopNavComponent],
23
+ template: `
24
+ <bf-top-nav label="Desktop navigation">
25
+ @for (link of links; track link.path) {
26
+ <a bfNavItem [routerLink]="link.path" routerLinkActive ariaCurrentWhenActive="page">{{
27
+ link.label
28
+ }}</a>
29
+ }
30
+ </bf-top-nav>
31
+ `,
32
+ })
33
+ export class TopNavOverviewExampleComponent {
34
+ protected readonly links: readonly { path: string; label: string }[] = [
35
+ { path: '/components/sidebar', label: 'Sidebar' },
36
+ { path: '/components/top-nav', label: 'Top nav' },
37
+ { path: '/components/nav-item', label: 'Nav item' },
38
+ { path: '/components/nav-group', label: 'Nav group' },
39
+ ];
40
+ }
41
+ ```
42
+
43
+ ## Overview
44
+
45
+ The horizontal navigation of a desktop app with few sections: a `nav` landmark holding a row
46
+ of `a[bfNavItem]` links that wraps onto more lines in a narrow container. Each link marks the
47
+ current page from `aria-current="page"`, as in `bf-sidebar`.
48
+
49
+ The horizontal navigation of a desktop web app with few sections: a `nav` landmark holding a row of [`a[bfNavItem]`](https://bitforge-ui.web.app/components/nav-item) links, usually in the app's header bar. In a narrow container the row wraps onto more lines. It shares its item with [`bf-sidebar`](https://bitforge-ui.web.app/components/sidebar), so the current page, the count badge and the focus ring look the same in both.
50
+
51
+ ## When to use
52
+
53
+ Use `bf-top-nav` for the two to five top-level destinations of a desktop web app. Use [bf-sidebar](https://bitforge-ui.web.app/components/sidebar) instead for more sections or sections in groups, and [bf-segmented](https://bitforge-ui.web.app/components/segmented) to switch a view in place. In the Capacitor shell use [bf-tabs](https://bitforge-ui.web.app/components/tabs) with [bf-tab-bar](https://bitforge-ui.web.app/components/tab-bar) instead.
54
+
55
+ ## Basic usage
56
+
57
+ Put `bfNavItem` on your own `<a routerLink routerLinkActive ariaCurrentWhenActive="page">`: the router sets `aria-current="page"` on the current page's link and the item shows it in the tonal fill. Items lay out inline at their own width; an icon in `bfSlot="icon"` and a `badge` work as in the sidebar. Name the landmark with `label` (it falls back to `provideBfLabels()`'s `navigation`, "Main").
58
+
59
+ ## Icons and badges
60
+
61
+ An icon in `bfSlot="icon"` sits before the label and `badge` adds a count pill, announced as "4 new", as in the sidebar.
62
+
63
+ ```ts
64
+ import { Component } from '@angular/core';
65
+ import { RouterLink, RouterLinkActive } from '@angular/router';
66
+ import { BfNavItemComponent, BfTopNavComponent } from '@bitforgehq/ui';
67
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
68
+ import { ChartColumn, Inbox, LayoutDashboard } from 'lucide';
69
+
70
+ /**
71
+ * Icons and a count badge in a top nav.
72
+ *
73
+ * Each `a[bfNavItem]` takes an icon in `bfSlot="icon"` and a `badge`, as in the sidebar.
74
+ */
75
+ @Component({
76
+ selector: 'app-example-top-nav-icons',
77
+ imports: [RouterLink, RouterLinkActive, BfIconComponent, BfNavItemComponent, BfTopNavComponent],
78
+ template: `
79
+ <bf-top-nav label="Sections with icons">
80
+ <a bfNavItem routerLink="/components/top-nav" routerLinkActive ariaCurrentWhenActive="page">
81
+ <bf-icon bfSlot="icon" [icon]="dashboard" />
82
+ Overview
83
+ </a>
84
+ <a
85
+ bfNavItem
86
+ routerLink="/components/badge"
87
+ routerLinkActive
88
+ ariaCurrentWhenActive="page"
89
+ [badge]="4"
90
+ >
91
+ <bf-icon bfSlot="icon" [icon]="inbox" />
92
+ Inbox
93
+ </a>
94
+ <a bfNavItem routerLink="/density" routerLinkActive ariaCurrentWhenActive="page">
95
+ <bf-icon bfSlot="icon" [icon]="chart" />
96
+ Reports
97
+ </a>
98
+ </bf-top-nav>
99
+ `,
100
+ })
101
+ export class TopNavIconsExampleComponent {
102
+ protected readonly dashboard = LayoutDashboard;
103
+ protected readonly inbox = Inbox;
104
+ protected readonly chart = ChartColumn;
105
+ }
106
+ ```
107
+
108
+ ## Small screens
109
+
110
+ The row wraps rather than scrolling sideways. When it would wrap onto too many lines, drive a [`bf-menu-button`](https://bitforge-ui.web.app/components/menu-button) of `a[bfMenuItem]` links below a breakpoint from the same array (`class="hidden md:block"` on `bf-top-nav`, `class="md:hidden"` on the menu button), as the [sidebar's responsive layout](https://bitforge-ui.web.app/components/sidebar) shows.
111
+
112
+ ## Desktop
113
+
114
+ The top nav is a desktop pattern. Its items take a `surface-muted` fill and the pointer on hover, a `surface-pressed` fill while pressed and the focus ring on keyboard focus only; the current page keeps its tonal fill with its own hover and pressed steps. Item height follows `provideBfDefaults()` (`controlSize`); under a coarse pointer every item is at least 44 px tall. Ctrl/Cmd-click and a middle click open a link in a new tab.
115
+
116
+ ## Theming
117
+
118
+ - Layout: `flex flex-wrap` with a 4 px gap; no surface of its own, so it sits on the header bar or page behind it.
119
+ - The items' tokens are on the [nav item](https://bitforge-ui.web.app/components/nav-item#theming) page.
120
+
121
+ ## Accessibility
122
+
123
+ `bf-top-nav` renders a native `<nav>` landmark named by `label`, else `provideBfLabels()`'s `navigation` ("Main"); give each landmark on a page a different name. Its links are plain `<a>` elements in DOM order; the current page is the one with `aria-current="page"`, which screen readers announce ("current page") and which an outline marks under forced colours.
124
+
125
+ ### Keyboard
126
+
127
+ | Key | Action |
128
+ | ------------------------------------------------ | ------------------------ |
129
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves between the links |
130
+ | <kbd>Enter</kbd> | Follows the focused link |
131
+
132
+ ## Inputs
133
+
134
+ | Name | Type | Default | Description |
135
+ | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `label` | `string` | `''` | Accessible name of the navigation; empty falls back to `provideBfLabels()`'s `navigation` ("Main"). Name each landmark differently when a page has more than one. |
137
+
138
+ ## Slots
139
+
140
+ - `default`: The destinations, `a[bfNavItem]` links.
141
+
142
+ ## Notes
143
+
144
+ ### Do
145
+
146
+ - Keep it to a few short labels that fit one line at the usual window width.
147
+ - Mark the current page with `routerLinkActive` and `ariaCurrentWhenActive="page"`.
148
+
149
+ ### Don't
150
+
151
+ - Don't put more than five or six destinations in it; use a `bf-sidebar`.
152
+ - Don't use it to switch a view inside a page; use a `bf-segmented`.
153
+
154
+ ## Migration notes
155
+
156
+ ### 0.3.1
157
+
158
+ New `bf-top-nav`: the horizontal navigation of a desktop app with few sections, a `nav` row of
159
+ `a[bfNavItem]` links that wraps in a narrow container, named by `label` (else `navigation`).