forty-cdk 0.21.1 → 0.23.0

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 (190) hide show
  1. package/README.md +57 -35
  2. package/accordion/README.md +2 -2
  3. package/aspect-ratio/README.md +1 -1
  4. package/breakpoints/README.md +22 -1
  5. package/calendar/README.md +3 -3
  6. package/combobox/README.md +10 -10
  7. package/context-menu/README.md +3 -3
  8. package/date-field/README.md +72 -0
  9. package/date-picker/README.md +8 -7
  10. package/dialog/README.md +1 -1
  11. package/disclosure/README.md +1 -1
  12. package/drag-drop/README.md +3 -4
  13. package/drawer/README.md +2 -2
  14. package/dropdown-menu/README.md +5 -5
  15. package/fesm2022/forty-cdk-accordion.mjs +154 -145
  16. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  17. package/fesm2022/forty-cdk-aspect-ratio.mjs +43 -43
  18. package/fesm2022/forty-cdk-avatar.mjs +166 -119
  19. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-breadcrumbs.mjs +70 -70
  21. package/fesm2022/forty-cdk-breakpoints.mjs +66 -60
  22. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-button.mjs +152 -154
  24. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  25. package/fesm2022/forty-cdk-calendar.mjs +594 -589
  26. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  27. package/fesm2022/forty-cdk-carousel.mjs +351 -342
  28. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  29. package/fesm2022/forty-cdk-checkbox.mjs +118 -113
  30. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  31. package/fesm2022/forty-cdk-combobox.mjs +1354 -1397
  32. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  33. package/fesm2022/forty-cdk-context-menu.mjs +286 -330
  34. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-core-overlay.mjs +4748 -0
  36. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-core.mjs +3621 -8510
  38. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-date-field.mjs +851 -230
  40. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  41. package/fesm2022/forty-cdk-date-picker.mjs +639 -645
  42. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  43. package/fesm2022/forty-cdk-dialog.mjs +278 -269
  44. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  45. package/fesm2022/forty-cdk-disclosure.mjs +73 -68
  46. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-drag-drop.mjs +342 -332
  48. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  49. package/fesm2022/forty-cdk-drawer.mjs +978 -902
  50. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  51. package/fesm2022/forty-cdk-dropdown-menu.mjs +232 -278
  52. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  53. package/fesm2022/forty-cdk-field.mjs +229 -221
  54. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  55. package/fesm2022/forty-cdk-fieldset.mjs +113 -109
  56. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  57. package/fesm2022/forty-cdk-file-upload.mjs +95 -90
  58. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  59. package/fesm2022/forty-cdk-hover-card.mjs +204 -199
  60. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  61. package/fesm2022/forty-cdk-input.mjs +129 -129
  62. package/fesm2022/forty-cdk-internationalized-date.mjs +90 -90
  63. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-listbox.mjs +398 -392
  65. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-menu.mjs +751 -784
  67. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-menubar.mjs +461 -464
  69. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-meter.mjs +73 -68
  71. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-navigation-menu.mjs +502 -553
  73. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-number-input.mjs +357 -352
  75. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-otp-input.mjs +185 -180
  77. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  78. package/fesm2022/forty-cdk-pagination.mjs +149 -144
  79. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-pane-resizer.mjs +140 -140
  81. package/fesm2022/forty-cdk-popover.mjs +270 -265
  82. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  83. package/fesm2022/forty-cdk-progress.mjs +101 -96
  84. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  85. package/fesm2022/forty-cdk-radio-group.mjs +173 -163
  86. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  87. package/fesm2022/forty-cdk-scroll-area.mjs +307 -294
  88. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  89. package/fesm2022/forty-cdk-search.mjs +185 -180
  90. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  91. package/fesm2022/forty-cdk-select.mjs +741 -796
  92. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  93. package/fesm2022/forty-cdk-separator.mjs +33 -33
  94. package/fesm2022/forty-cdk-shared.mjs +5 -4
  95. package/fesm2022/forty-cdk-shared.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-slider.mjs +274 -258
  97. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-stepper.mjs +328 -319
  99. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-switch.mjs +65 -65
  101. package/fesm2022/forty-cdk-table-virtualization.mjs +132 -123
  102. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  103. package/fesm2022/forty-cdk-table.mjs +1752 -1732
  104. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  105. package/fesm2022/forty-cdk-tabs.mjs +149 -129
  106. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  107. package/fesm2022/forty-cdk-time-field.mjs +878 -231
  108. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  109. package/fesm2022/forty-cdk-time-picker.mjs +333 -360
  110. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  111. package/fesm2022/forty-cdk-toast.mjs +599 -607
  112. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  113. package/fesm2022/forty-cdk-toggle.mjs +232 -227
  114. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  115. package/fesm2022/forty-cdk-toolbar.mjs +147 -136
  116. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  117. package/fesm2022/forty-cdk-tooltip.mjs +268 -263
  118. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  119. package/fesm2022/forty-cdk-tree.mjs +702 -586
  120. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  121. package/fesm2022/forty-cdk-virtual-reorder.mjs +75 -69
  122. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  123. package/fesm2022/forty-cdk-virtualization.mjs +163 -159
  124. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  125. package/fesm2022/forty-cdk-visually-hidden.mjs +4 -4
  126. package/fesm2022/forty-cdk.mjs +3 -3
  127. package/hover-card/README.md +13 -13
  128. package/internationalized-date/README.md +1 -1
  129. package/menu/README.md +12 -11
  130. package/menubar/README.md +8 -8
  131. package/package.json +5 -9
  132. package/popover/README.md +13 -13
  133. package/select/README.md +43 -14
  134. package/shared/README.md +2 -29
  135. package/table/README.md +6 -6
  136. package/time-field/README.md +77 -0
  137. package/time-picker/README.md +31 -2
  138. package/tooltip/README.md +13 -18
  139. package/tree/README.md +54 -26
  140. package/types/forty-cdk-accordion.d.ts +3 -4
  141. package/types/forty-cdk-avatar.d.ts +35 -11
  142. package/types/forty-cdk-breakpoints.d.ts +1 -0
  143. package/types/forty-cdk-button.d.ts +1 -3
  144. package/types/forty-cdk-calendar.d.ts +1 -1
  145. package/types/forty-cdk-carousel.d.ts +29 -18
  146. package/types/forty-cdk-combobox.d.ts +167 -155
  147. package/types/forty-cdk-context-menu.d.ts +33 -64
  148. package/types/forty-cdk-core-overlay.d.ts +3596 -0
  149. package/types/forty-cdk-core.d.ts +866 -4204
  150. package/types/forty-cdk-date-field.d.ts +415 -38
  151. package/types/forty-cdk-date-picker.d.ts +65 -87
  152. package/types/forty-cdk-dialog.d.ts +8 -9
  153. package/types/forty-cdk-disclosure.d.ts +1 -1
  154. package/types/forty-cdk-drag-drop.d.ts +2 -2
  155. package/types/forty-cdk-drawer.d.ts +9 -10
  156. package/types/forty-cdk-dropdown-menu.d.ts +25 -59
  157. package/types/forty-cdk-field.d.ts +2 -3
  158. package/types/forty-cdk-hover-card.d.ts +2 -1
  159. package/types/forty-cdk-internationalized-date.d.ts +2 -2
  160. package/types/forty-cdk-listbox.d.ts +7 -0
  161. package/types/forty-cdk-menu.d.ts +58 -116
  162. package/types/forty-cdk-menubar.d.ts +71 -100
  163. package/types/forty-cdk-navigation-menu.d.ts +54 -110
  164. package/types/forty-cdk-popover.d.ts +4 -3
  165. package/types/forty-cdk-radio-group.d.ts +1 -1
  166. package/types/forty-cdk-scroll-area.d.ts +3 -0
  167. package/types/forty-cdk-select.d.ts +163 -103
  168. package/types/forty-cdk-shared.d.ts +2 -1
  169. package/types/forty-cdk-slider.d.ts +5 -2
  170. package/types/forty-cdk-stepper.d.ts +2 -3
  171. package/types/forty-cdk-table.d.ts +315 -331
  172. package/types/forty-cdk-tabs.d.ts +15 -0
  173. package/types/forty-cdk-time-field.d.ts +441 -36
  174. package/types/forty-cdk-time-picker.d.ts +22 -44
  175. package/types/forty-cdk-toast.d.ts +14 -16
  176. package/types/forty-cdk-toolbar.d.ts +6 -0
  177. package/types/forty-cdk-tooltip.d.ts +2 -1
  178. package/types/forty-cdk-tree.d.ts +120 -78
  179. package/types/forty-cdk-virtual-reorder.d.ts +3 -3
  180. package/types/forty-cdk-virtualization.d.ts +2 -3
  181. package/types/forty-cdk-visually-hidden.d.ts +1 -1
  182. package/visually-hidden/README.md +47 -0
  183. package/date-range-field/README.md +0 -253
  184. package/fesm2022/forty-cdk-date-range-field.mjs +0 -656
  185. package/fesm2022/forty-cdk-date-range-field.mjs.map +0 -1
  186. package/fesm2022/forty-cdk-time-range-field.mjs +0 -696
  187. package/fesm2022/forty-cdk-time-range-field.mjs.map +0 -1
  188. package/time-range-field/README.md +0 -263
  189. package/types/forty-cdk-date-range-field.d.ts +0 -432
  190. package/types/forty-cdk-time-range-field.d.ts +0 -467
package/README.md CHANGED
@@ -4,6 +4,8 @@ Headless / styleless UI primitives for Angular with WAI-ARIA accessibility built
4
4
  Designed from the ground up for modern Angular — the API is built around signals, standalone
5
5
  directives, and dependency-injection composition.
6
6
 
7
+ **Browsing?** The [documentation site](https://tutkli.github.io/forty-cdk/) renders every primitive with live examples.
8
+
7
9
  **New here?** [Your first overlay](../../docs/your-first-overlay.md) walks one Popover from empty markup to styled-and-animated and explains the two concepts every overlay shares: the `@if` / open-state model and the portal → global CSS requirement.
8
10
 
9
11
  **Styling these primitives?** [Styling forty-cdk](../../docs/styling.md) explains the three hooks you style against — your own class (not the directive selector), `data-*` state attributes, and `--for-*` custom properties — and links to each primitive's styling reference.
@@ -18,33 +20,54 @@ npm install forty-cdk
18
20
 
19
21
  Required:
20
22
 
21
- - `@angular/common` `^22.0.0`
22
- - `@angular/core` `^22.0.0`
23
+ - `@angular/common` `^22.0.1`
24
+ - `@angular/core` `^22.0.1`
23
25
 
24
26
  Optional — install only if you use the matching entry point / primitives:
25
27
 
26
28
  | Peer | Needed by |
27
29
  | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `@angular/forms` `^22.0.0` | Form-control primitives (`Switch`, `Checkbox`, `RadioGroup`, `Listbox`, `Select`, `Slider`, `Combobox`, …). They implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so the published bundle never references the package — consumers using only non-form primitives can skip it. |
29
- | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves only depend on the abstract `DateAdapter` contract from the main entry point — install this peer only when you import that entry point. |
30
-
31
- `@angular/forms/signals` is stable as of Angular 22, so the peer follows the standard major range (`^22.0.0`).
30
+ | `@angular/forms` `^22.0.1` | Form-control primitives (`Switch`, `Checkbox`, `RadioGroup`, `Listbox`, `Select`, `Slider`, `Combobox`, …). They implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so the published bundle never references the package — consumers using only non-form primitives can skip it. |
31
+ | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/shared` — install this peer only when you import that entry point. |
32
32
 
33
33
  ### Regular dependencies
34
34
 
35
- `@floating-ui/dom` is a regular dependency, installed automatically with the package. Positioned overlays (`Tooltip`, `Popover`, `Menu`, `Combobox`, `Select`, etc.) import it statically from the main entry point, so every consumer's build must be able to resolve it — but it is internal-only (no floating-ui value crosses the public API) and tree-shakes out of your bundle when you don't use any positioned primitive.
35
+ Two packages are regular dependencies, installed automatically and never declared as peers, because nothing of either crosses the public API by value. Both tree-shake out of a bundle that imports no primitive using them.
36
+
37
+ | Dependency | Used by |
38
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `@floating-ui/dom` | Positioning for the anchored overlays — `Tooltip`, `Popover`, `Menu`, `Combobox`, `Select`, `Date Picker`, `Time Picker`, `Hover Card`. |
40
+ | `@tanstack/virtual-core` | The windowing core behind `forty-cdk/virtualization`, and therefore `forty-cdk/table-virtualization` and `forty-cdk/virtual-reorder`. |
36
41
 
37
42
  ## Entry points
38
43
 
39
44
  **The package name itself exports nothing.** `import { … } from 'forty-cdk'` resolves to no symbol, and your editor will not auto-import anything under the bare package name — by design, so that every symbol has exactly one import path. There are three specifiers you do import from:
40
45
 
41
- | Specifier | What it exports |
42
- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
44
- | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Six of them ship from their own primitive instead; see its README. |
45
- | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
46
+ | Specifier | What it exports |
47
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
49
+ | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Eight ship from their own primitive instead; that README names them. |
50
+ | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
51
+
52
+ `forty-cdk/core` and `forty-cdk/core-overlay` resolve too, but neither is **public**: together they hold the engines and DI singletons the library refactors freely, and they exist so every primitive resolves that shared implementation to one compiled module. They are two rather than one for a bundling reason you get for free: a published module is a bundler's chunk-splitting unit, so keeping the positioning engine (`@floating-ui/dom` and the overlay shells) in its own module means a lazy route that renders no overlay does not load it. Measured on a seven-lazy-route app, that is **41.7 kB raw / 12.1 kB transfer** a non-overlay route no longer pays. If a symbol you need is not exported by the three specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from either.
53
+
54
+ ## Errors
55
+
56
+ Every error and warning the library reports carries a stable code and, where they add something, the cause and the fix:
57
+
58
+ ```text
59
+ [forty-cdk/dialog] FORCDK-DIALOG-001: ForDialogTitle must be used inside a [forDialog] element.
60
+
61
+ Cause: No FOR_DIALOG_CONTEXT provider is visible from ForDialogTitle. Angular resolves a
62
+ directive's dependencies at the template's declaration site rather than where it is stamped, so a
63
+ piece declared in an ng-template outside the root resolves nothing even when it renders inside it.
64
+
65
+ Fix: Move ForDialogTitle inside a [forDialog] element, declaring any ng-template it lives in there too.
66
+ ```
67
+
68
+ The code is `FORCDK-<AREA>-<NUMBER>`, where the area is the entry point you imported from — so `FORCDK-DATE-PICKER-003` came from `forty-cdk/date-picker`, and `FORCDK-CORE-*` from machinery shared across primitives (those still print the prefix of the primitive you actually wrote). **A code is stable and always means the same failure**, so it is safe to search for, quote in an issue, or match on in your own error handling; a retired code is never reused for something else.
46
69
 
47
- `forty-cdk/core` resolves too, but it is **not** public: it holds the engines and DI singletons the library refactors freely, and it exists so every primitive resolves that shared implementation to one compiled module. If a symbol you need is not exported by the three specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from `core`.
70
+ Warnings are dev-mode only. Errors are not: a piece that resolved no context would fail one line later anyway, so it throws in production too and says why.
48
71
 
49
72
  ## Primitives
50
73
 
@@ -106,15 +129,13 @@ The tables below group the primitives by purpose. The link on each name opens th
106
129
 
107
130
  ### Date & time
108
131
 
109
- | Primitive | What it is |
110
- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
111
- | [Calendar](calendar) | A single-date calendar grid (APG Grid) over a pluggable date adapter, with roving-tabindex navigation. |
112
- | [Date Field](date-field) | A segmented date (and optional time) input — each part a spinbutton with locale-driven order and clamping. |
113
- | [Date Picker](date-picker) | A trigger that opens a floating calendar to pick a date, composing Calendar inside a dismissible popover. |
114
- | [Date Range Field](date-range-field) | Two labelled spinbutton endpoints (start / end) sharing locale, granularity and bounds. |
115
- | [Time Field](time-field) | A segmented time-of-day input with 12 / 24-hour cycles, optional seconds and min / max clamping. |
116
- | [Time Picker](time-picker) | A trigger that opens a floating listbox of generated time slots over a pluggable date adapter. |
117
- | [Time Range Field](time-range-field) | Two time-of-day endpoints (start / end) sharing the hour cycle and min / max bounds. |
132
+ | Primitive | What it is |
133
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | [Calendar](calendar) | A single-date calendar grid (APG Grid) over a pluggable date adapter, with roving-tabindex navigation. |
135
+ | [Date Field](date-field) | A segmented date (and optional time) input — each part a spinbutton with locale-driven order and clamping. Ships `ForDateRangeField` too. |
136
+ | [Date Picker](date-picker) | A trigger that opens a floating calendar to pick a date, composing Calendar inside a dismissible popover. Ships `ForDateRangePicker` too. |
137
+ | [Time Field](time-field) | A segmented time-of-day input with 12 / 24-hour cycles, optional seconds and min / max clamping. Ships `ForTimeRangeField` too. |
138
+ | [Time Picker](time-picker) | A trigger that opens a floating listbox of generated time slots over a pluggable date adapter. |
118
139
 
119
140
  ### Disclosure & content
120
141
 
@@ -126,15 +147,16 @@ The tables below group the primitives by purpose. The link on each name opens th
126
147
 
127
148
  ### Data & layout
128
149
 
129
- | Primitive | What it is |
130
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
- | [Table](table) | A headless data table over a native `<table>` or `<div>` grid: sticky headers, 2D keyboard navigation, row selection, sortable headers, column resizing and reordering. |
132
- | [Tree](tree) | A nested tree view for hierarchical data: expandable nodes with roving-tabindex navigation, selection and typeahead. |
133
- | [Scroll Area](scroll-area) | A scrollable region with cross-browser, stylable synthetic scrollbars. |
134
- | [Pane Resizer](pane-resizer) | A focusable divider that resizes the panes on either side — draggable and keyboard-operable. |
135
- | [Separator](separator) | A static, optionally semantic divider between groups of content, horizontal or vertical. |
136
- | [Aspect Ratio](aspect-ratio) | A container that keeps its content at a fixed width-to-height ratio. |
137
- | [Avatar](avatar) | A user image with a graceful fallback across its loading lifecycle. |
150
+ | Primitive | What it is |
151
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
152
+ | [Table](table) | A headless data table over a native `<table>` or `<div>` grid: sticky headers, 2D keyboard navigation, row selection, sortable headers, column resizing and reordering. |
153
+ | [Tree](tree) | A nested tree view for hierarchical data: expandable nodes with roving-tabindex navigation, selection and typeahead. |
154
+ | [Scroll Area](scroll-area) | A scrollable region with cross-browser, stylable synthetic scrollbars. |
155
+ | [Pane Resizer](pane-resizer) | A focusable divider that resizes the panes on either side — draggable and keyboard-operable. |
156
+ | [Separator](separator) | A static, optionally semantic divider between groups of content, horizontal or vertical. |
157
+ | [Aspect Ratio](aspect-ratio) | A container that keeps its content at a fixed width-to-height ratio. |
158
+ | [Avatar](avatar) | A user image with a graceful fallback across its loading lifecycle. |
159
+ | [Visually Hidden](visually-hidden) | Hides content visually while keeping it in the accessibility tree — screen-reader-only labels, plus the injectable `LiveAnnouncer`. |
138
160
 
139
161
  ### Feedback
140
162
 
@@ -149,7 +171,7 @@ Headless — no DOM or ARIA of their own; an `inject*` / provider API that other
149
171
 
150
172
  | Utility | What it is |
151
173
  | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
152
- | [Breakpoints](breakpoints) | A signal-first, zoneless, SSR-safe viewport breakpoint observer (`injectBreakpoints`). |
174
+ | [Breakpoints](breakpoints) | A signal-first, zoneless, SSR-safe viewport breakpoint observer (`injectBreakpoints`), plus the `prefers-reduced-motion` detector. |
153
175
  | [Drag & Drop](drag-drop) | Headless, accessible drag-and-drop for sortable lists and cross-list transfers, keyboard and pointer driven. |
154
176
  | [Virtualization](virtualization) | A headless windowing core (`injectVirtualizer`) plus a `[forVirtualViewport]` layer that renders only the visible slice of huge lists. |
155
177
  | [Table Virtualization](table-virtualization) | `[forTableVirtualized]`, the adapter that windows a `[forTable]` grid — its own entry point because it composes both the table and the windowing core. |
@@ -158,7 +180,7 @@ Headless — no DOM or ARIA of their own; an `inject*` / provider API that other
158
180
  ## Building
159
181
 
160
182
  ```bash
161
- ng build forty-cdk
183
+ pnpm build
162
184
  ```
163
185
 
164
186
  Build artifacts land in `dist/forty-cdk` (consumed locally via the `forty-cdk` path alias in the root `tsconfig.json`).
@@ -169,11 +191,11 @@ Tests run on Vitest via the Angular CLI builder `@angular/build:unit-test`:
169
191
 
170
192
  ```bash
171
193
  pnpm test # all specs, single pass
172
- pnpm exec ng test forty-cdk --watch # watch mode
194
+ pnpm test:watch # watch mode
173
195
  pnpm exec ng test forty-cdk --include "../accordion/src/accordion.spec.ts" # single file (path relative to projects/forty-cdk/src/)
174
196
  pnpm exec ng test forty-cdk --filter "Enter and Space select" # tests by name (regex)
175
197
  ```
176
198
 
177
199
  The `-- <path>` / `-- -t "<name>"` passthrough forms do **not** work on this setup (pnpm mangles the quoted `--`, so `ng` rejects it) — use the builder's own `--include` (repeatable) and `--filter` (regex) flags instead.
178
200
 
179
- Every primitive's test suite includes a case running under `provideZonelessChangeDetection()` to keep reactivity working without Zone.js.
201
+ The whole suite runs under `provideZonelessChangeDetection()`, so reactivity is verified without Zone.js on every spec rather than in a per-primitive case.
@@ -111,12 +111,12 @@ export class DemoFaq {
111
111
 
112
112
  - **Heading wrapper is your job.** The library does not render a heading around the trigger — wrap it in the heading level (`<h2>`–`<h6>`) appropriate to your document outline. Without it, screen-reader landmark navigation is broken.
113
113
  - **Use a real `<button type="button">` for the trigger.** Native Enter / Space activation and focus come for free; the directive does not synthesize them.
114
- - **`role="region"`** is added to every panel automatically. APG recommends suppressing it on accordions with 6+ panels to avoid landmark proliferation. An opt-out input will be added to `ForAccordionContent` if this surfaces in real usage.
114
+ - **`role="region"`** is added to every panel automatically. APG recommends suppressing it on accordions with 6+ panels to avoid landmark proliferation; there is currently no opt-out.
115
115
  - **Closed panels leave the accessibility tree.** While closed, `ForAccordionContent` sets `aria-hidden="true"` and `inert` on the panel, removing it from both the accessibility tree and the focus order. The directive does **not** apply `[hidden]`, so pick how to hide it visually:
116
116
  - **Mount / unmount with `@if (item.expanded())`** — the panel is absent from the DOM while closed; the cleanest path for `animate.enter` / `animate.leave`. The trigger emits `aria-controls` only while expanded, so the reference never dangles at an unmounted panel.
117
117
  - **Leave it mounted** — preserve internal state or run CSS-only transitions off `data-state`. Add `display: none` (or your own collapse animation) keyed on `[data-state="closed"]` to also hide it visually.
118
118
  - **`aria-disabled`** is applied to the open trigger only when single mode is active and `collapsible=false`, indicating the user cannot collapse it from this trigger.
119
- - **A truly disabled item (`[disabled]` on `[forAccordionItem]`) uses the native `disabled` attribute on the trigger, by design.** This is the sanctioned exception in [rule #561](https://github.com/tutkli/forty-cdk/issues/561): the trigger is a real single-purpose `<button>`, not a roving-tabindex collection item (each trigger stays independently in the Tab order; arrow-key navigation is the APG-optional enhancement on top). The disabled trigger leaves the Tab order and the arrow-key navigation (which already skips it), but stays in the accessibility tree so screen readers announce it as unavailable in browse mode. The [APG Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) does not require disabled headers to remain focusable.
119
+ - **A truly disabled item (`[disabled]` on `[forAccordionItem]`) uses the native `disabled` attribute on the trigger, by design.** The trigger is a real single-purpose `<button>`, not a roving-tabindex collection item (each trigger stays independently in the Tab order; arrow-key navigation is the APG-optional enhancement on top). The disabled trigger leaves the Tab order and the arrow-key navigation (which already skips it), but stays in the accessibility tree so screen readers announce it as unavailable in browse mode. The [APG Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) does not require disabled headers to remain focusable.
120
120
 
121
121
  ## Styling
122
122
 
@@ -91,7 +91,7 @@ forty-cdk ships no styles. Add your own class to each piece — the `for*` selec
91
91
 
92
92
  ## Behavior notes
93
93
 
94
- - **Browser support.** Native `aspect-ratio` is in Baseline 2021 (Chrome 88+, Firefox 89+, Safari 15+) — same target as Angular 20+, so no polyfill is needed.
94
+ - **Browser support.** Native `aspect-ratio` is in Baseline 2021 (Chrome 88+, Firefox 89+, Safari 15+), so no polyfill is needed on any browser Angular itself supports.
95
95
  - **Width still on you.** The directive only sets `aspect-ratio`; you decide width / max-width / display. The height is computed from the ratio.
96
96
  - **Children fill the box.** Use `width: 100%; height: 100%; object-fit: cover` on inner media to fill without distortion. The directive imposes no styles on children.
97
97
  - **No role, no a11y.** This is a layout utility. The element it sits on keeps whatever semantics you give it (`<div>`, `<figure>`, `<a>`, …).
@@ -82,6 +82,27 @@ Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'deskt
82
82
  | `active` | the largest breakpoint whose `min-width` matches, or `null` below the smallest |
83
83
  | `matches(query)` | escape hatch for an arbitrary media query (orientation, `prefers-*`, …) |
84
84
 
85
+ ### `injectPrefersReducedMotion`
86
+
87
+ The same shape for a different query: `injectPrefersReducedMotion()` returns a `Signal<boolean>` that is `true` while the user has asked their OS to suppress animation, and flips if they change the setting mid-session. Call it from an injection context, like `injectBreakpoints()`.
88
+
89
+ ```ts
90
+ import { computed } from '@angular/core';
91
+ import { injectPrefersReducedMotion } from 'forty-cdk/breakpoints';
92
+
93
+ export class Panel {
94
+ private readonly reducedMotion = injectPrefersReducedMotion();
95
+
96
+ protected readonly transition = computed(() =>
97
+ this.reducedMotion() ? 'none' : 'transform 200ms ease-out',
98
+ );
99
+ }
100
+ ```
101
+
102
+ It is published here because forty-cdk ships no styles: the animation on a `data-state` change is yours, so honouring the preference is yours too — and a signal is what a `computed()` or a `[style]` binding can branch on, which a CSS `@media` block cannot. Treat `true` as "skip the animated path entirely", not "shorten the duration": the setting asks for no motion, not less of it.
103
+
104
+ `bp.matches('(prefers-reduced-motion: reduce)')` resolves to the same thing. Prefer the named helper — it is the one the library's own motion-bearing primitives (drag gestures, carousel, drawer) read, so the query string stays spelled in one place.
105
+
85
106
  ## SSR
86
107
 
87
- On the server (or where `matchMedia` is unavailable) every query signal reads `false` and `active` reads `null`. No `matchMedia` access happens server-side, so the helper is safe under Angular Universal.
108
+ On the server (or where `matchMedia` is unavailable) every query signal reads `false` and `active` reads `null`. No `matchMedia` access happens server-side, so the helper is safe under Angular Universal. `injectPrefersReducedMotion()` reads `false` there for the same reason: the server render takes the animated branch, and the client applies the real preference on its first observation.
@@ -24,9 +24,9 @@ bootstrapApplication(App, {
24
24
  });
25
25
  ```
26
26
 
27
- `@internationalized/date` is a widely-used immutable date primitive; it works in every browser today with no polyfill, and its reference-equality-on-mutation makes it signal-friendly. Both `@internationalized/date` adapters operate on the **Gregorian** calendar today — `createDate` always builds a Gregorian date, so the grid stays Gregorian regardless of the runtime locale. True non-Gregorian calendar systems are deferred to the planned `Temporal.PlainDate` adapter ([#354](https://github.com/tutkli/forty-cdk/issues/354)), a non-breaking addition once the Temporal API is broadly available across browsers — the `DateAdapter<D>` seam means adopting it later is a drop-in, not a migration.
27
+ `@internationalized/date` is a widely-used immutable date primitive; it works in every browser today with no polyfill, and its reference-equality-on-mutation makes it signal-friendly.
28
28
 
29
- **Calendar system (Gregorian).** The adapter seam abstracts the date _library_ and locale-aware _formatting_, not the calendar _system_'s month structure. The grid, the month picker and the date field assume a Gregorian-structured year — exactly twelve months, `month` **1-12**, the year ending at month 12. Adapters over calendars with a different month structure (e.g. a 13-month year) are out of scope; calendar-system pluggability would be revisited with the `Temporal.PlainDate` adapter track ([#354](https://github.com/tutkli/forty-cdk/issues/354)). The optional `compareDate` hook overrides day-only _ordering_ only — it does not make the grid non-Gregorian.
29
+ **Calendar system (Gregorian).** The adapter seam abstracts the date _library_ and locale-aware _formatting_, not the calendar _system_'s month structure. Both `@internationalized/date` adapters build Gregorian dates, so the grid stays Gregorian regardless of the runtime locale, and the grid, the month picker and the date field all assume a Gregorian-structured year — exactly twelve months, `month` **1-12**, the year ending at month 12. Adapters over calendars with a different month structure (e.g. a 13-month year) are not supported. The optional `compareDate` hook overrides day-only _ordering_ only — it does not make the grid non-Gregorian.
30
30
 
31
31
  ## Anatomy
32
32
 
@@ -205,7 +205,7 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
205
205
 
206
206
  **`aria-selected`** in range mode is `"true"` across every committed-range cell (inclusive). During selecting (range null), it is `"false"` everywhere.
207
207
 
208
- **v1 scope.** Range mode is day-granular only — `granularity` / time is orthogonal and not supported in v1.
208
+ **Scope.** Range mode is day-granular only — `granularity` / time is orthogonal and not supported alongside it.
209
209
 
210
210
  ## Month / year navigation
211
211
 
@@ -140,7 +140,7 @@ Focus stays on the `<input>` the whole time the listbox is open, so options neve
140
140
 
141
141
  ## Anchoring to a field box
142
142
 
143
- By default the listbox is positioned against `[forComboboxInput]`. When the input lives inside a decorated field box — padding, a prefix icon, a clear button, or the multi-mode chip cluster — anchoring to the bare `<input>` makes the panel narrower than the visible field and offset from its edge. Wrap the field box in `[forComboboxAnchor]` so floating-ui positions (and sizes, via `--for-anchor-width`) the listbox against the box instead:
143
+ By default the listbox is positioned against `[forComboboxInput]`. When the input lives inside a decorated field box — padding, a prefix icon, a clear button, or the multi-mode chip cluster — anchoring to the bare `<input>` makes the panel narrower than the visible field and offset from its edge. Wrap the field box in `[forComboboxAnchor]` so floating-ui positions (and sizes, via `--for-floating-anchor-width`) the listbox against the box instead:
144
144
 
145
145
  ```html
146
146
  <div forCombobox #combobox="forCombobox" [(query)]="query" [(value)]="value">
@@ -150,7 +150,7 @@ By default the listbox is positioned against `[forComboboxInput]`. When the inpu
150
150
  <button class="clear" (click)="combobox.clear()">×</button>
151
151
  </div>
152
152
  @if (combobox.open()) {
153
- <div forComboboxContent style="width: var(--for-anchor-width)">
153
+ <div forComboboxContent style="width: var(--for-floating-anchor-width)">
154
154
  @for (it of filtered; track it.id) {
155
155
  <div forComboboxOption [value]="it.id" [label]="it.label">{{ it.label }}</div>
156
156
  }
@@ -582,7 +582,7 @@ When `[totalCount]` is omitted, the directive falls back to `options().length` a
582
582
  `[forCombobox]` exposes a `dir: 'ltr' | 'rtl'` input (default `'ltr'`). It drives:
583
583
 
584
584
  - **Chip keyboard navigation** — ArrowLeft / ArrowRight roles swap so they follow the visual order of the chip cluster, not the DOM order. See _Chip keyboard_ above.
585
- - **Default popover placement** — `align` defaults to `'start'` in LTR and `'end'` in RTL so the listbox anchors to the visually-leading edge of the input (`side` defaults to `'bottom'` in both). A consumer-provided `[align]` is honoured as-is — no automatic flip — so advanced layouts can pin an alignment regardless of writing direction.
585
+ - **Default popover placement** — `align` defaults to `'start'` in LTR and `'end'` in RTL so the listbox anchors to the visually-leading edge of the input (`side` defaults to `'bottom'` in both). A consumer-provided `[align]` is honoured as-is — no automatic flip — so advanced layouts can pin an alignment regardless of writing direction. `provideForComboboxDefaults({ align })` pins it for a whole scope the same way; its default is `null`, which is what "follow the writing direction" is spelled as there. `side` is scope-defaultable through the same provider, with the plain `'bottom'` fallback — writing direction does not enter into it.
586
586
 
587
587
  The native `<input>` handles caret movement and BiDi from the document's CSS `direction` already, so there's nothing extra to do for the typed text itself.
588
588
 
@@ -633,13 +633,13 @@ forty-cdk ships no styles. Add your own class to each piece — the for\* select
633
633
 
634
634
  `[forComboboxContent]` is portaled to `document.body` and gets its position resolved by floating-ui. The resolved geometry is exposed as custom properties on the content host (cleared on close):
635
635
 
636
- | Custom property | Type / range | Meaning |
637
- | -------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------- |
638
- | `--for-anchor-width` | px | Anchor (input / wrapper) width — match the listbox to the input with `width: var(--for-anchor-width)`. |
639
- | `--for-anchor-height` | px | Anchor height. |
640
- | `--for-available-width` | px | Space available along the inline axis (floating-ui `size` middleware) — clamp with `max-width`. |
641
- | `--for-available-height` | px | Space available along the block axis — clamp with `max-height`. |
642
- | `--for-content-transform-origin` | `<origin>` keywords | `transform-origin` matching the resolved side / align, so a `scale` enter animation pivots from the input. |
636
+ | Custom property | Type / range | Meaning |
637
+ | ----------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------- |
638
+ | `--for-floating-anchor-width` | px | Anchor (input / wrapper) width — match the listbox to the input with `width: var(--for-floating-anchor-width)`. |
639
+ | `--for-floating-anchor-height` | px | Anchor height. |
640
+ | `--for-floating-available-width` | px | Space available along the inline axis (floating-ui `size` middleware) — clamp with `max-width`. |
641
+ | `--for-floating-available-height` | px | Space available along the block axis — clamp with `max-height`. |
642
+ | `--for-floating-content-transform-origin` | `<origin>` keywords | `transform-origin` matching the resolved side / align, so a `scale` enter animation pivots from the input. |
643
643
 
644
644
  > `[forComboboxContent]` is portaled to `document.body`, so it lives outside your component's view-encapsulated styles. Style it with global CSS (or a class you pass through) and the shared positioner properties above. See [Styling floating content](../../../docs/styling-floating-content.md) for the full positioner-variable list and the portal styling rules.
645
645
 
@@ -120,8 +120,8 @@ Both triggers carry `[menuPositioning]`, a partial `{ side, align, sideOffset, a
120
120
  | Property | Type | Description |
121
121
  | --------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
122
  | `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
123
- | `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'` |
124
- | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
123
+ | `side` | `input<string>` | Anchor side relative to the pointer. The default is read from `provideForContextMenuDefaults` for the surrounding scope.<br>**Default:** `'bottom'` |
124
+ | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`). The default is read from `provideForContextMenuDefaults` for the surrounding scope.<br>**Default:** `'start'` |
125
125
  | `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
126
126
  | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
127
127
  | `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForContextMenuDefaults` for the surrounding scope — set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
@@ -161,7 +161,7 @@ Same vetoable dismiss API as DropdownMenu. Call `preventDefault()` on the emitte
161
161
 
162
162
  forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes listed under [Data attributes](#data-attributes).
163
163
 
164
- > The menu content (`[forMenuContent]`, from the [`menu/`](../menu/README.md) folder) portals to `document.body`, so it sits outside the trigger's DOM subtree — descendant selectors won't reach it. Style it with **global CSS** or a class on the content element. The content host also exposes the shared positioner custom properties (`--for-anchor-width` / `--for-anchor-height`, `--for-available-width` / `--for-available-height`, `--for-content-transform-origin`); see [Styling floating content](../../../docs/styling-floating-content.md) for the full list and the animation rules.
164
+ > The menu content (`[forMenuContent]`, from the [`menu/`](../menu/README.md) folder) portals to `document.body`, so it sits outside the trigger's DOM subtree — descendant selectors won't reach it. Style it with **global CSS** or a class on the content element. The content host also exposes the shared positioner custom properties (`--for-floating-anchor-width` / `--for-floating-anchor-height`, `--for-floating-available-width` / `--for-floating-available-height`, `--for-floating-content-transform-origin`); see [Styling floating content](../../../docs/styling-floating-content.md) for the full list and the animation rules.
165
165
 
166
166
  ```css
167
167
  .context-menu-trigger[data-state='open'] {
@@ -188,6 +188,78 @@ providers: [
188
188
 
189
189
  `segmentLabels` supplies each segment's default `aria-label`, keyed by part type. Unset keys keep the library default (the part name, and `'AM/PM'` for the `dayPeriod` segment), so overriding a single key never wipes the rest. A segment's own `[ariaLabel]` still wins over the scope default.
190
190
 
191
+ ## Range selection — `ForDateRangeField`
192
+
193
+ For a date range use the dedicated `ForDateRangeField` root (selector `[forDateRangeField]`), shipped from this same entry point. It is the keyboard-first, form-capable counterpart to [DateRangePicker](../date-picker/README.md): two labelled `role="group"` endpoints (start / end), each holding a row of spinbutton segments — the same machinery as `ForDateField` — nested inside one outer `role="group"`. It implements `FormValueControl<DateRange<D> | null>`, the **same** contract as `ForDateRangePicker`, so the committed range auto-wires with `[formField]`. The value stays `null` until **both** endpoints are fully entered and ordered (`start <= end`).
194
+
195
+ The pieces are the range-specific `[forDateRangeFieldStart]` / `[forDateRangeFieldEnd]` endpoint groups plus `[forDateRangeFieldSegment]` / `[forDateRangeFieldLiteral]`; each endpoint exposes its own `segments()` list, so the same `@for` template renders both sides.
196
+
197
+ ```html
198
+ <div forDateRangeField [(value)]="stay" ariaLabel="Stay">
199
+ <div forDateRangeFieldStart #start="forDateRangeFieldStart">
200
+ @for (seg of start.segments(); track seg.id) { @if (seg.isLiteral) {
201
+ <span forDateRangeFieldLiteral>{{ seg.text }}</span>
202
+ } @else {
203
+ <span forDateRangeFieldSegment [segment]="seg.type!">{{ seg.text }}</span>
204
+ } }
205
+ </div>
206
+ <span aria-hidden="true">–</span>
207
+ <div forDateRangeFieldEnd #end="forDateRangeFieldEnd">
208
+ @for (seg of end.segments(); track seg.id) { @if (seg.isLiteral) {
209
+ <span forDateRangeFieldLiteral>{{ seg.text }}</span>
210
+ } @else {
211
+ <span forDateRangeFieldSegment [segment]="seg.type!">{{ seg.text }}</span>
212
+ } }
213
+ </div>
214
+ </div>
215
+ ```
216
+
217
+ ```ts
218
+ import {
219
+ ForDateRangeField,
220
+ ForDateRangeFieldEnd,
221
+ ForDateRangeFieldLiteral,
222
+ ForDateRangeFieldSegment,
223
+ ForDateRangeFieldStart,
224
+ } from 'forty-cdk/date-field';
225
+ import type { DateRange } from 'forty-cdk/shared';
226
+
227
+ readonly model = signal({ stay: null as DateRange<CalendarDate> | null });
228
+ readonly booking = form(this.model);
229
+ ```
230
+
231
+ ### `ForDateRangeField` API
232
+
233
+ | Property | Type | Description |
234
+ | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
235
+ | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
236
+ | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` — see note below.<br>**Default:** `null` |
237
+ | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
238
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
239
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
240
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
241
+ | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints.<br>**Default:** `{}` |
242
+ | `ariaLabel` | `input<string \| null>` | Accessible name for the whole range field group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
243
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
244
+
245
+ The endpoint groups each accept an `ariaLabel` input for their own group label, falling back to the scope defaults (`'Start date'` / `'End date'`). Plus the shared `FormUiControl` members bound automatically by `[formField]`.
246
+
247
+ > **Why `minDate` / `maxDate`, not `min` / `max`?** Beyond the reason above, `FormUiControl.min` / `max` are additionally typed `NonNullable<TValue>` — the range object itself — which is meaningless as a bound.
248
+
249
+ `[forDateRangeField]` reflects the same `data-disabled` / `data-readonly` / `data-empty` hooks as `[forDateField]`, plus `data-range-error`; `[forDateRangeFieldSegment]` reflects the same four segment hooks. `data-empty` marks the field only while **both** endpoints are entirely empty; a partially-filled or complete-but-disordered range is **not** empty.
250
+
251
+ ### Ordering
252
+
253
+ The two endpoints are typed independently, so order is not guaranteed by construction the way the picker's two-click flow guarantees it. The field preserves the `DateRange` `end >= start` invariant by **never emitting an out-of-order range**: when both endpoints are complete but `start > end`, the typed segments are kept (not silently rewritten), `value` stays `null`, and the root reflects `aria-invalid="true"` + `data-range-error` so the disorder is perceivable and stylable. Editing either endpoint back into order emits the range.
254
+
255
+ ### Range keyboard and accessibility
256
+
257
+ Each endpoint is its own tab stop, so `Tab` moves start group → end group → next control; arrows move between segments **within** an endpoint. Every other key behaves as in the [Keyboard](#keyboard) table below. Roving tabindex is per endpoint, and `aria-invalid="true"` is reflected on the root when the form marks it invalid **or** when two complete endpoints are out of order; everything else matches the [Accessibility](#accessibility) notes below.
258
+
259
+ ### Range scope defaults
260
+
261
+ `provideForDateRangeFieldDefaults` mirrors `provideForDateFieldDefaults` and adds `startLabel` / `endLabel` for the two endpoint group `aria-label`s (`'Start date'` / `'End date'` by default). Both wrapper patterns work via `FOR_DATE_RANGE_FIELD_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_FIELD_HOST_DIRECTIVE_OUTPUTS` — see [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
262
+
191
263
  ## Keyboard
192
264
 
193
265
  Key behavior applies per segment. Horizontal arrows mirror under `dir="rtl"`.
@@ -157,7 +157,7 @@ The library is styleless: presence in the DOM is the consumer's job (`@if (open(
157
157
  | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
158
158
  | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar — bind its `[locale]` too.<br>**Default:** `null` → runtime locale |
159
159
  | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
160
- | `side` / `align` | `input` | Anchored placement (popover mode only).<br>**Default:** `'bottom'` / `'start'` |
160
+ | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
161
161
  | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
162
162
 
163
163
  Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `name`, `errors`, and the `touched` model) and the floating tunables (`sideOffset`, `alignOffset`, `avoidCollisions`, `collisionPadding`, `sticky`, `hideWhenDetached`).
@@ -219,7 +219,7 @@ By default the surface is positioned against `[forDatePickerTrigger]`. When the
219
219
  </div>
220
220
  ```
221
221
 
222
- `[forDatePickerAnchor]` changes **only** positioning. The trigger keeps `aria-haspopup` / `aria-expanded` / `aria-controls`, the click toggle, focus return on close, and its exemption from outside-pointer dismissal. Without an anchor the surface falls back to the trigger, so existing markup is unaffected. At most one `[forDatePickerAnchor]` per `[forDatePicker]` — a second one throws `[forty-cdk/date-picker]`. (A calendar has its own intrinsic width and ignores `--for-anchor-width`, so the anchor mainly affects start / side alignment to the box edge.)
222
+ `[forDatePickerAnchor]` changes **only** positioning. The trigger keeps `aria-haspopup` / `aria-expanded` / `aria-controls`, the click toggle, focus return on close, and its exemption from outside-pointer dismissal. Without an anchor the surface falls back to the trigger, so existing markup is unaffected. At most one `[forDatePickerAnchor]` per `[forDatePicker]` — a second one throws `[forty-cdk/date-picker]`. (A calendar has its own intrinsic width and ignores `--for-floating-anchor-width`, so the anchor mainly affects start / side alignment to the box edge.)
223
223
 
224
224
  ## Modal vs non-modal
225
225
 
@@ -275,10 +275,11 @@ The value display (`[forDatePickerValue]`) automatically appends the time to its
275
275
 
276
276
  For date-range selection use the dedicated `ForDateRangePicker` root (selector `[forDateRangePicker]`). It is the root **and** the form value, implementing `FormValueControl<DateRange<D> | null>`, so the committed range auto-wires with `[formField]` exactly like any other control.
277
277
 
278
- It reuses the same pieces — `[forDatePickerTrigger]`, `[forDatePickerContent]`, `[forDatePickerValue]`, `[forDatePickerAnchor]` — through a shared base, and provides `FOR_DATE_PICKER_CONTEXT` so they resolve under it. Project a `[forCalendar]` in `selectionMode="range"` and bind its range to the picker's `value`; the two-click anchor → commit flow keeps `value` `null` until both endpoints are chosen (the form never sees a half-entered range), and `start <= end` is an invariant. Range is day-granular in v1 (no time composition).
278
+ It reuses the same pieces — `[forDatePickerTrigger]`, `[forDatePickerContent]`, `[forDatePickerValue]`, `[forDatePickerAnchor]` — through a shared base, and provides `FOR_DATE_PICKER_CONTEXT` so they resolve under it. Project a `[forCalendar]` in `selectionMode="range"` and bind its range to the picker's `value`; the two-click anchor → commit flow keeps `value` `null` until both endpoints are chosen (the form never sees a half-entered range), and `start <= end` is an invariant. Range is day-granular (no time composition).
279
279
 
280
280
  ```ts
281
- import { type DateRange, ForDateRangePicker } from 'forty-cdk/date-picker';
281
+ import { ForDateRangePicker } from 'forty-cdk/date-picker';
282
+ import type { DateRange } from 'forty-cdk/shared';
282
283
  import { form } from '@angular/forms/signals';
283
284
 
284
285
  interface Booking {
@@ -321,7 +322,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
321
322
  - **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts.
322
323
  - **Bounds naming.** `minDate` / `maxDate` (not `min` / `max`) for the same reason as `ForDatePicker` — and additionally because `FormUiControl.min` / `max` are typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
323
324
 
324
- Defaults are configured with `provideForDateRangePickerDefaults` (`sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples — see [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
325
+ Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `align` / `sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples — see [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
325
326
 
326
327
  ## Keyboard
327
328
 
@@ -338,7 +339,7 @@ Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA
338
339
 
339
340
  - **`role="combobox"`** on the trigger with **`aria-haspopup="dialog"`**, `aria-expanded` reflecting `open()`, and `aria-controls` pointing at the surface while open — the same shape `[forSelectTrigger]` / `[forTimePickerTrigger]` ship, with the `dialog` popup token ARIA 1.2 allows for a combobox surface. The role is also what makes the form-control ARIA below legal: `role="button"` supports neither `aria-readonly` nor `aria-required`.
340
341
  - **`role="dialog"`** on the surface, named by `[ariaLabel]` (or `aria-labelledby` the trigger when no label is set). `aria-modal="true"` only in modal mode (truthy-only).
341
- - **Form-control ARIA** (`aria-readonly` / `aria-required` / `aria-invalid` / `aria-busy`) is reflected on the focusable trigger so assistive tech announces validity on the element that takes focus, alongside the `data-readonly` styling hook. The disabled state is the exception: it reflects through the native `disabled` attribute alone (plus `data-disabled`), never `aria-disabled` — one channel per #561 D2.
342
+ - **Form-control ARIA** (`aria-readonly` / `aria-required` / `aria-invalid` / `aria-busy`) is reflected on the focusable trigger so assistive tech announces validity on the element that takes focus, alongside the `data-readonly` styling hook. The disabled state is the exception: it reflects through the native `disabled` attribute alone (plus `data-disabled`), never `aria-disabled` — one channel only.
342
343
  - **Inside a `[forField]` the labelled element is the trigger**, not the `[forDatePicker]` / `[forDateRangePicker]` wrapper: the field's `controlId` and its `aria-labelledby` / `aria-describedby` / `aria-errormessage` land on `[forDatePickerTrigger]`, so `[forLabel]`'s `for` points at the element that takes focus, clicking a non-`<label>` `[forLabel]` opens the surface, and Signal Forms' focus-on-error reaches the trigger. `role="combobox"` takes its name from the author, so this is the channel that names the control — the root's `[ariaLabel]` names the `role="dialog"` surface instead.
343
344
  - **Focus management**: focus enters the surface on open (the calendar's roving cell in non-modal mode) and returns to the trigger on close, both vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)`.
344
345
  - **Dismissal**: Escape (`(escapeKeyDown)`) and outside-pointer (`(pointerDownOutside)` / `(interactOutside)`) close the surface, each vetoable.
@@ -347,7 +348,7 @@ Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA
347
348
 
348
349
  forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed under [Data attributes](#data-attributes).
349
350
 
350
- > `[forDatePickerContent]` is portaled to `document.body`, so it lives outside your component's view-encapsulated styles. Style it with **global CSS** (or a class you pass through) rather than component-scoped rules — see [Styling floating content](../../../docs/styling-floating-content.md). In non-modal (anchored) mode the surface also exposes the shared positioner custom properties (`--for-anchor-width` / `--for-anchor-height`, `--for-available-width` / `--for-available-height`, `--for-content-transform-origin`); that same guide tabulates the full set.
351
+ > `[forDatePickerContent]` is portaled to `document.body`, so it lives outside your component's view-encapsulated styles. Style it with **global CSS** (or a class you pass through) rather than component-scoped rules — see [Styling floating content](../../../docs/styling-floating-content.md). In non-modal (anchored) mode the surface also exposes the shared positioner custom properties (`--for-floating-anchor-width` / `--for-floating-anchor-height`, `--for-floating-available-width` / `--for-floating-available-height`, `--for-floating-content-transform-origin`); that same guide tabulates the full set.
351
352
 
352
353
  ```css
353
354
  .date-picker-trigger .date-picker-value[data-placeholder] {
package/dialog/README.md CHANGED
@@ -75,7 +75,7 @@ This is different from trigger-anchored overlays (Popover, DropdownMenu, etc.) w
75
75
 
76
76
  The payload is a `ForDialogCloseReason` string (`'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`) — use it if you need to branch on why the dialog closed, for example to show a "save changes?" prompt before dismissing. Emitting `(dismiss)` without acting on it is always safe: you can call `preventDefault()` on the preceding dismiss outputs (`(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)`) to suppress the `(dismiss)` entirely.
77
77
 
78
- > **Declarative vs. imperative naming asymmetry.** The declarative output is `(dismiss)`, but the imperative handle method stays `ForDialogRef.close()`, the `[forDialogClose]` directive selector is unchanged, and the `ForDialogCloseReason` type keeps its name. This is intentional: the output rename removes the native-event collision (see [#814](https://github.com/tutkli/forty-cdk/issues/814)) while the imperative surface follows the convention established before that rename.
78
+ > **The declarative and imperative surfaces spell this differently, on purpose.** The output is `(dismiss)` — an output named `close` would collide with the native DOM event and break any wrapper re-exposing it through `hostDirectives`. Nothing else changes name: the imperative handle method is `ForDialogRef.close()`, the directive selector is `[forDialogClose]`, and the payload type is `ForDialogCloseReason`.
79
79
 
80
80
  ### Trigger / surface id wiring
81
81
 
@@ -76,7 +76,7 @@ The library ships no styles. Hide animations / transitions can be driven off `da
76
76
  | `data-state` | `open` \| `closed` |
77
77
  | `data-disabled` | present \| absent |
78
78
 
79
- Reflects on its host: `id`, `aria-expanded`, `aria-controls`, `disabled`, `data-state`. Toggles the state on click. The disabled reflection (the native `disabled` attribute plus `data-disabled`; no `aria-disabled`, single channel per #561 D2) and the click guard follow the effective state — the trigger's own `disabled` OR the root's.
79
+ Reflects on its host: `id`, `aria-expanded`, `aria-controls`, `disabled`, `data-state`. Toggles the state on click. The disabled reflection (the native `disabled` attribute plus `data-disabled`; no `aria-disabled` — one channel only) and the click guard follow the effective state — the trigger's own `disabled` OR the root's.
80
80
 
81
81
  `aria-controls` is emitted only while open — mirroring the overlay triggers' open-only gating — so the reference never dangles at an unmounted panel under the recommended `@if (open())` mount pattern.
82
82
 
@@ -392,8 +392,8 @@ Both `data-dragging` rows hold for a drag a **coordinator** composing the list o
392
392
  than starting through `[forDraggable]` itself — the keyboard lift of `[forVirtualReorder]`, and
393
393
  the virtualized branch of `[forTableRowReorder]`. Those intercept the lift key before the item
394
394
  sees it, so the list carries no lift state for the gesture, and the coordinator marks the item
395
- instead ([#1693](https://github.com/tutkli/forty-cdk/issues/1693)). Styling keyed off either
396
- attribute therefore behaves the same whether the collection is windowed or not.
395
+ instead. Styling keyed off either attribute therefore behaves the same whether the collection is
396
+ windowed or not.
397
397
 
398
398
  The `data-for-drag-preview` row is also the supported hook for **keeping the clone out of element
399
399
  queries**. The default preview is a `cloneNode(true)` copy appended to `document.body`, so for the
@@ -401,8 +401,7 @@ whole gesture — and past the drop, while a settle transition runs — it answe
401
401
  selector (`[forDraggable]`, or a composed one such as `[forTableRow]`) and repeats its `data-index`.
402
402
  `id` and `data-testid` are stripped from the clone and its whole subtree, so a hook that identifies
403
403
  a single element stays unambiguous; anything that **enumerates** items by attribute selector during
404
- a drag must filter the preview out with `:not([data-for-drag-preview])`
405
- ([#1691](https://github.com/tutkli/forty-cdk/issues/1691)).
404
+ a drag must filter the preview out with `:not([data-for-drag-preview])`.
406
405
 
407
406
  ## Sortable list
408
407
 
package/drawer/README.md CHANGED
@@ -272,7 +272,7 @@ Declaratively the same recipe is the four vetoable outputs on `[forDrawer]`: `(i
272
272
 
273
273
  `ForDrawerCloseReason`: `'escape' | 'backdrop' | 'pointerDownOutside' | 'focusOutside' | 'closeButton' | 'swipe' | 'programmatic'`.
274
274
 
275
- > **Declarative vs. imperative naming asymmetry.** The declarative output is `(dismiss)`, but the imperative handle method stays `ForDrawerRef.close()`, the `[forDrawerClose]` directive selector is unchanged, and the `ForDrawerCloseReason` type keeps its name. This is intentional: the output rename removes the native-event collision (see [#814](https://github.com/tutkli/forty-cdk/issues/814)) while the imperative surface follows the convention established before that rename.
275
+ > **The declarative and imperative surfaces spell this differently, on purpose.** The output is `(dismiss)` — an output named `close` would collide with the native DOM event and break any wrapper re-exposing it through `hostDirectives`. Nothing else changes name: the imperative handle method is `ForDrawerRef.close()`, the directive selector is `[forDrawerClose]`, and the payload type is `ForDrawerCloseReason`.
276
276
 
277
277
  | Data attribute | Values |
278
278
  | ------------------------ | -------------------------------------------- |
@@ -319,7 +319,7 @@ Three accepted shapes:
319
319
  - `'NN%'` — equivalent to a fraction (`'50%' === 0.5`).
320
320
  - `'NNpx'` — absolute pixel size measured from the anchored edge.
321
321
 
322
- Pass them in **strictly increasing** order (closest-to-edge first). The directive throws `[forty-cdk/drawer] snapPoints must be strictly increasing (closest-to-edge first).` otherwise. `fadeFromIndex` must be a valid index into `snapPoints`.
322
+ Pass them in **strictly increasing** order (closest-to-edge first); the directive throws `FORCDK-DRAWER-009` otherwise. Mixed units (`'200px'` next to `0.5`) can only be ordered against the live drawer size, so they are re-checked on first measurement and fail with `FORCDK-DRAWER-010`, which names the offending point and the dimension it resolved against. `fadeFromIndex` must be a valid index into `snapPoints`.
323
323
 
324
324
  ```ts
325
325
  [snapPoints] =