@bitforgehq/ui 0.3.0 → 0.3.2

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 (158) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/README.md +18 -11
  3. package/ai/MANIFEST.index.json +306 -0
  4. package/ai/MANIFEST.json +8915 -3788
  5. package/ai/REFERENCE.index.json +476 -0
  6. package/ai/REFERENCE.json +5810 -2905
  7. package/ai/UI_GUIDE.md +212 -58
  8. package/ai/components/app-frame.md +328 -0
  9. package/ai/components/back-button.md +42 -2
  10. package/ai/components/badge.md +17 -7
  11. package/ai/components/bar-chart.md +188 -0
  12. package/ai/components/breadcrumb-item.md +121 -0
  13. package/ai/components/breadcrumb.md +277 -0
  14. package/ai/components/button.md +82 -15
  15. package/ai/components/calendar.md +300 -0
  16. package/ai/components/card.md +119 -4
  17. package/ai/components/chart-card.md +283 -0
  18. package/ai/components/combobox.md +411 -0
  19. package/ai/components/date-input.md +396 -0
  20. package/ai/components/datetime-input.md +215 -0
  21. package/ai/components/description-item.md +111 -0
  22. package/ai/components/description-list.md +160 -0
  23. package/ai/components/donut-chart.md +169 -0
  24. package/ai/components/empty-state.md +99 -6
  25. package/ai/components/header.md +71 -2
  26. package/ai/components/icon-button.md +74 -16
  27. package/ai/components/icon.md +63 -7
  28. package/ai/components/line-chart.md +234 -0
  29. package/ai/components/list.md +70 -6
  30. package/ai/components/menu.md +5 -0
  31. package/ai/components/multi-combobox.md +257 -0
  32. package/ai/components/nav-group.md +178 -0
  33. package/ai/components/nav-item.md +210 -0
  34. package/ai/components/page-header.md +237 -0
  35. package/ai/components/pagination.md +173 -0
  36. package/ai/components/popover.md +250 -0
  37. package/ai/components/side-panel.md +391 -0
  38. package/ai/components/sidebar-toggle.md +287 -0
  39. package/ai/components/sidebar.md +541 -0
  40. package/ai/components/skeleton.md +215 -0
  41. package/ai/components/sparkline.md +188 -0
  42. package/ai/components/stat.md +297 -0
  43. package/ai/components/tab-group.md +226 -0
  44. package/ai/components/tab-panel.md +163 -0
  45. package/ai/components/table.md +697 -0
  46. package/ai/components/tabs.md +46 -2
  47. package/ai/components/theme-menu-button.md +182 -0
  48. package/ai/components/theme-switch.md +170 -0
  49. package/ai/components/time-input.md +240 -0
  50. package/ai/components/toolbar.md +354 -0
  51. package/ai/components/tooltip.md +230 -0
  52. package/ai/components/top-nav.md +159 -0
  53. package/ai/reference/bf-app-frame-breakpoint.md +27 -0
  54. package/ai/reference/bf-app-frame-content-width.md +29 -0
  55. package/ai/reference/bf-badge-appearance.md +36 -0
  56. package/ai/reference/bf-badge-variant.md +1 -1
  57. package/ai/reference/bf-bar-chart-orientation.md +27 -0
  58. package/ai/reference/bf-button-appearance.md +36 -0
  59. package/ai/reference/bf-button-variant.md +1 -1
  60. package/ai/reference/bf-calendar-disabled-date.md +23 -0
  61. package/ai/reference/bf-calendar-harness.md +51 -0
  62. package/ai/reference/bf-calendar-weekday.md +31 -0
  63. package/ai/reference/bf-chart-card-heading-level.md +26 -0
  64. package/ai/reference/bf-chart-curve.md +28 -0
  65. package/ai/reference/bf-chart-harness.md +51 -0
  66. package/ai/reference/bf-chart-legend-position.md +30 -0
  67. package/ai/reference/bf-chart-series.md +23 -0
  68. package/ai/reference/bf-chart-value-format.md +23 -0
  69. package/ai/reference/bf-color-scheme-service.md +30 -0
  70. package/ai/reference/bf-color-scheme.md +31 -0
  71. package/ai/reference/bf-combobox-filter.md +27 -0
  72. package/ai/reference/bf-combobox-harness.md +89 -0
  73. package/ai/reference/bf-combobox-option.md +33 -0
  74. package/ai/reference/bf-compare-with.md +2 -0
  75. package/ai/reference/bf-date-input-harness.md +67 -0
  76. package/ai/reference/bf-date-input-picker.md +27 -0
  77. package/ai/reference/bf-date-time-input-harness.md +76 -0
  78. package/ai/reference/bf-date-time-input-picker.md +27 -0
  79. package/ai/reference/bf-description-list-layout.md +26 -0
  80. package/ai/reference/bf-donut-slice.md +22 -0
  81. package/ai/reference/bf-empty-state-heading-level.md +1 -1
  82. package/ai/reference/bf-empty-state-heading.md +33 -0
  83. package/ai/reference/bf-labels.md +73 -20
  84. package/ai/reference/bf-menu-button-variant.md +1 -0
  85. package/ai/reference/bf-menu-placement.md +1 -0
  86. package/ai/reference/bf-overlay-options.md +28 -27
  87. package/ai/reference/bf-overlay-placement.md +34 -0
  88. package/ai/reference/bf-overlay-position.md +1 -1
  89. package/ai/reference/bf-overlay-service.md +5 -3
  90. package/ai/reference/bf-page-header-heading-level.md +26 -0
  91. package/ai/reference/bf-pagination-harness.md +61 -0
  92. package/ai/reference/bf-platform-service.md +17 -15
  93. package/ai/reference/bf-popover-harness.md +42 -0
  94. package/ai/reference/bf-popover-placement.md +28 -0
  95. package/ai/reference/bf-popover-trigger-directive.md +56 -0
  96. package/ai/reference/bf-side-panel-config.md +35 -0
  97. package/ai/reference/bf-side-panel-harness.md +34 -0
  98. package/ai/reference/bf-side-panel-ref.md +44 -0
  99. package/ai/reference/bf-side-panel-service.md +43 -0
  100. package/ai/reference/bf-side-panel-template-context.md +32 -0
  101. package/ai/reference/bf-side-panel-width.md +28 -0
  102. package/ai/reference/bf-sidebar-state-service.md +21 -0
  103. package/ai/reference/bf-sidebar-toggle-harness.md +32 -0
  104. package/ai/reference/bf-size.md +13 -0
  105. package/ai/reference/bf-skeleton-shape.md +28 -0
  106. package/ai/reference/bf-stat-appearance.md +33 -0
  107. package/ai/reference/bf-stat-trend-intent.md +27 -0
  108. package/ai/reference/bf-stat-trend.md +27 -0
  109. package/ai/reference/bf-stat-variant.md +26 -0
  110. package/ai/reference/bf-tab-group-harness.md +44 -0
  111. package/ai/reference/bf-table-align.md +26 -0
  112. package/ai/reference/bf-table-cell-context.md +24 -0
  113. package/ai/reference/bf-table-cell-directive.md +22 -0
  114. package/ai/reference/bf-table-column.md +28 -0
  115. package/ai/reference/bf-table-harness.md +77 -0
  116. package/ai/reference/bf-table-row-key.md +21 -0
  117. package/ai/reference/bf-table-sort-direction.md +26 -0
  118. package/ai/reference/bf-table-sort.md +27 -0
  119. package/ai/reference/bf-table-stack.md +29 -0
  120. package/ai/reference/bf-theme.md +5 -4
  121. package/ai/reference/bf-time-input-harness.md +67 -0
  122. package/ai/reference/bf-time-input-hour-cycle.md +27 -0
  123. package/ai/reference/bf-time-input-picker.md +27 -0
  124. package/ai/reference/bf-tooltip-directive.md +43 -0
  125. package/ai/reference/bf-tooltip-harness.md +43 -0
  126. package/ai/reference/bf-tooltip-placement.md +28 -0
  127. package/ai/reference/bf_color_scheme_storage.md +20 -0
  128. package/ai/reference/bf_side_panel_data.md +17 -0
  129. package/ai/reference/bf_sidebar_state_storage.md +20 -0
  130. package/ai/reference/bf_theme_storage.md +3 -1
  131. package/ai/reference/provide-bf-color-scheme.md +20 -0
  132. package/ai/reference/provide-bf-labels.md +13 -4
  133. package/ai/reference/provide-bf-sidebar-state.md +20 -0
  134. package/ai/reference/slide-in-from-end.md +24 -0
  135. package/ai/reference/slide-out-to-end.md +24 -0
  136. package/bin/bf-ui.js +433 -3
  137. package/fesm2022/bitforgehq-ui-charts.mjs +3045 -0
  138. package/fesm2022/bitforgehq-ui-charts.mjs.map +1 -0
  139. package/fesm2022/bitforgehq-ui-icons.mjs +107 -3
  140. package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -1
  141. package/fesm2022/bitforgehq-ui-mobile.mjs +14 -59
  142. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  143. package/fesm2022/bitforgehq-ui-overlay.mjs +6566 -38
  144. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  145. package/fesm2022/bitforgehq-ui-testing.mjs +634 -1
  146. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  147. package/fesm2022/bitforgehq-ui.mjs +5708 -137
  148. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  149. package/llms-full.txt +26766 -14470
  150. package/llms.txt +106 -1
  151. package/package.json +7 -3
  152. package/theme.css +101 -0
  153. package/types/bitforgehq-ui-charts.d.ts +967 -0
  154. package/types/bitforgehq-ui-icons.d.ts +13 -1
  155. package/types/bitforgehq-ui-mobile.d.ts +25 -18
  156. package/types/bitforgehq-ui-overlay.d.ts +1895 -7
  157. package/types/bitforgehq-ui-testing.d.ts +782 -1
  158. package/types/bitforgehq-ui.d.ts +2393 -45
@@ -0,0 +1,300 @@
1
+ # bf-calendar
2
+
3
+ Inline month calendar for picking one date, with month and year jumps
4
+
5
+ `BfCalendarComponent` from `@bitforgehq/ui` · Forms · since 0.3.1
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import { BfCalendarComponent } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-calendar overview.
15
+ *
16
+ * Live demo of `bf-calendar`: a due date picked by click or keyboard (the value is a plain ISO
17
+ * date string), the header's month buttons and month and year jump, and a status line with the
18
+ * picked date and the month on show.
19
+ */
20
+ @Component({
21
+ selector: 'app-example-calendar-overview',
22
+ imports: [BfCalendarComponent],
23
+ template: `
24
+ <div class="flex flex-col gap-3">
25
+ <bf-calendar label="Due date" locale="en-GB" [(value)]="due" [(month)]="month" />
26
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
27
+ </div>
28
+ `,
29
+ })
30
+ export class CalendarOverviewExampleComponent {
31
+ /** The picked date, 'YYYY-MM-DD'. */
32
+ readonly due = signal<string | null>('2026-10-14');
33
+ /** The month on show, 'YYYY-MM'; null opens on the due date's month. */
34
+ readonly month = signal<string | null>(null);
35
+
36
+ /** The picked date, and the month on show once the user has browsed to one. */
37
+ readonly status = computed(() => {
38
+ const month = this.month();
39
+ return `Due: ${this.due() ?? 'none'}${month ? ` · showing ${month}` : ''}`;
40
+ });
41
+ }
42
+ ```
43
+
44
+ ## Overview
45
+
46
+ Inline month calendar for picking one date, an ARIA grid with full keyboard support. The value
47
+ is a plain ISO date string ('YYYY-MM-DD', never a Date with a time zone) or `null`; month and
48
+ weekday names come from `Intl` in the calendar's `locale`; implements the Signal Forms value
49
+ contract.
50
+
51
+ A calendar shows one month as a grid of days for picking a single date. Its value is a plain ISO date string (`'2026-10-14'`), never a `Date` with a time of day, so no time zone can shift the day. Month and weekday names come from `Intl` in the calendar's locale, the header jumps to another month or year in two clicks, and it binds to Signal Forms like any other field.
52
+
53
+ ## When to use
54
+
55
+ Use `bf-calendar` for an inline calendar that sits on the page: a booking page, a dashboard filter, a side panel where the month is always in view. For a date in a form field use [bf-date-input](https://bitforge-ui.web.app/components/date-input) instead: a typed date field that opens this calendar in a popover. Use [bf-select](https://bitforge-ui.web.app/components/select) or [bf-segmented](https://bitforge-ui.web.app/components/segmented) instead when the choice is a few fixed dates (today, tomorrow, next week). The calendar works the same in both shells: in a Capacitor app make it `lg` (48 px cells) for touch.
56
+
57
+ ## Value and month
58
+
59
+ `value` is the picked date as `'YYYY-MM-DD'`, `null` while none is picked. `(changed)` emits the new date right after the user picks a different one, never for a value you set; `(picked)` emits on every pick, also of the date that already was the value, so a popover around the calendar knows when to close.
60
+
61
+ `month` (`'YYYY-MM'`) is the month on show, two-way bindable. Left `null`, the calendar opens on the value's month, else on today's (kept inside `min` and `max`); the previous/next buttons, the month and year views and the keyboard write it. Bind `[(month)]` to keep it, or set it to open on another month. Once the user has browsed away, a new `value` you set does not move the view; set `month` with it. `today` sets the date marked as today (for an app that works in another time zone than the device); unset it is the device's local date.
62
+
63
+ Only the days of the shown month are rendered; the cells of the neighbouring months stay empty. So every date appears once, the keyboard moves through one month at a time and crosses into the next one by moving past its end, and a screen reader never reads a date twice. The grid always takes six weeks of height, so the calendar never changes height between months.
64
+
65
+ ## Min, max and disabled dates
66
+
67
+ `min` and `max` (ISO dates) bound the dates that can be picked; earlier and later days show disabled, the previous/next buttons stop at the months that hold them, and the year view lists only their years. `disabledDate` rules out more dates: a function called with each ISO date, here every weekend. Disabled days stay focusable (`aria-disabled`, as the ARIA grid pattern asks), so the arrow keys step onto them and past them, but a click, <kbd>Enter</kbd> or <kbd>Space</kbd> does nothing.
68
+
69
+ ```ts
70
+ import { Component, signal } from '@angular/core';
71
+ import { BfCalendarComponent, type BfCalendarDisabledDate } from '@bitforgehq/ui';
72
+
73
+ /**
74
+ * Min, max and disabled weekends.
75
+ *
76
+ * A delivery date between 2 November and 18 December 2026 on working days only: `min` and `max`
77
+ * bound the range (the month buttons stop there) and `disabledDate` rules out Saturdays and
78
+ * Sundays.
79
+ */
80
+ @Component({
81
+ selector: 'app-example-calendar-min-max-disabled',
82
+ imports: [BfCalendarComponent],
83
+ template: `
84
+ <bf-calendar
85
+ label="Delivery date"
86
+ locale="en-GB"
87
+ min="2026-11-02"
88
+ max="2026-12-18"
89
+ [disabledDate]="weekends"
90
+ [(value)]="delivery"
91
+ />
92
+ `,
93
+ })
94
+ export class CalendarMinMaxDisabledExampleComponent {
95
+ readonly delivery = signal<string | null>('2026-11-04');
96
+
97
+ /** Saturdays and Sundays; an ISO date parses as UTC midnight, so getUTCDay is its weekday. */
98
+ readonly weekends: BfCalendarDisabledDate = (iso) => [0, 6].includes(new Date(iso).getUTCDay());
99
+ }
100
+ ```
101
+
102
+ ## Locale and first day of the week
103
+
104
+ `locale` (a BCP 47 tag) sets the month and weekday names, the full date each day is announced with, and the first column of the week. Empty, it follows the document's `lang`, else English. `firstDayOfWeek` (`0` Sunday to `6` Saturday) overrides the week the locale gives; without it the calendar asks `Intl.Locale`'s week info and falls back to Monday where the runtime has none. Here a German calendar, which starts on Monday. Put a `lang` on the element around it, so a screen reader reads the names in that language. The button names (previous/next month, choose month, choose year) are labels: translate them with `provideBfLabels()`.
105
+
106
+ ```ts
107
+ import { Component, signal } from '@angular/core';
108
+ import { BfCalendarComponent } from '@bitforgehq/ui';
109
+
110
+ /**
111
+ * A German calendar.
112
+ *
113
+ * `locale="de"` gives German month and weekday names and a week that starts on Monday; the `lang`
114
+ * around it makes a screen reader read them in German.
115
+ */
116
+ @Component({
117
+ selector: 'app-example-calendar-locale',
118
+ imports: [BfCalendarComponent],
119
+ template: `
120
+ <div lang="de">
121
+ <bf-calendar label="Liefertermin" locale="de" [(value)]="delivery" />
122
+ </div>
123
+ `,
124
+ })
125
+ export class CalendarLocaleExampleComponent {
126
+ readonly delivery = signal<string | null>('2026-10-14');
127
+ }
128
+ ```
129
+
130
+ ## Sizes
131
+
132
+ `size` sets the cell: `sm` 32 px, `md` 40 px (default), `lg` 48 px; the previous/next buttons and the title button take the same height. Unset, `size` follows `provideBfDefaults()`'s `controlSize`. Under a coarse pointer (touch) every day cell and header button is at least 44 px: the `sm` and `md` cells grow to 44 px (the calendar gets wider), `lg` stays 48 px. The calendar is seven cells wide and shrinks with a narrow column, the columns getting narrower and the rows keeping their height.
133
+
134
+ ```ts
135
+ import { Component } from '@angular/core';
136
+ import { BfCalendarComponent } from '@bitforgehq/ui';
137
+
138
+ /**
139
+ * Calendar sizes.
140
+ *
141
+ * The sm, md and lg steps (32, 40 and 48 px cells); under a coarse pointer the sm and md cells
142
+ * grow to 44 px.
143
+ */
144
+ @Component({
145
+ selector: 'app-example-calendar-sizes',
146
+ imports: [BfCalendarComponent],
147
+ template: `
148
+ <div class="flex flex-wrap items-start gap-6">
149
+ <bf-calendar size="sm" label="Small" locale="en-GB" value="2026-10-14" />
150
+ <bf-calendar label="Medium" locale="en-GB" value="2026-10-14" />
151
+ <bf-calendar size="lg" label="Large" locale="en-GB" value="2026-10-14" />
152
+ </div>
153
+ `,
154
+ })
155
+ export class CalendarSizesExampleComponent {}
156
+ ```
157
+
158
+ ## Month and year views
159
+
160
+ The title button (the month and year) opens the month view: the twelve months of the year, the shown one selected. Its title (the year) opens the year view, a scrolling list of years (those of `min` to `max`, else a hundred years either side). Picking a year opens its months; picking a month shows its days. <kbd>Escape</kbd> in either view returns to the days without picking.
161
+
162
+ ## With Signal Forms
163
+
164
+ Bind the field with `[formField]`. Required, touched, errors, disabled and readonly flow in; the first error shows under the calendar once focus has left it, and `errorMessage` shows at once. `readonly` shows the value and lets the user browse months but blocks picking; `disabled` blocks everything and takes the calendar out of the tab order. `name` is accepted for Signal Forms but not rendered (the calendar has no native form element). `focus()` focuses the day that holds the tab stop.
165
+
166
+ ## Naming the calendar
167
+
168
+ `label` is the calendar's accessible name (it is not shown), such as "Due date". Without it use `ariaLabel`, or `ariaLabelledby` pointing at a visible heading. The day grid itself is named by the month and year in the header. In specs, `BfCalendarHarness` from `@bitforgehq/ui/testing` finds a calendar by its `label` and picks a date by its ISO string.
169
+
170
+ ## Desktop
171
+
172
+ Hovering a pickable day tints its mark `surface-muted` (the selected day `primary-hover`) with the pointer cursor; disabled days show the not-allowed cursor and no tint, and a read-only calendar has neither. Hover styles apply only on devices that can hover. The focus ring shows on keyboard focus only, on the day's round mark. Under a fine pointer each cell is its own hit area at every size; desktop density is `md`, and `provideBfDefaults({ size: 'sm' })` makes a dense calendar for a sidebar or a toolbar popover ([density guide](https://bitforge-ui.web.app/density)).
173
+
174
+ ## Theming
175
+
176
+ - Day mark: `text`, hover `surface-muted`, pressed `surface-pressed`. Selected: `primary` with `on-primary`, hover `primary-hover`, pressed `primary-pressed`, bold (under `forced-colors` the system `Highlight` / `HighlightText` pair).
177
+ - Today: a 1.5 px inset `primary-strong` ring and bold text, so it never rests on colour alone (`CanvasText` under `forced-colors`).
178
+ - Disabled day: `text-disabled`; a disabled selected day keeps a `surface-sunken` pill with a `text-disabled` ring (`GrayText` under `forced-colors`).
179
+ - Weekday header: `text-muted`. Header buttons: `text-muted`, hover `surface-muted` with `text`; the title `text`, bold.
180
+ - Focus ring: `ring`. Error: a 1.5 px `danger-border` ring around the grid, the message below in `danger`.
181
+ - Shape: the marks, month and year pills and header buttons use `rounded-control` (`9999px` by default, so the day marks are round); the error ring `rounded-card`.
182
+
183
+ ## Accessibility
184
+
185
+ The calendar is a `role="group"` named by `label` (else `ariaLabel` or `ariaLabelledby`). The days are a `table` with `role="grid"`, named by the header's month and year (`aria-labelledby`); weekday columns are `th scope="col"` with the short name shown and the full name in `abbr`. Each day is a `gridcell` named by its full date in the locale ("Wednesday, 14 October 2026"); the selected day has `aria-selected="true"` (the others `"false"`), today `aria-current="date"`, and disabled days `aria-disabled="true"`: they stay focusable so the keyboard can move past them. One day holds the tab stop (roving `tabindex`): the last one moved to, else the selected day, else today, else the first day of the month inside `min`. A polite live region announces the month and year when they change. The grid carries `aria-readonly`, `aria-required`, `aria-disabled`, and once touched and invalid `aria-invalid` with `aria-errormessage`; the group's `aria-describedby` names the error. The previous/next buttons are named from the `calendarPreviousMonth` and `calendarNextMonth` labels and turn `aria-disabled` (keeping focus) past `min`/`max`; the title button reads its text followed by `calendarChooseMonth` or `calendarChooseYear`. The month and year views are grids too, with the shown month or year selected and the current one `aria-current="date"`; each month is named with its year. The chevrons and the caret are CSS borders and mirror under `dir="rtl"`, where <kbd>ArrowLeft</kbd> and <kbd>ArrowRight</kbd> swap as the days run right to left. Focus moves into the new grid when a view opens or closes.
186
+
187
+ ### Keyboard
188
+
189
+ Moving never picks; <kbd>Enter</kbd> or <kbd>Space</kbd> does. Moves stop at `min` and `max`. While `readonly` the keys still move, and picking a day does nothing.
190
+
191
+ | Key | Where | Action |
192
+ | -------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
193
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Calendar | Moves between the title button, the previous/next buttons and the grid's one tab stop |
194
+ | <kbd>ArrowRight</kbd> / <kbd>ArrowLeft</kbd> | Days | Next / previous day, into the neighbouring month at the ends (swapped under `dir="rtl"`) |
195
+ | <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Days | Same weekday next / previous week |
196
+ | <kbd>PageDown</kbd> / <kbd>PageUp</kbd> | Days | Same day next / previous month (the 31st becomes the month's last day) |
197
+ | <kbd>Shift</kbd>+<kbd>PageDown</kbd> / <kbd>PageUp</kbd> | Days | Same day next / previous year |
198
+ | <kbd>Home</kbd> / <kbd>End</kbd> | Days | First / last day of the week |
199
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Days | Picks the focused day (not a disabled one) |
200
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Header buttons | The title opens the month (then the year) view; previous/next show the month |
201
+ | <kbd>ArrowRight</kbd> / <kbd>ArrowLeft</kbd> | Months, years | Next / previous month or year (swapped under `dir="rtl"`) |
202
+ | <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Months, years | Next / previous row (three months, four years) |
203
+ | <kbd>Home</kbd> / <kbd>End</kbd> | Months, years | First / last of the row |
204
+ | <kbd>PageDown</kbd> / <kbd>PageUp</kbd> | Months, years | Same month next / previous year; twenty years on or back |
205
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Months, years | A month shows its days; a year opens its months |
206
+ | <kbd>Escape</kbd> | Months, years | Back to the days without picking (stops there, so a popover around it stays open) |
207
+
208
+ ## Inputs
209
+
210
+ | Name | Type | Default | Description |
211
+ | ---------------- | --------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
212
+ | `min` | `string \| undefined` | `undefined` | The earliest date that can be picked (ISO); earlier dates show disabled. Set by `[formField]`; do not bind it yourself. |
213
+ | `max` | `string \| undefined` | `undefined` | The latest date that can be picked (ISO); later dates show disabled. Set by `[formField]`; do not bind it yourself. |
214
+ | `disabledDate` | `BfCalendarDisabledDate \| null` | `null` | Marks more dates as not pickable (weekends, booked days); called with each ISO date. |
215
+ | `firstDayOfWeek` | `BfCalendarWeekday \| undefined` (`0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| undefined`) | `undefined` | The first column's weekday, 0 Sunday to 6 Saturday; unset follows the locale's week (else Monday). |
216
+ | `locale` | `string` | `''` | BCP 47 locale of the month and weekday names and of the week; empty follows the document's `lang`, else English. |
217
+ | `today` | `string \| undefined` | `undefined` | The date marked as today (ISO); unset is the device's local date. |
218
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Cell size: sm 32, md 40, lg 48 px (44 px under a coarse pointer); unset follows `provideBfDefaults()` (`controlSize`, `md` by default). |
219
+ | `disabled` | `boolean` | `false` | Disables the whole calendar: nothing is focusable or pickable. Set by `[formField]`; do not bind it yourself. |
220
+ | `readonly` | `boolean` | `false` | Shows the value and lets the user browse the months, but blocks picking. Set by `[formField]`; do not bind it yourself. |
221
+ | `required` | `boolean` | `false` | Marks the date required (`aria-required` on the day grid). Set by `[formField]`; do not bind it yourself. |
222
+ | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
223
+ | `touched` | `boolean` | `false` | Whether the user has left the calendar; errors show only once touched. Set by `[formField]`; do not bind it yourself. |
224
+ | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms; the first message is shown once touched. Set by `[formField]`; do not bind it yourself. |
225
+ | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
226
+ | `label` | `string` | `''` | Accessible name of the calendar (not rendered visibly), such as "Due date". |
227
+ | `ariaLabel` | `string` | `''` | Accessible name when `label` is not set. |
228
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the calendar (a visible heading) when `label` is not set. |
229
+ | `name` | `string` | `''` | Name of the bound field. Not rendered (the calendar has no native form element); kept so Signal Forms can bind it. Set by `[formField]`; do not bind it yourself. |
230
+ | `id` | `string` | `generated` | The calendar's id; generated when not set. |
231
+ | `value` | `string \| null` | `null` | The picked date as an ISO string ('YYYY-MM-DD'), `null` while none is picked; two-way bindable and bound by `[formField]`. Two-way: `[(value)]`. |
232
+ | `month` | `string \| null` | `null` | The month on show ('YYYY-MM'), two-way bindable; `null` shows the value's month, else today's. The previous/next buttons, the month and year views and the keyboard set it. Two-way: `[(month)]`. |
233
+
234
+ ## Outputs
235
+
236
+ | Name | Payload | Description |
237
+ | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `changed` | `string` | Emitted with the new date right after the user picks a different one. |
239
+ | `picked` | `string` | Emitted with the date the user picked (click, Enter or Space), also when it already was the value, so a popover around the calendar can close. |
240
+ | `touch` | `void` | Emitted when focus leaves the calendar, so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
241
+
242
+ ## Methods
243
+
244
+ ### `isDateDisabled(iso: string): boolean`
245
+
246
+ Whether a date cannot be picked: it is outside `min`…`max` or `disabledDate` rules it out.
247
+
248
+ | Parameter | Type | Description |
249
+ | --------- | -------- | ---------------------- |
250
+ | `iso` | `string` | The ISO date to check. |
251
+
252
+ ### `focus(options?: FocusOptions): void`
253
+
254
+ Focuses the cell that owns the tab stop of the grid on show; Signal Forms calls it from
255
+ `focusBoundControl()`.
256
+
257
+ | Parameter | Type | Description |
258
+ | ---------- | -------------- | --------------------------------------------------------------------- |
259
+ | `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
260
+
261
+ ### `select(iso: string): void`
262
+
263
+ Picks `iso` as a click on its day would: ignored while disabled or read-only, and for an
264
+ invalid or disabled date. Shows its month.
265
+
266
+ | Parameter | Type | Description |
267
+ | --------- | -------- | --------------------- |
268
+ | `iso` | `string` | The ISO date to pick. |
269
+
270
+ ## Notes
271
+
272
+ ### Do
273
+
274
+ - Give the calendar a `label` (or `ariaLabel` / `ariaLabelledby`); it is the accessible name.
275
+ - Store and send the value as the ISO string it is; turn it into a `Date` only to display or compute, at UTC midnight.
276
+ - Bound the range with `min` and `max` rather than `disabledDate` alone, so the buttons and the year view stop there too.
277
+
278
+ ### Don't
279
+
280
+ - Don't use it for a date range or several dates; it picks one date.
281
+ - Don't put a calendar in a dense form row for an ordinary date field; a date input with a popover is the field form.
282
+
283
+ ## Migration notes
284
+
285
+ ### 0.3.1
286
+
287
+ New `bf-calendar` in `@bitforgehq/ui`: an inline month calendar for picking one date. The value
288
+ is a plain ISO date string (`'YYYY-MM-DD'`) or `null`, never a `Date`, so no time zone shifts
289
+ the day; `(changed)` emits a new pick and `(picked)` every pick (for a popover to close).
290
+ `[(month)]` (`'YYYY-MM'`) is the month on show. `min`, `max` and `disabledDate`
291
+ (`BfCalendarDisabledDate`) disable dates; `firstDayOfWeek` (`BfCalendarWeekday`) and `locale`
292
+ (default the document's `lang`) set the week and the `Intl` month and weekday names; `today`
293
+ overrides the device's date; `size` sets the cells (32/40/48 px, 44 px under a coarse pointer).
294
+ The header's title opens a month view and then a year view for quick jumps. An ARIA grid with
295
+ roving focus: arrows by day and week (mirrored in RTL), Page Up/Down by month (Shift by year),
296
+ Home/End to the week's ends, Enter/Space picks, Escape leaves the jump views; disabled days stay
297
+ focusable with `aria-disabled`. Signal Forms value contract like `bf-segmented`. New label keys
298
+ `calendarPreviousMonth`, `calendarNextMonth`, `calendarChooseMonth`, `calendarChooseYear`, and
299
+ a `BfCalendarHarness` in `@bitforgehq/ui/testing` (`getMonthLabel()`, `selectDate(iso)`,
300
+ `getSelected()`, `nextMonth()`, `previousMonth()`).
@@ -95,7 +95,7 @@ Surface for a piece of content: a project summary, a stat, a focus prompt. Four
95
95
  the design: raised (default), tint, ink and flat. To make the whole card clickable, put a
96
96
  `bfAction` link or button in its heading (the stretched-link pattern).
97
97
 
98
- Cards group a piece of content on the canvas. The raised card is the workhorse; tint highlights a stat or a callout; ink is the dark focus card; flat sits inside another surface.
98
+ Cards group a piece of content on the canvas. The raised card is the workhorse; tint highlights a stat or a callout; ink is the dark focus card; flat is the outlined card for a page level that should stay quiet, such as a dense grid of cards.
99
99
 
100
100
  ## When to use
101
101
 
@@ -148,6 +148,107 @@ import { BfCardComponent } from '@bitforgehq/ui';
148
148
  export class CardVariantsExampleComponent {}
149
149
  ```
150
150
 
151
+ ## Nesting surfaces
152
+
153
+ Each level of a page takes one surface style, and a level inside a card steps down, never up:
154
+
155
+ - **Raised card, then sunken tile.** Inside a raised card, group figures or blocks on sunken, borderless tiles: [bf-stat](https://bitforge-ui.web.app/components/stat) `variant="tile"`, or your own block on `bg-surface-sunken` with the `card` radius.
156
+ - **Never an outlined surface inside a raised one.** A `flat` card or a `card` stat inside a raised card draws a border on top of a shadow, two edges for one level; use a tile instead. Don't nest raised cards either.
157
+ - **One surface style per level of a dashboard.** All cards on the page are raised (or all flat), and all tiles in them are sunken; mixing raised and flat cards side by side, or tiles and outlined stats in one card, reads as different kinds of content. The [bf-stat dashboard example](https://bitforge-ui.web.app/components/stat) puts raised cards on the page and sunken tiles in each.
158
+
159
+ ```ts
160
+ import { Component } from '@angular/core';
161
+ import { BfCardComponent, BfStatComponent } from '@bitforgehq/ui';
162
+ import { BfIconComponent } from '@bitforgehq/ui/icons';
163
+ import { CircleCheck, Clock, ListTodo } from 'lucide';
164
+
165
+ /**
166
+ * A raised card holding a row of sunken tiles.
167
+ *
168
+ * One surface per level: the card is raised on the page, and the stats inside it are sunken,
169
+ * borderless tiles (`bf-stat variant="tile"`), never outlined surfaces.
170
+ */
171
+ @Component({
172
+ selector: 'app-example-card-nesting',
173
+ imports: [BfCardComponent, BfIconComponent, BfStatComponent],
174
+ template: `
175
+ <bf-card padding="lg">
176
+ <h3 class="text-heading">This week</h3>
177
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
178
+ <bf-stat label="Open tasks" value="14" variant="tile">
179
+ <bf-icon bfSlot="icon" [icon]="listTodo" />
180
+ </bf-stat>
181
+ <bf-stat label="Done" value="23" variant="tile">
182
+ <bf-icon bfSlot="icon" [icon]="circleCheck" />
183
+ </bf-stat>
184
+ <bf-stat label="Time logged" value="31 h" variant="tile">
185
+ <bf-icon bfSlot="icon" [icon]="clock" />
186
+ </bf-stat>
187
+ </div>
188
+ </bf-card>
189
+ `,
190
+ })
191
+ export class CardNestingExampleComponent {
192
+ protected readonly listTodo = ListTodo;
193
+ protected readonly circleCheck = CircleCheck;
194
+ protected readonly clock = Clock;
195
+ }
196
+ ```
197
+
198
+ ## Footer
199
+
200
+ Content in `bfSlot="footer"` sits on the card's bottom edge: it takes the free space above it, so in a grid of cards (a `bf-card` fills its grid cell, `h-full`) every footer lines up, however differently the descriptions wrap, and the body keeps its 12 px gap above the footer. Put the footer's parts in one element (`<div bfSlot="footer" class="flex …">`), so they move together.
201
+
202
+ ```ts
203
+ import { Component } from '@angular/core';
204
+ import { BfBadgeComponent, BfButtonComponent, BfCardComponent } from '@bitforgehq/ui';
205
+
206
+ interface Project {
207
+ readonly id: string;
208
+ readonly name: string;
209
+ readonly summary: string;
210
+ readonly status: string;
211
+ }
212
+
213
+ /** A grid of cards whose descriptions wrap differently, with every footer on the bottom edge. */
214
+ @Component({
215
+ selector: 'app-example-card-grid',
216
+ imports: [BfBadgeComponent, BfButtonComponent, BfCardComponent],
217
+ template: `
218
+ <div class="grid gap-4 @md:grid-cols-3">
219
+ @for (project of projects; track project.id) {
220
+ <bf-card>
221
+ <h3 class="text-heading">{{ project.name }}</h3>
222
+ <p class="text-body text-text-muted">{{ project.summary }}</p>
223
+ <div bfSlot="footer" class="flex items-center justify-between gap-2">
224
+ <bf-badge>{{ project.status }}</bf-badge>
225
+ <bf-button variant="outline" size="sm">Open</bf-button>
226
+ </div>
227
+ </bf-card>
228
+ }
229
+ </div>
230
+ `,
231
+ })
232
+ export class CardGridExampleComponent {
233
+ readonly projects: readonly Project[] = [
234
+ { id: 'atlas', name: 'Atlas rebrand', summary: 'New logo and colours.', status: 'Active' },
235
+ {
236
+ id: 'harbor',
237
+ name: 'Harbor onboarding',
238
+ summary:
239
+ 'Rewrite the first-run flow so a new team reaches its first shared board in under five minutes, with sample data.',
240
+ status: 'Planning',
241
+ },
242
+ {
243
+ id: 'lumen',
244
+ name: 'Lumen reports',
245
+ summary: 'Weekly PDF exports for the finance team, scheduled per workspace.',
246
+ status: 'Review',
247
+ },
248
+ ];
249
+ }
250
+ ```
251
+
151
252
  ## Clickable card
152
253
 
153
254
  A card that opens something is a stretched link: its heading holds the one interactive element, an `<a bfAction>` (navigation, with `href` or `routerLink`) or a `<button bfAction>` (an action on the page), and `bfAction` stretches that element's hit area over the whole card with an `::after`. The heading stays a heading, the link's name is the title alone, and the card shows the pointer, the `shadow-floating` hover and the focus ring (drawn on the card while the link has keyboard focus). Import `BfActionDirective` from `@bitforgehq/ui`.
@@ -252,25 +353,39 @@ The card renders a plain `<div>` with no role. A clickable card follows the stre
252
353
 
253
354
  - `header`: Content above the body, such as a title row.
254
355
  - `default`: The card body.
255
- - `footer`: Content below the body, such as its actions.
356
+ - `footer`: Content below the body, such as its actions; it sits on the card's bottom edge.
256
357
 
257
358
  ## Notes
258
359
 
259
360
  ### Do
260
361
 
261
- - One idea per card; use `bf-list` for repeated rows.
362
+ - One idea per card; use `bf-list` for repeated rows (`[inset]="false" divided class="-mx-card"` runs them edge to edge on the card).
262
363
  - Use `lg` padding for cards that hold a heading and several blocks.
364
+ - Put a card's actions and status in one `bfSlot="footer"` element, so they line up across a grid.
263
365
  - Make a card clickable with one `bfAction` link or button in its heading; give other controls in it `class="relative z-1"`.
264
366
 
265
367
  ### Don't
266
368
 
267
- - Don't nest raised cards; use `flat` inside.
369
+ - Don't nest raised cards, and don't put an outlined surface (`flat`, a `card` stat) inside a raised one; use sunken tiles (`bf-stat variant="tile"`) inside.
370
+ - Don't mix surface styles on one level of a dashboard; keep one per level.
268
371
  - Don't use `interactive` (deprecated) or wrap a card in an `<a>`; both hide the heading and nest interactive content.
269
372
  - Don't put more than one `bfAction` in a card.
270
373
  - Don't put a `text` button on an ink card — its primary colour fails contrast on ink; use `tonal` or `outline`.
271
374
 
272
375
  ## Migration notes
273
376
 
377
+ ### 0.3.2
378
+
379
+ New "Nesting surfaces" docs section: a raised card holds sunken tiles, never an outlined
380
+ surface (`flat`, a `card` stat), and each level of a dashboard keeps one surface style. New
381
+ `card/nesting` example. No code change.
382
+
383
+ ### 0.3.1
384
+
385
+ The footer (`bfSlot="footer"`) sits on the card's bottom edge (`mt-auto`), so in a grid of cards
386
+ whose descriptions wrap differently every footer lines up. A card whose footer already followed
387
+ the body directly looks the same unless the card is taller than its content.
388
+
274
389
  ### 0.3.0
275
390
 
276
391
  **Behaviour:** logical properties: content aligns to the inline start (`text-start`).