@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,188 @@
1
+ # bf-sparkline
2
+
3
+ Tiny trend line with no axes or legend, for stats and table cells
4
+
5
+ `BfSparklineComponent` from `@bitforgehq/ui/charts` · Data display · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { BfStatComponent } from '@bitforgehq/ui';
12
+ import { BfSparklineComponent } from '@bitforgehq/ui/charts';
13
+
14
+ /**
15
+ * The bf-sparkline overview.
16
+ *
17
+ * Live demo of `bf-sparkline`: three stats with a 30-day trend in their supporting line, the
18
+ * trend also stated in words; the middle one fills its area.
19
+ */
20
+ @Component({
21
+ selector: 'app-example-sparkline-overview',
22
+ imports: [BfStatComponent, BfSparklineComponent],
23
+ template: `
24
+ <div class="grid w-full grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-3">
25
+ <bf-stat label="Revenue" value="CHF 48.2k" variant="card">
26
+ <span class="flex flex-col gap-1">
27
+ <bf-sparkline [values]="revenue" />
28
+ Up 12% in 30 days
29
+ </span>
30
+ </bf-stat>
31
+ <bf-stat label="Sign-ups" value="1,204" variant="card">
32
+ <span class="flex flex-col gap-1">
33
+ <bf-sparkline area curve="smooth" [values]="signups" />
34
+ Up 4% in 30 days
35
+ </span>
36
+ </bf-stat>
37
+ <bf-stat label="Churn" value="2.1%" variant="card">
38
+ <span class="flex flex-col gap-1">
39
+ <bf-sparkline [values]="churn" />
40
+ Down 0.4 points in 30 days
41
+ </span>
42
+ </bf-stat>
43
+ </div>
44
+ `,
45
+ })
46
+ export class SparklineOverviewExampleComponent {
47
+ readonly revenue = [31, 34, 33, 36, 35, 39, 42, 41, 45, 48];
48
+ readonly signups = [980, 1010, 1060, 1040, 1100, 1120, 1150, 1170, 1190, 1204];
49
+ readonly churn = [2.6, 2.5, 2.6, 2.4, 2.4, 2.3, 2.3, 2.2, 2.2, 2.1];
50
+ }
51
+ ```
52
+
53
+ ## Overview
54
+
55
+ A tiny line of one series with no axes, legend or tooltip, for a trend beside a figure (in a
56
+ `bf-stat`'s supporting line, a table cell). Its scale spans the values' own range, so it shows
57
+ the shape of the trend, not its size; a dot marks the latest value. With a `label` it is an
58
+ image with that name (say what the trend means, such as "Revenue up 12% over 30 days"); without
59
+ one it is decorative and hidden from assistive technology. It draws in on the first render and
60
+ on every data change, still under reduced motion; under `dir="rtl"` it runs from the right.
61
+
62
+ A sparkline is a word-sized line of one series with no axes, legend or tooltip: the shape of a trend beside a figure. `bf-sparkline` draws it in SVG from `values`, oldest first, scaled to their own range, with a dot on the latest value. It lives in `@bitforgehq/ui/charts` (`import { BfSparklineComponent } from '@bitforgehq/ui/charts'`) and works the same in a desktop web app and in the Capacitor shell.
63
+
64
+ ## When to use
65
+
66
+ Use `bf-sparkline` in a [bf-stat](https://bitforge-ui.web.app/components/stat)'s supporting line, a table cell or a list row, where the figure carries the number and the line only its direction. Use [bf-line-chart](https://bitforge-ui.web.app/components/line-chart) instead when readers need the values, the axis or more than one series.
67
+
68
+ ## Area and size
69
+
70
+ `area` fills under the line; `curve="smooth"` draws a monotone curve. `height` sets the height in px (32 by default); the width follows the container, so give the element a width (`class="w-24"`) where it sits in a row. A flat series draws a level line in the middle, and a `null` value leaves a gap. The line draws in on the first render and whenever `values` change.
71
+
72
+ ```ts
73
+ import { Component } from '@angular/core';
74
+ import { BfSparklineComponent } from '@bitforgehq/ui/charts';
75
+
76
+ interface Region {
77
+ readonly name: string;
78
+ readonly total: string;
79
+ readonly trend: readonly (number | null)[];
80
+ readonly summary: string;
81
+ }
82
+
83
+ /**
84
+ * Sparklines in a table.
85
+ *
86
+ * A fixed-width sparkline per row, each named for screen readers by what its trend means; a
87
+ * `null` leaves a gap where a week was not reported.
88
+ */
89
+ @Component({
90
+ selector: 'app-example-sparkline-table',
91
+ imports: [BfSparklineComponent],
92
+ template: `
93
+ <table class="text-label-sm w-full text-start">
94
+ <caption class="sr-only">
95
+ Orders by region
96
+ </caption>
97
+ <thead>
98
+ <tr class="border-border border-b">
99
+ <th scope="col" class="text-label-xs text-text-muted h-8 px-3 text-start">Region</th>
100
+ <th scope="col" class="text-label-xs text-text-muted h-8 px-3 text-end">Orders</th>
101
+ <th scope="col" class="text-label-xs text-text-muted h-8 px-3 text-start">8 weeks</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ @for (region of regions; track region.name) {
106
+ <tr class="border-divider border-b">
107
+ <th scope="row" class="text-text h-10 px-3 text-start font-bold">{{ region.name }}</th>
108
+ <td class="text-text h-10 px-3 text-end tabular-nums">{{ region.total }}</td>
109
+ <td class="h-10 px-3">
110
+ <bf-sparkline
111
+ class="w-24"
112
+ [height]="24"
113
+ [values]="region.trend"
114
+ [label]="region.summary"
115
+ />
116
+ </td>
117
+ </tr>
118
+ }
119
+ </tbody>
120
+ </table>
121
+ `,
122
+ })
123
+ export class SparklineTableExampleComponent {
124
+ readonly regions: readonly Region[] = [
125
+ {
126
+ name: 'North',
127
+ total: '1,284',
128
+ trend: [120, 132, 128, 141, 150, 149, 162, 171],
129
+ summary: 'Rising',
130
+ },
131
+ {
132
+ name: 'Central',
133
+ total: '962',
134
+ trend: [131, 127, 119, null, 112, 118, 109, 104],
135
+ summary: 'Falling',
136
+ },
137
+ {
138
+ name: 'South',
139
+ total: '1,047',
140
+ trend: [128, 131, 126, 133, 129, 132, 130, 131],
141
+ summary: 'Steady',
142
+ },
143
+ ];
144
+ }
145
+ ```
146
+
147
+ ## Desktop
148
+
149
+ A sparkline is not interactive: no hover, no tooltip, no focus stop. Its size does not follow `provideBfDefaults()`. Inside `provideBfShell()` it renders the same.
150
+
151
+ ## Theming
152
+
153
+ - Line: `--color-chart-1` (`stroke-chart-1`), 2 px; area the same at 16 % (`fill-chart-1/16`); the end dot `fill-chart-1`.
154
+ - Motion: `--animate-chart-draw` and `--animate-chart-fade`; none under `prefers-reduced-motion`.
155
+
156
+ ## Accessibility
157
+
158
+ With a `label` the SVG is a `role="img"` named by it: say what the trend means ("Revenue up 12% over 30 days"), not that it is a chart. Without one it is decorative and hidden from assistive technology, which is right beside a figure whose supporting text already states the trend. It has no focus stop and no data table; put the values in a table or a `bf-line-chart` when readers need them.
159
+
160
+ ## Inputs
161
+
162
+ | Name | Type | Default | Description |
163
+ | -------- | --------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
164
+ | `values` | `readonly (number \| null)[]` | required | The values in order, oldest first; `null` leaves a gap. |
165
+ | `label` | `string` | `''` | What the trend means, its accessible name ("Revenue up 12% this month"); empty makes the sparkline decorative. |
166
+ | `height` | `number` | `32` | Height in px; the width follows the container. |
167
+ | `area` | `boolean` | `false` | Fills the area under the line. |
168
+ | `curve` | `BfChartCurve` (`'linear' \| 'smooth'`) | `'linear'` | The line between points: `linear` (default) or `smooth`, a monotone curve. |
169
+
170
+ ## Notes
171
+
172
+ ### Do
173
+
174
+ - State the trend in words next to it (the stat's supporting line, a `+12%`), so it is never colour or shape alone.
175
+ - Use the same time window for every sparkline in a row.
176
+
177
+ ### Don't
178
+
179
+ - Don't compare sparklines' heights: each one is scaled to its own range.
180
+ - Don't put a tooltip or a link on a sparkline; link the stat or the row instead.
181
+
182
+ ## Migration notes
183
+
184
+ ### 0.3.1
185
+
186
+ New `bf-sparkline` in `@bitforgehq/ui/charts`: a tiny line of one series with no axes, legend or
187
+ tooltip, for a `bf-stat`'s supporting line or a table cell; `area`, `curve` and `height`, a dot on
188
+ the latest value. With a `label` it is a named image, without one it is decorative.
@@ -0,0 +1,233 @@
1
+ # bf-stat
2
+
3
+ One labelled figure with an optional icon tile and a supporting line
4
+
5
+ `BfStatComponent` from `@bitforgehq/ui` · Data display · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component } from '@angular/core';
11
+ import { BfStatComponent } from '@bitforgehq/ui';
12
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
13
+ import { Clock, CookingPot, Timer, Users } from 'lucide';
14
+
15
+ /**
16
+ * The bf-stat overview.
17
+ *
18
+ * Live demo of `bf-stat`: a recipe's prep time, cook time, total time and servings as four card
19
+ * stats, each with an icon tile, wrapping from four across to two.
20
+ */
21
+ @Component({
22
+ selector: 'app-example-stat-overview',
23
+ imports: [BfStatComponent, BfIconComponent],
24
+ template: `
25
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
26
+ <bf-stat label="Prep time" value="15 min" variant="card">
27
+ <bf-icon bfSlot="icon" [icon]="timer" />
28
+ </bf-stat>
29
+ <bf-stat label="Cook time" value="30 min" variant="card">
30
+ <bf-icon bfSlot="icon" [icon]="cookingPot" />
31
+ </bf-stat>
32
+ <bf-stat label="Total time" value="45 min" variant="card">
33
+ <bf-icon bfSlot="icon" [icon]="clock" />
34
+ </bf-stat>
35
+ <bf-stat label="Servings" value="4" variant="card">
36
+ <bf-icon bfSlot="icon" [icon]="users" />
37
+ </bf-stat>
38
+ </div>
39
+ `,
40
+ })
41
+ export class StatOverviewExampleComponent {
42
+ protected readonly timer = Timer;
43
+ protected readonly cookingPot = CookingPot;
44
+ protected readonly clock = Clock;
45
+ protected readonly users = Users;
46
+ }
47
+ ```
48
+
49
+ ## Overview
50
+
51
+ One figure with its label, such as a count, a duration or a total: a native `dl` pair with the
52
+ value set large, an optional icon tile at the start and a supporting line under the value.
53
+
54
+ A `delta` with a `trend` adds a pill beside the value, toned by `trendIntent`.
55
+
56
+ A stat shows one figure with its label: a recipe's cook time, a project's open tasks, a month's revenue. The value is set large, an optional icon tile sits at the start, a supporting line under the value adds context such as a period or a badge, and an optional delta pill beside the value shows the change and its direction. `bf-stat` renders its own native `dl` with the label as the `dt` and the value as the `dd`. It works the same in both shells: in the Capacitor shell and in a desktop web app.
57
+
58
+ ## When to use
59
+
60
+ Use `bf-stat` for a few headline figures the reader should take in at a glance, usually in a row or grid of them. Use [bf-description-list](https://bitforge-ui.web.app/components/description-list) instead for many details of one record, and [bf-card](https://bitforge-ui.web.app/components/card) instead when the tile holds more than one figure (a title, a progress bar, actions).
61
+
62
+ ## Cards
63
+
64
+ `variant="card"` gives each stat its own outlined tile, the `flat` card surface. In a grid the cards stretch to the row's height, so a row of stats lines up even when one has a supporting line and another has none. Put an icon in the `icon` slot (`bfSlot="icon"`, a `bf-icon`) to show what the figure is about.
65
+
66
+ ```ts
67
+ import { Component } from '@angular/core';
68
+ import { BfStatComponent } from '@bitforgehq/ui';
69
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
70
+ import { Clock, CookingPot, Timer, Users } from 'lucide';
71
+
72
+ /**
73
+ * The bf-stat overview.
74
+ *
75
+ * Live demo of `bf-stat`: a recipe's prep time, cook time, total time and servings as four card
76
+ * stats, each with an icon tile, wrapping from four across to two.
77
+ */
78
+ @Component({
79
+ selector: 'app-example-stat-overview',
80
+ imports: [BfStatComponent, BfIconComponent],
81
+ template: `
82
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
83
+ <bf-stat label="Prep time" value="15 min" variant="card">
84
+ <bf-icon bfSlot="icon" [icon]="timer" />
85
+ </bf-stat>
86
+ <bf-stat label="Cook time" value="30 min" variant="card">
87
+ <bf-icon bfSlot="icon" [icon]="cookingPot" />
88
+ </bf-stat>
89
+ <bf-stat label="Total time" value="45 min" variant="card">
90
+ <bf-icon bfSlot="icon" [icon]="clock" />
91
+ </bf-stat>
92
+ <bf-stat label="Servings" value="4" variant="card">
93
+ <bf-icon bfSlot="icon" [icon]="users" />
94
+ </bf-stat>
95
+ </div>
96
+ `,
97
+ })
98
+ export class StatOverviewExampleComponent {
99
+ protected readonly timer = Timer;
100
+ protected readonly cookingPot = CookingPot;
101
+ protected readonly clock = Clock;
102
+ protected readonly users = Users;
103
+ }
104
+ ```
105
+
106
+ ## Plain
107
+
108
+ `variant="plain"` (the default) draws no surface, for stats that sit on a card or a page section of their own. The default slot is the supporting line under the value: a short trend ("+12% this week") or a `bf-badge`.
109
+
110
+ ```ts
111
+ import { Component } from '@angular/core';
112
+ import { BfBadgeComponent, BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
113
+
114
+ /** Plain stats on a card, with a trend and a badge as their supporting lines. */
115
+ @Component({
116
+ selector: 'app-example-stat-plain',
117
+ imports: [BfBadgeComponent, BfCardComponent, BfStatComponent],
118
+ template: `
119
+ <bf-card>
120
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-4">
121
+ <bf-stat label="Recipes saved" value="1,204">+12% this week</bf-stat>
122
+ <bf-stat label="Average rating" value="4.8">From 312 reviews</bf-stat>
123
+ <bf-stat label="Shopping list" value="9 items">
124
+ <bf-badge variant="warning">3 missing</bf-badge>
125
+ </bf-stat>
126
+ </div>
127
+ </bf-card>
128
+ `,
129
+ })
130
+ export class StatPlainExampleComponent {}
131
+ ```
132
+
133
+ ## Trend
134
+
135
+ `delta` (the change, already formatted, such as `+12%`) and `trend` (`up`, `down` or `flat`) add a pill beside the value; on a narrow stat it wraps under it. The pill carries an arrow mark (a chevron up or down, a dash for no change) and, for screen readers, the direction as a word before the delta ("Up +12%"), so the colour is never the only signal. An empty `delta` shows no pill; a `delta` without a `trend` shows a neutral pill with no mark.
136
+
137
+ The colour says whether the change is good, which depends on the figure. `trendIntent="positive"` (the default) is for figures where up is good, such as revenue or sign-ups: up is `success`, down is `danger`. `trendIntent="negative"` is for figures where down is good, such as costs, churn or load time: down is `success`, up is `danger`. `flat` is neutral in both. Put the comparison ("vs. last month") in the supporting line.
138
+
139
+ ```ts
140
+ import { Component } from '@angular/core';
141
+ import { BfStatComponent } from '@bitforgehq/ui';
142
+
143
+ /**
144
+ * Stats with a trend.
145
+ *
146
+ * Revenue up (good), costs down (good, so `trendIntent="negative"`) and churn unchanged, each with
147
+ * a delta pill beside the value.
148
+ */
149
+ @Component({
150
+ selector: 'app-example-stat-trend',
151
+ imports: [BfStatComponent],
152
+ template: `
153
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(11rem,1fr))] gap-3">
154
+ <bf-stat label="Revenue" value="€48,200" delta="+12%" trend="up" variant="card">
155
+ vs. last month
156
+ </bf-stat>
157
+ <bf-stat
158
+ label="Costs"
159
+ value="€12,900"
160
+ delta="-8%"
161
+ trend="down"
162
+ trendIntent="negative"
163
+ variant="card"
164
+ >
165
+ vs. last month
166
+ </bf-stat>
167
+ <bf-stat label="Churn" value="2.1%" delta="0%" trend="flat" variant="card">
168
+ vs. last month
169
+ </bf-stat>
170
+ </div>
171
+ `,
172
+ })
173
+ export class StatTrendExampleComponent {}
174
+ ```
175
+
176
+ ## Desktop
177
+
178
+ A stat is not interactive: no hover, no pointer cursor, no focus stop. Its size does not follow `provideBfDefaults()`; the figure keeps the `title` type on every density. Inside `provideBfShell()` (the Capacitor shell) it renders and behaves exactly the same. A grid of stats wraps by itself with `grid-cols-[repeat(auto-fit,minmax(9rem,1fr))]` on the grid that holds them, so the same row reads as four across on a desktop and two on a phone.
179
+
180
+ ## Theming
181
+
182
+ - Label (`dt`): `text-caption` in `text-muted`. Value (`dd`): `text-title` in `text`, with tabular figures (`tabular-nums`) so numbers line up across stats.
183
+ - Supporting line: `text-caption` in `text-muted`.
184
+ - Delta pill: the badge's label shape (`control` radius, `text-micro` bold, tabular figures); good changes `success-soft` with `success` text, bad ones `danger-soft` with `danger` text, flat or no trend `neutral-soft` with `neutral` text. The arrow mark is drawn with borders in `currentColor`, so it takes the pill's text colour.
185
+ - Icon tile: `surface-sunken` background, `text-muted` icon, `tile` radius, 40 px.
186
+ - `card`: `surface`, the `card` radius, `--spacing-card` padding (`p-card`) and a 1.5 px `border-strong` edge, as the `flat` `bf-card`.
187
+
188
+ ## Accessibility
189
+
190
+ `bf-stat` renders a native `dl` holding the label as the `dt`, the value as a `dd` and the supporting line as a second `dd`, so a screen reader announces the label with its value and context. The icon tile sits outside the `dl`; keep the icon decorative (a `bf-icon` without a `label` is hidden from assistive technology). The delta pill sits in the value's `dd`, so it is read with the value; its arrow mark is `aria-hidden`, and a visually hidden word (the `statTrendUp`, `statTrendDown` and `statTrendFlat` labels, "Up", "Down", "No change"; translate them with `provideBfLabels()`) names the direction before the delta. The pill uses the badge's tone pairs, so its text keeps the badge's contrast in both modes; in forced colours the mark still shows, as it is drawn with borders. The stat adds no other ARIA.
191
+
192
+ ## Inputs
193
+
194
+ | Name | Type | Default | Description |
195
+ | ------------- | -------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------- |
196
+ | `label` | `string` | required | What the figure measures, shown above it. |
197
+ | `value` | `string` | required | The figure, already formatted (`45 min`, `1,204`, `98%`). |
198
+ | `variant` | `BfStatVariant` (`'plain' \| 'card'`) | `'plain'` | Surface: plain (default) on its container, or card, an outlined tile of its own. |
199
+ | `delta` | `string` | `''` | A change shown in a pill beside the value, already formatted (`+12%`); empty shows none. |
200
+ | `trend` | `BfStatTrend \| undefined` (`'up' \| 'down' \| 'flat' \| undefined`) | `undefined` | The delta's direction: an arrow mark and a hidden word; left unset, the pill is neutral. |
201
+ | `trendIntent` | `BfStatTrendIntent` (`'positive' \| 'negative'`) | `'positive'` | Whether up is good (positive, default) or down is good (negative, such as costs). |
202
+
203
+ ## Slots
204
+
205
+ - `icon`: The icon in the tile at the start.
206
+ - `default`: A supporting line under the value, such as a trend or a badge.
207
+
208
+ ## Notes
209
+
210
+ ### Do
211
+
212
+ - Format the value before passing it (`45 min`, `1,204`, `98%`); the stat prints it as given.
213
+ - Keep the supporting line to a few words.
214
+ - Set `trendIntent="negative"` for figures where a fall is good news (costs, churn, errors).
215
+
216
+ ### Don't
217
+
218
+ - Don't use a stat for a value the user edits or acts on; give it its own control.
219
+ - Don't put long text in `value`; it is set at the `title` size for a short figure.
220
+ - Don't put the comparison period in `delta`; keep it to the change (`+12%`) and say "vs. last month" in the supporting line.
221
+
222
+ ## Migration notes
223
+
224
+ ### 0.3.1
225
+
226
+ New: `bf-stat`, one labelled figure in its own `dl`, with the value at the `title` size in
227
+ tabular figures, an `icon` slot for a tile at the start and a supporting line in the default slot.
228
+ `variant="card"` gives it the outlined `flat` card surface; `plain` (default) draws none. An
229
+ opt-in trend: `delta` (the formatted change, such as `+12%`; empty shows nothing) with `trend`
230
+ (`up`, `down`, `flat`) renders a pill beside the value with a CSS arrow mark and the direction as a
231
+ hidden word (new labels `statTrendUp`, `statTrendDown`, `statTrendFlat`). `trendIntent`
232
+ (`positive` default, up is good; `negative` when down is good, such as costs) picks `success` or
233
+ `danger`; flat is neutral. New types `BfStatVariant`, `BfStatTrend` and `BfStatTrendIntent`.
@@ -0,0 +1,226 @@
1
+ # bf-tab-group
2
+
3
+ In-page tabs that switch between content panels, with keyboard navigation and badges
4
+
5
+ `BfTabGroupComponent` from `@bitforgehq/ui` · Navigation · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, signal } from '@angular/core';
11
+ import { BfTabGroupComponent, BfTabPanelComponent } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-tab-group overview.
15
+ *
16
+ * Live demo of `bf-tab-group`: a project's Overview, Activity and Settings sections, Activity with
17
+ * a count badge; click a tab, or focus one and use the arrow keys, Home and End.
18
+ */
19
+ @Component({
20
+ selector: 'app-example-tab-group-overview',
21
+ imports: [BfTabGroupComponent, BfTabPanelComponent],
22
+ template: `
23
+ <bf-tab-group label="Project sections" [(selected)]="tab">
24
+ <bf-tab-panel value="overview" label="Overview">
25
+ <p class="text-body">Atlas ships the new onboarding flow in March.</p>
26
+ </bf-tab-panel>
27
+ <bf-tab-panel value="activity" label="Activity" [badge]="3">
28
+ <ng-template>
29
+ <p class="text-body">Mara commented on the brief, and two tasks were closed.</p>
30
+ </ng-template>
31
+ </bf-tab-panel>
32
+ <bf-tab-panel value="settings" label="Settings">
33
+ <ng-template>
34
+ <p class="text-body">Only owners can change the project settings.</p>
35
+ </ng-template>
36
+ </bf-tab-panel>
37
+ </bf-tab-group>
38
+ <p data-status aria-live="polite" class="text-caption text-text-muted mt-3">
39
+ Selected tab: {{ tab() }}
40
+ </p>
41
+ `,
42
+ })
43
+ export class TabGroupOverviewExampleComponent {
44
+ readonly tab = signal('overview');
45
+ }
46
+ ```
47
+
48
+ ## Overview
49
+
50
+ In-page tabs: a row of tabs over the content they switch, one `bf-tab-panel` per tab. The
51
+ `selected` model holds the shown panel's `value`; unset or unknown, the first enabled panel
52
+ shows. Selection follows focus: the arrow keys (mirrored under `dir="rtl"`), Home and End move
53
+ to a tab and show its panel. When the tabs do not fit, the row scrolls sideways.
54
+
55
+ In-page tabs: a row of tabs over the content they switch, such as Overview / Activity / Settings on a project's detail page. `bf-tab-group` holds the row and the selection; each tab and its content is a [`bf-tab-panel`](https://bitforge-ui.web.app/components/tab-panel). It works the same in a desktop web app and in the Capacitor shell.
56
+
57
+ ## When to use
58
+
59
+ Use `bf-tab-group` to split one page's content into a few sections that the user switches in place, without leaving the page. 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 for the app's top-level destinations at the bottom of the screen: those are routes, these are sections of one page. Use [bf-top-nav](https://bitforge-ui.web.app/components/top-nav) with `a[bfNavItem]` links instead when each tab is its own route (a URL to share or open in a new tab): links are navigation, tabs switch content in place. Use [bf-segmented](https://bitforge-ui.web.app/components/segmented) for a two-to-four-way switch of one view, such as a chart's range.
60
+
61
+ ## Badges and icons
62
+
63
+ A panel's `badge` adds a count pill after its tab's label, announced as "3 new"; an icon in the panel's `bfSlot="icon"` shows in the tab before the label. A `disabled` panel's tab cannot be picked.
64
+
65
+ ```ts
66
+ import { Component } from '@angular/core';
67
+ import { BfTabGroupComponent, BfTabPanelComponent } from '@bitforgehq/ui';
68
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
69
+ import { Activity, FileText, LayoutDashboard, Lock } from 'lucide';
70
+
71
+ /**
72
+ * Tabs with icons, a count badge and a disabled tab.
73
+ *
74
+ * Each panel's `bfSlot="icon"` shows in its tab before the label; `badge` adds an announced
75
+ * count, and `disabled` takes a tab out of the selection and the keyboard navigation.
76
+ */
77
+ @Component({
78
+ selector: 'app-example-tab-group-badges-and-icons',
79
+ imports: [BfTabGroupComponent, BfTabPanelComponent, BfIconComponent],
80
+ template: `
81
+ <bf-tab-group label="Project with icons">
82
+ <bf-tab-panel value="overview" label="Overview">
83
+ <bf-icon bfSlot="icon" [icon]="dashboard" />
84
+ <p class="text-body">The project at a glance.</p>
85
+ </bf-tab-panel>
86
+ <bf-tab-panel value="activity" label="Activity" [badge]="12">
87
+ <bf-icon bfSlot="icon" [icon]="activity" />
88
+ <p class="text-body">12 updates since your last visit.</p>
89
+ </bf-tab-panel>
90
+ <bf-tab-panel value="files" label="Files">
91
+ <bf-icon bfSlot="icon" [icon]="files" />
92
+ <p class="text-body">The brief and two mock-ups.</p>
93
+ </bf-tab-panel>
94
+ <bf-tab-panel value="billing" label="Billing" [disabled]="true">
95
+ <bf-icon bfSlot="icon" [icon]="lock" />
96
+ <p class="text-body">Billing is managed by the workspace owner.</p>
97
+ </bf-tab-panel>
98
+ </bf-tab-group>
99
+ `,
100
+ })
101
+ export class TabGroupBadgesAndIconsExampleComponent {
102
+ protected readonly dashboard = LayoutDashboard;
103
+ protected readonly activity = Activity;
104
+ protected readonly files = FileText;
105
+ protected readonly lock = Lock;
106
+ }
107
+ ```
108
+
109
+ ## Lazy content
110
+
111
+ Put a panel's content in an `ng-template` inside it and the content is created only while its tab is selected, and destroyed when another tab shows: a heavy chart or a feed costs nothing until it is opened. `keepAlive` keeps it in the DOM, hidden, once it has been shown, so its scroll position and form input survive switching tabs. Plain content (not in an `ng-template`) renders at once and is only hidden.
112
+
113
+ ```ts
114
+ import { Component, signal } from '@angular/core';
115
+ import { BfInputComponent, BfTabGroupComponent, BfTabPanelComponent } from '@bitforgehq/ui';
116
+
117
+ /**
118
+ * Lazy panels, one kept alive.
119
+ *
120
+ * Each panel's content is an `ng-template`, created when its tab shows. Notes is `keepAlive`:
121
+ * type in it, switch tabs and come back, and the text is still there; Comments is created anew
122
+ * each time.
123
+ */
124
+ @Component({
125
+ selector: 'app-example-tab-group-lazy',
126
+ imports: [BfInputComponent, BfTabGroupComponent, BfTabPanelComponent],
127
+ template: `
128
+ <bf-tab-group label="Task details" [(selected)]="tab">
129
+ <bf-tab-panel value="notes" label="Notes" keepAlive>
130
+ <ng-template>
131
+ <bf-input label="Note" placeholder="Type, then switch tabs" />
132
+ </ng-template>
133
+ </bf-tab-panel>
134
+ <bf-tab-panel value="comments" label="Comments">
135
+ <ng-template>
136
+ <bf-input label="Comment" placeholder="Cleared when you leave this tab" />
137
+ </ng-template>
138
+ </bf-tab-panel>
139
+ </bf-tab-group>
140
+ `,
141
+ })
142
+ export class TabGroupLazyExampleComponent {
143
+ readonly tab = signal('notes');
144
+ }
145
+ ```
146
+
147
+ ## Selection
148
+
149
+ `[(selected)]` holds the shown panel's `value`; `(changed)` emits the new value after a user change, never for a write of `selected`. While `selected` matches no panel (unset, or a value that went away), the first enabled panel shows; `selected` itself keeps its value until the user picks a tab.
150
+
151
+ ## Sizes and width
152
+
153
+ `size` sets the tabs' height: `sm` 32 px, `md` 40 px, `lg` 48 px. Unset, it follows `provideBfDefaults()`'s `controlSize`, `md` by default. `fitted` stretches the tabs to share the group's width (two or three tabs on a phone). When the tabs do not fit, the row scrolls sideways; focusing a tab with the keyboard scrolls it into view.
154
+
155
+ ## Desktop
156
+
157
+ Under a mouse an unselected tab takes a `surface-muted` fill and `text` on hover, with the pointer cursor, and `surface-pressed` while pressed; a disabled tab shows `not-allowed` and does not react. The focus ring shows on keyboard focus only, on the tab and on the panel (which is in the tab order so its content can be reached and scrolled by keyboard). Under a fine pointer the tab's visual box is its hit area; under a coarse pointer (touch) every tab is at least 44 px tall. Desktop density is the default `md` step; `provideBfDefaults({ controlSize: 'sm' })` in a component's `providers` makes a dense sub-tree ([density guide](https://bitforge-ui.web.app/density)).
158
+
159
+ ## Theming
160
+
161
+ - Tabs: `text-muted` at rest, hover `surface-muted` with `text`, pressed `surface-pressed`; top corners `rounded-tile`.
162
+ - Selected tab: `primary-text` with a 2 px `primary` underline on its bottom edge, so the selection never rests on colour alone (`CanvasText` under forced colours; `text-disabled` on a disabled selected tab).
163
+ - Disabled tab: `text-disabled` (never opacity).
164
+ - The row: a 1 px `border` line under the tabs, across the group's width.
165
+ - Badge: `primary` with `on-primary` text, `rounded-control`, `text-micro`.
166
+ - Focus ring: `ring`, on the tabs and on the panel (`rounded-tile`).
167
+ - Type: `text-label-xs` / `text-label-sm` / `text-label` for `sm` / `md` / `lg`, bold.
168
+
169
+ ## Accessibility
170
+
171
+ The row is a `role="tablist"` named by `label`, else `provideBfLabels()`'s `tabGroup` ("Tabs"); give each group on a page its own `label`. Each tab is a native `<button role="tab">` with `aria-selected` and `aria-controls` naming its panel; each `bf-tab-panel` is a `role="tabpanel"` with `aria-labelledby` naming its tab and `tabindex="0"`, and an unselected panel is `hidden`. Only the selected tab is in the tab order (a roving tab stop), so <kbd>Tab</kbd> moves from it into the panel. Selection follows focus (automatic activation): the arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> move to a tab and show its panel at once, so keep a panel's first render quick. A disabled tab is native-disabled and skipped. A badge's pill is `aria-hidden`; its count is read after the label from visually hidden text (`tabBadge(n)`, "3 new").
172
+
173
+ ### Keyboard
174
+
175
+ | Key | Action |
176
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
177
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Moves focus to the selected tab, then into the shown panel, and out again |
178
+ | <kbd>ArrowRight</kbd> | Selects and focuses the next enabled tab, wrapping at the end (the previous one under `dir="rtl"`) |
179
+ | <kbd>ArrowLeft</kbd> | Selects and focuses the previous enabled tab, wrapping at the start (the next one under `dir="rtl"`) |
180
+ | <kbd>Home</kbd> / <kbd>End</kbd> | Selects and focuses the first / last enabled tab |
181
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Selects the focused tab (it is already selected after an arrow key) |
182
+
183
+ ## Inputs
184
+
185
+ | Name | Type | Default | Description |
186
+ | ---------- | ----------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | `label` | `string` | `''` | Accessible name of the tablist; empty falls back to `provideBfLabels()`'s `tabGroup` ("Tabs"). |
188
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step of the tabs: sm 32, md 40, lg 48 px (at least 44 px under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
189
+ | `fitted` | `boolean` | `false` | Stretches the tabs to share the group's width. |
190
+ | `selected` | `string` | `''` | The shown panel's `value`; two-way bindable. Unset or unknown shows the first enabled panel (the model keeps its value until the user picks a tab). Two-way: `[(selected)]`. |
191
+
192
+ ## Outputs
193
+
194
+ | Name | Payload | Description |
195
+ | --------- | -------- | -------------------------------------------------------------------- |
196
+ | `changed` | `string` | Emitted with the newly selected panel's `value` after a user change. |
197
+
198
+ ## Slots
199
+
200
+ - `default`: The panels, each a `bf-tab-panel`.
201
+
202
+ ## Notes
203
+
204
+ ### Do
205
+
206
+ - Keep tab labels to one or two short words, and the tabs to a handful that fit the usual width.
207
+ - Put heavy content in an `ng-template` so it renders only when its tab is opened.
208
+
209
+ ### Don't
210
+
211
+ - Don't use tabs to navigate between pages; use `bf-top-nav` links, which the router and the browser understand.
212
+ - Don't nest a tab group in another tab group's panel; split the page instead.
213
+
214
+ ## Migration notes
215
+
216
+ ### 0.3.1
217
+
218
+ New `bf-tab-group`: in-page tabs over the content they switch, in both shells. A `tablist` of
219
+ `button role="tab"` from its `bf-tab-panel` children, the `selected` model of the shown panel's
220
+ `value` with `changed` after a user change (unset or unknown shows the first enabled panel),
221
+ named by `label` (else the new `tabGroup` label, "Tabs"), `size` from `controlSize` (44 px at
222
+ least under a coarse pointer) and `fitted` to share the width. A roving tab stop and automatic
223
+ activation: the arrow keys (mirrored under `dir="rtl"`), Home and End select and focus a tab,
224
+ skipping disabled ones. The selected tab is underlined in `primary`; the row scrolls sideways
225
+ when the tabs do not fit. New `BfTabGroupHarness` in `@bitforgehq/ui/testing` (`getTabLabels()`,
226
+ `getSelectedLabel()`, `selectTab(label)`, `getPanelText()`).