forty-cdk 0.21.0 → 0.22.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 (175) hide show
  1. package/README.md +50 -28
  2. package/accordion/README.md +2 -2
  3. package/aspect-ratio/README.md +1 -1
  4. package/calendar/README.md +3 -3
  5. package/combobox/README.md +9 -9
  6. package/context-menu/README.md +1 -1
  7. package/date-field/README.md +72 -0
  8. package/date-picker/README.md +6 -5
  9. package/dialog/README.md +1 -1
  10. package/disclosure/README.md +1 -1
  11. package/drag-drop/README.md +15 -0
  12. package/drawer/README.md +2 -2
  13. package/dropdown-menu/README.md +2 -2
  14. package/fesm2022/forty-cdk-accordion.mjs +16 -7
  15. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-avatar.mjs +58 -11
  17. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-breakpoints.mjs +7 -2
  19. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-button.mjs +1 -3
  21. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-calendar.mjs +9 -4
  23. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-carousel.mjs +15 -6
  25. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-checkbox.mjs +7 -2
  27. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-combobox.mjs +110 -102
  29. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-context-menu.mjs +18 -15
  31. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-core.mjs +783 -1365
  33. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-date-field.mjs +663 -42
  35. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-date-picker.mjs +69 -50
  37. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-dialog.mjs +18 -10
  39. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-disclosure.mjs +8 -3
  41. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-drag-drop.mjs +55 -10
  43. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-drawer.mjs +202 -127
  45. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-dropdown-menu.mjs +10 -8
  47. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-field.mjs +52 -18
  49. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-fieldset.mjs +7 -3
  51. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-file-upload.mjs +7 -2
  53. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  54. package/fesm2022/forty-cdk-hover-card.mjs +14 -10
  55. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  56. package/fesm2022/forty-cdk-internationalized-date.mjs +2 -2
  57. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-listbox.mjs +29 -4
  59. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  60. package/fesm2022/forty-cdk-menu.mjs +42 -9
  61. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-menubar.mjs +40 -54
  63. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-meter.mjs +7 -2
  65. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-navigation-menu.mjs +65 -117
  67. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-number-input.mjs +7 -2
  69. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-otp-input.mjs +7 -2
  71. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-pagination.mjs +7 -2
  73. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-pane-resizer.mjs +1 -1
  75. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-popover.mjs +16 -12
  77. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  78. package/fesm2022/forty-cdk-progress.mjs +7 -2
  79. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-radio-group.mjs +14 -4
  81. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  82. package/fesm2022/forty-cdk-scroll-area.mjs +18 -5
  83. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  84. package/fesm2022/forty-cdk-search.mjs +7 -2
  85. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  86. package/fesm2022/forty-cdk-select.mjs +69 -50
  87. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  88. package/fesm2022/forty-cdk-slider.mjs +27 -11
  89. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  90. package/fesm2022/forty-cdk-stepper.mjs +15 -6
  91. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  92. package/fesm2022/forty-cdk-table-virtualization.mjs +13 -4
  93. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  94. package/fesm2022/forty-cdk-table.mjs +247 -172
  95. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-tabs.mjs +22 -2
  97. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-time-field.mjs +687 -40
  99. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-time-picker.mjs +24 -16
  101. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  102. package/fesm2022/forty-cdk-toast.mjs +97 -55
  103. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  104. package/fesm2022/forty-cdk-toggle.mjs +7 -2
  105. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  106. package/fesm2022/forty-cdk-toolbar.mjs +13 -2
  107. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  108. package/fesm2022/forty-cdk-tooltip.mjs +14 -10
  109. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  110. package/fesm2022/forty-cdk-tree.mjs +189 -72
  111. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  112. package/fesm2022/forty-cdk-virtual-reorder.mjs +65 -10
  113. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  114. package/fesm2022/forty-cdk-virtualization.mjs +8 -4
  115. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  116. package/field/README.md +2 -0
  117. package/hover-card/README.md +13 -13
  118. package/internationalized-date/README.md +1 -1
  119. package/menu/README.md +8 -8
  120. package/menubar/README.md +1 -1
  121. package/package.json +1 -9
  122. package/popover/README.md +13 -13
  123. package/scroll-area/README.md +1 -1
  124. package/select/README.md +13 -13
  125. package/shared/README.md +1 -28
  126. package/table/README.md +3 -3
  127. package/time-field/README.md +77 -0
  128. package/time-picker/README.md +2 -2
  129. package/tooltip/README.md +13 -18
  130. package/tree/README.md +54 -26
  131. package/types/forty-cdk-accordion.d.ts +3 -4
  132. package/types/forty-cdk-avatar.d.ts +35 -11
  133. package/types/forty-cdk-button.d.ts +1 -3
  134. package/types/forty-cdk-calendar.d.ts +1 -1
  135. package/types/forty-cdk-carousel.d.ts +29 -18
  136. package/types/forty-cdk-combobox.d.ts +124 -111
  137. package/types/forty-cdk-context-menu.d.ts +10 -9
  138. package/types/forty-cdk-core.d.ts +642 -1011
  139. package/types/forty-cdk-date-field.d.ts +415 -38
  140. package/types/forty-cdk-date-picker.d.ts +28 -39
  141. package/types/forty-cdk-dialog.d.ts +6 -8
  142. package/types/forty-cdk-disclosure.d.ts +1 -1
  143. package/types/forty-cdk-drag-drop.d.ts +41 -4
  144. package/types/forty-cdk-drawer.d.ts +6 -8
  145. package/types/forty-cdk-dropdown-menu.d.ts +2 -2
  146. package/types/forty-cdk-field.d.ts +20 -11
  147. package/types/forty-cdk-internationalized-date.d.ts +2 -2
  148. package/types/forty-cdk-listbox.d.ts +7 -0
  149. package/types/forty-cdk-menu.d.ts +11 -3
  150. package/types/forty-cdk-menubar.d.ts +33 -49
  151. package/types/forty-cdk-navigation-menu.d.ts +54 -110
  152. package/types/forty-cdk-popover.d.ts +2 -2
  153. package/types/forty-cdk-radio-group.d.ts +1 -1
  154. package/types/forty-cdk-scroll-area.d.ts +3 -0
  155. package/types/forty-cdk-select.d.ts +142 -59
  156. package/types/forty-cdk-slider.d.ts +5 -2
  157. package/types/forty-cdk-stepper.d.ts +2 -3
  158. package/types/forty-cdk-table.d.ts +212 -207
  159. package/types/forty-cdk-tabs.d.ts +15 -0
  160. package/types/forty-cdk-time-field.d.ts +441 -36
  161. package/types/forty-cdk-time-picker.d.ts +4 -4
  162. package/types/forty-cdk-toast.d.ts +14 -16
  163. package/types/forty-cdk-toolbar.d.ts +6 -0
  164. package/types/forty-cdk-tree.d.ts +120 -78
  165. package/types/forty-cdk-virtual-reorder.d.ts +15 -1
  166. package/types/forty-cdk-virtualization.d.ts +2 -3
  167. package/virtual-reorder/README.md +2 -0
  168. package/date-range-field/README.md +0 -253
  169. package/fesm2022/forty-cdk-date-range-field.mjs +0 -656
  170. package/fesm2022/forty-cdk-date-range-field.mjs.map +0 -1
  171. package/fesm2022/forty-cdk-time-range-field.mjs +0 -696
  172. package/fesm2022/forty-cdk-time-range-field.mjs.map +0 -1
  173. package/time-range-field/README.md +0 -263
  174. package/types/forty-cdk-date-range-field.d.ts +0 -432
  175. 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,21 +20,24 @@ 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
 
@@ -46,6 +51,24 @@ Optional — install only if you use the matching entry point / primitives:
46
51
 
47
52
  `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`.
48
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.
69
+
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.
71
+
49
72
  ## Primitives
50
73
 
51
74
  Every primitive ships as its own secondary entry point, and each lives in its own folder under `projects/forty-cdk/` with its own `README.md` documenting its anatomy, API, keyboard interaction and styling hooks. Standalone directives plus `"sideEffects": false` mean your bundle only ever includes the primitives you import.
@@ -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 and announcements. |
138
160
 
139
161
  ### Feedback
140
162
 
@@ -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>`, …).
@@ -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
  }
@@ -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
 
@@ -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"`.
@@ -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 {
@@ -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
 
@@ -388,6 +388,21 @@ for the full analysis.
388
388
  | `data-drag-animating` | `[forDraggable]` | Present while the item's FLIP reorder transition plays (requires `[animateReorder]`) |
389
389
  | `data-settling` | preview element | Present while the drop-settle transition plays (requires `[animateReorder]`) |
390
390
 
391
+ Both `data-dragging` rows hold for a drag a **coordinator** composing the list owns rather
392
+ than starting through `[forDraggable]` itself — the keyboard lift of `[forVirtualReorder]`, and
393
+ the virtualized branch of `[forTableRowReorder]`. Those intercept the lift key before the item
394
+ sees it, so the list carries no lift state for the gesture, and the coordinator marks the item
395
+ instead. Styling keyed off either attribute therefore behaves the same whether the collection is
396
+ windowed or not.
397
+
398
+ The `data-for-drag-preview` row is also the supported hook for **keeping the clone out of element
399
+ queries**. The default preview is a `cloneNode(true)` copy appended to `document.body`, so for the
400
+ whole gesture — and past the drop, while a settle transition runs — it answers the item's own
401
+ selector (`[forDraggable]`, or a composed one such as `[forTableRow]`) and repeats its `data-index`.
402
+ `id` and `data-testid` are stripped from the clone and its whole subtree, so a hook that identifies
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
+
391
406
  ## Sortable list
392
407
 
393
408
  ```html
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] =
@@ -185,13 +185,13 @@ Once focus is in the menu, see [`menu/README.md`](../menu/README.md) for the in-
185
185
 
186
186
  `[forDropdownMenu]` implements the [WAI-ARIA Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). The trigger wires `aria-haspopup="menu"`, `aria-expanded`, and `aria-controls`; the menu surface and item roles come from the shared [`menu/`](../menu/README.md) primitives.
187
187
 
188
- A disabled trigger (its own `[disabled]`, or the root's) reflects through a **single channel**: the native `disabled` attribute plus the `data-disabled` styling hook. No `aria-disabled` is emitted — the trigger is a real single-purpose `<button>` and the native attribute already conveys the state to assistive technology, per the sanctioned native-`disabled` case in [rule #561](https://github.com/tutkli/forty-cdk/issues/561) (D2). Style the disabled trigger off `[disabled]` or `[data-disabled]`, never `[aria-disabled]`.
188
+ A disabled trigger (its own `[disabled]`, or the root's) reflects through a **single channel**: the native `disabled` attribute plus the `data-disabled` styling hook. No `aria-disabled` is emitted — the trigger is a real single-purpose `<button>` and the native attribute already conveys the state to assistive technology. Style the disabled trigger off `[disabled]` or `[data-disabled]`, never `[aria-disabled]`.
189
189
 
190
190
  ## Styling
191
191
 
192
192
  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).
193
193
 
194
- > The menu content (`[forMenuContent]`) portals to `document.body`, so a class scoped to your trigger's component cannot reach it. Style it with **global CSS** or a class you pass through (see [Styling floating content](../../../docs/styling-floating-content.md)). The content host also exposes the shared positioner custom properties — `--for-anchor-width` / `--for-anchor-height`, `--for-available-width` / `--for-available-height`, and `--for-content-transform-origin` — documented in full in [Styling floating content](../../../docs/styling-floating-content.md).
194
+ > The menu content (`[forMenuContent]`) portals to `document.body`, so a class scoped to your trigger's component cannot reach it. Style it with **global CSS** or a class you pass through (see [Styling floating content](../../../docs/styling-floating-content.md)). 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`, and `--for-floating-content-transform-origin` — documented in full in [Styling floating content](../../../docs/styling-floating-content.md).
195
195
 
196
196
  ```css
197
197
  .dropdown-menu-trigger .chevron {
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, input, booleanAttribute, model, Directive, computed, signal, ElementRef } from '@angular/core';
3
- import { assertRootContext, Collection, injectTextDirection, moveIndex, IdGenerator, adoptHostId, hostButtonType, registerHandle, reflectDisabled, resolveListNavigation, hostLabelledBy } from 'forty-cdk/core';
3
+ import { orphanContextError, assertRootContext, Collection, injectTextDirection, moveIndex, IdGenerator, adoptHostId, hostButtonType, registerHandle, reflectDisabled, resolveListNavigation, hostLabelledBy } from 'forty-cdk/core';
4
4
 
5
5
  /**
6
6
  * DI token for the accordion's coordination surface, provided by `[forAccordion]`.
@@ -17,7 +17,12 @@ const FOR_ACCORDION_ITEM_CONTEXT = new InjectionToken('FOR_ACCORDION_ITEM_CONTEX
17
17
  function injectAccordionContext(piece) {
18
18
  const ctx = inject(FOR_ACCORDION_CONTEXT, { optional: true });
19
19
  if (!ctx) {
20
- throw new Error(`[forty-cdk/accordion] ${piece} must be used inside a [forAccordion] element.`);
20
+ throw orphanContextError({
21
+ code: 'FORCDK-ACCORDION-001',
22
+ piece,
23
+ root: '[forAccordion]',
24
+ token: 'FOR_ACCORDION_CONTEXT',
25
+ });
21
26
  }
22
27
  assertRootContext({
23
28
  entryPoint: 'accordion',
@@ -31,7 +36,12 @@ function injectAccordionContext(piece) {
31
36
  function injectAccordionItemContext(piece) {
32
37
  const ctx = inject(FOR_ACCORDION_ITEM_CONTEXT, { optional: true });
33
38
  if (!ctx) {
34
- throw new Error(`[forty-cdk/accordion] ${piece} must be used inside a [forAccordionItem] element.`);
39
+ throw orphanContextError({
40
+ code: 'FORCDK-ACCORDION-002',
41
+ piece,
42
+ root: '[forAccordionItem]',
43
+ token: 'FOR_ACCORDION_ITEM_CONTEXT',
44
+ });
35
45
  }
36
46
  return ctx;
37
47
  }
@@ -186,9 +196,8 @@ class ForAccordionItem {
186
196
  * When true, this item's trigger ignores clicks and reflects the native
187
197
  * `disabled` attribute (not `aria-disabled`): dropped from the Tab order and
188
198
  * skipped by arrow-key navigation, but kept in the accessibility tree so
189
- * screen readers still announce it. See `ForAccordionTrigger` for the
190
- * rationale (rule #561 D2). Bind via `[disabled]`; read the composed
191
- * {@link disabled} for state.
199
+ * screen readers still announce it. See `ForAccordionTrigger` for the rationale. Bind via
200
+ * `[disabled]`; read the composed {@link disabled} for state.
192
201
  */
193
202
  disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), transform: booleanAttribute, alias: 'disabled' });
194
203
  /**
@@ -264,7 +273,7 @@ class ForAccordionTrigger {
264
273
  /**
265
274
  * APG: aria-disabled is true only when the panel is open AND the accordion
266
275
  * disallows collapse. A real `disabled` item is reflected via the native
267
- * `disabled` attribute instead — the sanctioned exception in rule #561 (D2):
276
+ * `disabled` attribute instead — the sanctioned exception to that rule:
268
277
  * the trigger is a real single-purpose `<button>`, not a roving collection
269
278
  * item (every trigger stays independently in the Tab order; the arrow-key
270
279
  * navigation is an APG-optional enhancement layered on top, not a