forty-cdk 0.6.0 → 0.8.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 (196) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -3
  3. package/accordion/README.md +9 -7
  4. package/aspect-ratio/README.md +19 -0
  5. package/avatar/README.md +3 -3
  6. package/breadcrumbs/README.md +12 -0
  7. package/calendar/README.md +20 -17
  8. package/combobox/README.md +1 -1
  9. package/context-menu/README.md +9 -0
  10. package/date-picker/README.md +25 -67
  11. package/date-range-field/README.md +7 -7
  12. package/dialog/README.md +8 -8
  13. package/drag-drop/README.md +11 -10
  14. package/drawer/README.md +9 -7
  15. package/fesm2022/forty-cdk-accordion.mjs +32 -26
  16. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  17. package/fesm2022/forty-cdk-aspect-ratio.mjs +11 -15
  18. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -1
  19. package/fesm2022/forty-cdk-avatar.mjs +12 -7
  20. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-breadcrumbs.mjs +32 -21
  22. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-button.mjs +253 -20
  25. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-calendar.mjs +110 -77
  27. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-carousel.mjs +13 -2
  29. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-checkbox.mjs +2 -15
  31. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-combobox.mjs +210 -151
  33. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-context-menu.mjs +62 -5
  35. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-core.mjs +1928 -1245
  37. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-date-field.mjs +15 -0
  39. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-date-picker.mjs +51 -79
  41. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-date-range-field.mjs +50 -110
  43. package/fesm2022/forty-cdk-date-range-field.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-dialog.mjs +72 -22
  45. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-disclosure.mjs +2 -15
  47. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-drag-drop.mjs +131 -22
  49. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-drawer.mjs +73 -39
  51. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-field.mjs +53 -45
  53. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  54. package/fesm2022/forty-cdk-fieldset.mjs +2 -15
  55. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  56. package/fesm2022/forty-cdk-file-upload.mjs +73 -32
  57. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-hover-card.mjs +32 -14
  59. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  60. package/fesm2022/forty-cdk-input.mjs +2 -15
  61. package/fesm2022/forty-cdk-input.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-internationalized-date.mjs +10 -8
  63. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-listbox.mjs +124 -4
  65. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-menu.mjs +191 -218
  67. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-menubar.mjs +36 -28
  69. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-meter.mjs +31 -29
  71. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-navigation-menu.mjs +10 -2
  73. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-number-input.mjs +33 -11
  75. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-otp-input.mjs +7 -17
  77. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  78. package/fesm2022/forty-cdk-pagination.mjs +20 -8
  79. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-pane-resizer.mjs +20 -17
  81. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  82. package/fesm2022/forty-cdk-popover.mjs +8 -1
  83. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  84. package/fesm2022/forty-cdk-progress.mjs +25 -14
  85. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  86. package/fesm2022/forty-cdk-radio-group.mjs +34 -19
  87. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  88. package/fesm2022/forty-cdk-scroll-area.mjs +18 -3
  89. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  90. package/fesm2022/forty-cdk-search.mjs +136 -38
  91. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  92. package/fesm2022/forty-cdk-select.mjs +363 -171
  93. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  94. package/fesm2022/forty-cdk-separator.mjs +1 -15
  95. package/fesm2022/forty-cdk-separator.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-slider.mjs +97 -92
  97. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-stepper.mjs +21 -7
  99. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-switch.mjs +2 -15
  101. package/fesm2022/forty-cdk-switch.mjs.map +1 -1
  102. package/fesm2022/forty-cdk-table.mjs +478 -71
  103. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  104. package/fesm2022/forty-cdk-time-field.mjs +15 -0
  105. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  106. package/fesm2022/forty-cdk-time-picker.mjs +61 -129
  107. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  108. package/fesm2022/forty-cdk-time-range-field.mjs +89 -115
  109. package/fesm2022/forty-cdk-time-range-field.mjs.map +1 -1
  110. package/fesm2022/forty-cdk-toast.mjs +24 -22
  111. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  112. package/fesm2022/forty-cdk-toggle.mjs +20 -6
  113. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  114. package/fesm2022/forty-cdk-toolbar.mjs +10 -3
  115. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  116. package/fesm2022/forty-cdk-tooltip.mjs +53 -25
  117. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  118. package/fesm2022/forty-cdk-tree.mjs +145 -35
  119. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  120. package/fesm2022/forty-cdk-virtualization.mjs +54 -27
  121. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  122. package/field/README.md +13 -10
  123. package/file-upload/README.md +5 -0
  124. package/internationalized-date/README.md +23 -23
  125. package/listbox/README.md +1 -1
  126. package/menu/README.md +3 -1
  127. package/meter/README.md +12 -10
  128. package/package.json +1 -1
  129. package/progress/README.md +3 -1
  130. package/radio-group/README.md +14 -12
  131. package/scroll-area/README.md +18 -2
  132. package/search/README.md +34 -15
  133. package/select/README.md +7 -1
  134. package/signal-forms/README.md +1 -1
  135. package/stepper/README.md +15 -2
  136. package/table/README.md +32 -36
  137. package/time-range-field/README.md +23 -18
  138. package/toast/README.md +10 -10
  139. package/toggle/README.md +1 -1
  140. package/toolbar/README.md +4 -4
  141. package/tree/README.md +12 -10
  142. package/types/forty-cdk-accordion.d.ts +31 -28
  143. package/types/forty-cdk-aspect-ratio.d.ts +11 -20
  144. package/types/forty-cdk-avatar.d.ts +2 -2
  145. package/types/forty-cdk-breadcrumbs.d.ts +18 -8
  146. package/types/forty-cdk-button.d.ts +6 -22
  147. package/types/forty-cdk-calendar.d.ts +37 -26
  148. package/types/forty-cdk-carousel.d.ts +1 -0
  149. package/types/forty-cdk-checkbox.d.ts +2 -20
  150. package/types/forty-cdk-combobox.d.ts +120 -35
  151. package/types/forty-cdk-context-menu.d.ts +22 -1
  152. package/types/forty-cdk-core.d.ts +910 -336
  153. package/types/forty-cdk-date-field.d.ts +9 -1
  154. package/types/forty-cdk-date-picker.d.ts +33 -41
  155. package/types/forty-cdk-date-range-field.d.ts +24 -10
  156. package/types/forty-cdk-dialog.d.ts +71 -8
  157. package/types/forty-cdk-disclosure.d.ts +3 -20
  158. package/types/forty-cdk-drag-drop.d.ts +43 -7
  159. package/types/forty-cdk-drawer.d.ts +59 -14
  160. package/types/forty-cdk-dropdown-menu.d.ts +1 -0
  161. package/types/forty-cdk-field.d.ts +21 -33
  162. package/types/forty-cdk-fieldset.d.ts +1 -20
  163. package/types/forty-cdk-file-upload.d.ts +36 -27
  164. package/types/forty-cdk-hover-card.d.ts +11 -1
  165. package/types/forty-cdk-input.d.ts +1 -20
  166. package/types/forty-cdk-internationalized-date.d.ts +9 -6
  167. package/types/forty-cdk-listbox.d.ts +51 -2
  168. package/types/forty-cdk-menu.d.ts +92 -50
  169. package/types/forty-cdk-menubar.d.ts +22 -16
  170. package/types/forty-cdk-meter.d.ts +29 -27
  171. package/types/forty-cdk-navigation-menu.d.ts +9 -3
  172. package/types/forty-cdk-number-input.d.ts +15 -3
  173. package/types/forty-cdk-otp-input.d.ts +6 -23
  174. package/types/forty-cdk-pagination.d.ts +24 -2
  175. package/types/forty-cdk-pane-resizer.d.ts +11 -20
  176. package/types/forty-cdk-popover.d.ts +19 -2
  177. package/types/forty-cdk-progress.d.ts +31 -11
  178. package/types/forty-cdk-radio-group.d.ts +32 -19
  179. package/types/forty-cdk-scroll-area.d.ts +16 -1
  180. package/types/forty-cdk-search.d.ts +137 -34
  181. package/types/forty-cdk-select.d.ts +157 -122
  182. package/types/forty-cdk-separator.d.ts +1 -20
  183. package/types/forty-cdk-slider.d.ts +17 -17
  184. package/types/forty-cdk-stepper.d.ts +27 -4
  185. package/types/forty-cdk-switch.d.ts +1 -20
  186. package/types/forty-cdk-table.d.ts +133 -42
  187. package/types/forty-cdk-tabs.d.ts +1 -0
  188. package/types/forty-cdk-time-field.d.ts +9 -1
  189. package/types/forty-cdk-time-picker.d.ts +47 -76
  190. package/types/forty-cdk-time-range-field.d.ts +53 -15
  191. package/types/forty-cdk-toast.d.ts +3 -3
  192. package/types/forty-cdk-toggle.d.ts +19 -4
  193. package/types/forty-cdk-toolbar.d.ts +2 -0
  194. package/types/forty-cdk-tooltip.d.ts +41 -6
  195. package/types/forty-cdk-tree.d.ts +38 -2
  196. package/types/forty-cdk-virtualization.d.ts +16 -0
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 tutkli
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tutkli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -36,9 +36,9 @@ Optional — install only if you use the matching entry point / primitives:
36
36
 
37
37
  ## Primitives
38
38
 
39
- Each primitive lives under [`src/lib/<primitive>/`](src/lib) with its own `README.md` and a minimal styleless usage example.
39
+ Each primitive lives in its own folder under `projects/forty-cdk/` (e.g. [`accordion/`](accordion), [`dialog/`](dialog)) with its own `README.md` and a minimal styleless usage example.
40
40
 
41
- The library ships one main entry point (`forty-cdk`) plus a single secondary entry point, `forty-cdk/internationalized-date`, which holds the `@internationalized/date` adapters so that optional peer stays truly optional. Standalone directives plus `"sideEffects": false` let tree-shakers drop primitives you don't import.
41
+ Every primitive ships as its own **secondary entry point** — import `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on — backed by the shared `forty-cdk/core` entry point. The `@internationalized/date` adapters live in a dedicated `forty-cdk/internationalized-date` entry point so that optional peer stays truly optional. The main `forty-cdk` barrel is **intentionally empty** (it exports no symbols): always import from the specific `forty-cdk/<primitive>` entry point. Standalone directives plus `"sideEffects": false` mean your bundle only ever includes the primitives you import.
42
42
 
43
43
  ## Directive → host element matrix
44
44
 
@@ -335,7 +335,7 @@ Tests run on Vitest via the Angular CLI builder `@angular/build:unit-test`:
335
335
  ```bash
336
336
  pnpm test # all specs, single pass
337
337
  pnpm exec ng test forty-cdk --watch # watch mode
338
- pnpm exec ng test forty-cdk --include "projects/forty-cdk/src/lib/accordion/accordion.spec.ts" # single file
338
+ pnpm exec ng test forty-cdk --include "../accordion/src/accordion.spec.ts" # single file (path relative to projects/forty-cdk/src/)
339
339
  pnpm exec ng test forty-cdk --filter "Enter and Space select" # tests by name (regex)
340
340
  ```
341
341
 
@@ -56,17 +56,19 @@ export class DemoFaq {
56
56
 
57
57
  ### `ForAccordion`
58
58
 
59
- | Property | Type | Description |
60
- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
- | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
62
- | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
63
- | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
64
- | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
65
- | `dir` | `input<'ltr' \| 'rtl'>` | Writing direction. Only relevant in horizontal mode — swaps the meaning of Left/Right arrows.<br>**Default:** — |
59
+ | Property | Type | Description |
60
+ | ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
+ | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
62
+ | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
63
+ | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
64
+ | `disabled` | `input<boolean>` | When true, disables every item — each trigger reflects the native `disabled` attribute and cannot toggle. Composes with a per-item `[disabled]`.<br>**Default:** `false` |
65
+ | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
66
+ | `dir` | `input<'ltr' \| 'rtl'>` | Writing direction. Only relevant in horizontal mode — swaps the meaning of Left/Right arrows.<br>**Default:** — |
66
67
 
67
68
  | Data attribute | Values |
68
69
  | ------------------ | -------------------------- |
69
70
  | `data-orientation` | `horizontal` \| `vertical` |
71
+ | `data-disabled` | present \| absent |
70
72
 
71
73
  ### `ForAccordionItem`
72
74
 
@@ -4,6 +4,25 @@ A container that keeps its content at a fixed width-to-height ratio.
4
4
 
5
5
  Pure visual utility — it locks an element's box via the native CSS `aspect-ratio` property, with no ARIA semantics. Reach for it to reserve space for media before it loads (preventing layout shift), keep cards on a grid uniform, or wrap responsive iframes.
6
6
 
7
+ ## Why this exists
8
+
9
+ A fixed, never-changing ratio is one line of CSS — you don't need this primitive for that:
10
+
11
+ ```css
12
+ .card-cover {
13
+ aspect-ratio: 16 / 9;
14
+ }
15
+ ```
16
+
17
+ `[forAspectRatio]` earns its place when the ratio is **dynamic or must be validated**. It is more than the static declaration:
18
+
19
+ - **Reactive `ratio` input.** Bind `[ratio]="ratio()"` and the host style recomputes as the value changes — no manual style writes.
20
+ - **Invalid-value guarding.** `0`, negative, and non-finite ratios fall back to `1`, so a bad computed value never emits invalid CSS.
21
+ - **SSR-safe.** The `aspect-ratio` style is bound declaratively (never touched imperatively), so it renders identically on the server and hydrates cleanly.
22
+ - **Consistent headless API.** Same shape as the other primitives, so it composes the same way.
23
+
24
+ If your ratio is a literal constant, prefer the CSS property directly and keep the bundle leaner. Import `[forAspectRatio]` when reactivity or validation buys you something.
25
+
7
26
  ## Anatomy
8
27
 
9
28
  ```html
package/avatar/README.md CHANGED
@@ -78,9 +78,9 @@ export class DemoAvatar {
78
78
 
79
79
  ### `ForAvatarImage`
80
80
 
81
- | Property | Type | Description |
82
- | --------------------- | ------------------------- | ------------------------------------------------------------------- |
83
- | `(loadStatusChanged)` | `output<ForAvatarStatus>` | Output. Emits whenever the lifecycle transitions.<br>**Default:** — |
81
+ | Property | Type | Description |
82
+ | -------------------- | ------------------------- | ------------------------------------------------------------------- |
83
+ | `(loadStatusChange)` | `output<ForAvatarStatus>` | Output. Emits whenever the lifecycle transitions.<br>**Default:** — |
84
84
 
85
85
  | Data attribute | Values |
86
86
  | -------------- | ------------------------------------------ |
@@ -40,6 +40,18 @@ export class DemoBreadcrumbs {}
40
40
 
41
41
  The root defaults its label to `Breadcrumb`. Override it with `ariaLabel="…"` (or point a native `aria-labelledby` at a visible heading) when a page hosts more than one breadcrumb trail.
42
42
 
43
+ ### Localizing the label
44
+
45
+ `Breadcrumb` is verbalized by screen readers, so translate it per injector scope with `provideForBreadcrumbsDefaults`. Configure it at the application root, or in any component's `providers` to scope the translation to a subtree. A per-instance `[ariaLabel]` still wins over the scope default.
46
+
47
+ ```ts
48
+ import { provideForBreadcrumbsDefaults } from 'forty-cdk/breadcrumbs';
49
+
50
+ bootstrapApplication(App, {
51
+ providers: [provideForBreadcrumbsDefaults({ label: 'Ruta de navegación' })],
52
+ });
53
+ ```
54
+
43
55
  ## API
44
56
 
45
57
  ### `ForBreadcrumbs`
@@ -26,6 +26,8 @@ bootstrapApplication(App, {
26
26
 
27
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.
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.
30
+
29
31
  ## Anatomy
30
32
 
31
33
  ```html
@@ -121,21 +123,22 @@ The library is styleless: style the boolean `data-*` hooks on `[forCalendarCell]
121
123
 
122
124
  ### `ForCalendar`
123
125
 
124
- | Property | Type | Description |
125
- | ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
126
- | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
127
- | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
128
- | `range` | `model<CalendarDateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears.<br>**Default:** `null` |
129
- | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
130
- | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
131
- | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
132
- | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable.<br>**Default:** `null` |
133
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable).<br>**Default:** `() => false` |
134
- | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date).<br>**Default:** localized full date, outside-month days suffixed |
135
- | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`.<br>**Default:** — |
136
- | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`.<br>**Default:** — |
137
- | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday).<br>**Default:** `null` → the adapter's value (or `provideForCalendarDefaults`) |
138
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows |
126
+ | Property | Type | Description |
127
+ | ------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
129
+ | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
130
+ | `range` | `model<DateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears.<br>**Default:** `null` |
131
+ | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
132
+ | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
133
+ | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
134
+ | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable.<br>**Default:** `null` |
135
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable).<br>**Default:** `() => false` |
136
+ | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date).<br>**Default:** localized full date, outside-month days suffixed |
137
+ | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`.<br>**Default:** — |
138
+ | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`.<br>**Default:** — |
139
+ | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday).<br>**Default:** `null` → the adapter's value (or `provideForCalendarDefaults`) |
140
+ | `locale` | `input<string \| null>` | BCP 47 locale for the heading, weekday headers, month-picker options and cell `aria-label` names. The calendar system stays Gregorian.<br>**Default:** `null` → the runtime's default locale |
141
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows |
139
142
 
140
143
  ### Data attributes
141
144
 
@@ -161,9 +164,9 @@ Set `selectionMode="range"` and bind `[(range)]` to get date-range selection. In
161
164
  </div>
162
165
  ```
163
166
 
164
- **Interaction model.** Click (or `Enter` / `Space`) a first cell to set the anchor; the grid enters selecting state. Click (or `Enter` / `Space`) a second cell at or after the anchor to commit the range. Clicking before the anchor re-anchors (starts over from the earlier date). No explicit Escape-to-cancel — an in-progress anchor is simply overwritten by the next click.
167
+ **Interaction model.** Click (or `Enter` / `Space`) a first cell to set the anchor; the grid enters selecting state. Click (or `Enter` / `Space`) a second cell **in either direction** to commit the range — clicking before the anchor commits the inverted band `[click, anchor]` (matching the hover preview), it does not start over. There is no separate "start over" gesture and no explicit Escape-to-cancel: once a range is committed, the next click begins a fresh anchor.
165
168
 
166
- **Keyboard in range mode.** `Enter` / `Space` on the focused cell sets the anchor on the first press and commits on the second (same key as single mode). While selecting, arrow / `Home` / `End` / `PageUp` / `PageDown` move the keyboard focus and update the preview end (the keyboard equivalent of pointer hover).
169
+ **Keyboard in range mode.** `Enter` / `Space` on the focused cell sets the anchor on the first press and commits on the second (same key as single mode). While selecting, arrow / `Home` / `End` / `PageUp` / `PageDown` move the keyboard focus and update the preview cursor (the keyboard equivalent of pointer hover); moving before the anchor previews — and commits — the inverted band.
167
170
 
168
171
  **`min` / `max` / `isDateUnavailable`** still gate both endpoints. An unavailable or out-of-bounds date cannot become an anchor or an end.
169
172
 
@@ -22,7 +22,7 @@ The editable (default) anatomy — an `<input>` that filters a portaled listbox
22
22
  ```html
23
23
  <div forCombobox [(query)]="query" [(value)]="value">
24
24
  <input forComboboxInput placeholder="Search…" />
25
- <button forComboboxClear aria-label="Clear">×</button>
25
+ <button forComboboxClear>×</button>
26
26
 
27
27
  <!-- @if (open()) { -->
28
28
  <div forComboboxContent>
@@ -154,4 +154,13 @@ forty-cdk ships no styles. Add your own class to each piece — the for\* select
154
154
  - **Virtual anchor.** Right-click captures a 0×0 rect at the pointer location. `Shift+F10` and `ContextMenu` snapshot the bounding rect of the focused element (or the trigger if focus is on it directly), so the menu floats off the element under attention. Both forms feed floating-ui's `flip` and `shift` middleware, so corners and screen edges work without special-casing.
155
155
  - **Keyboard activators only fire while focus is inside the trigger.** Keyboard events dispatch to the focused element, so `Shift+F10` / `ContextMenu` anywhere outside the trigger goes to the browser default. The trigger is focusable by default (host-bound `tabindex="-1"`), so this works out of the box; use `tabindex="0"` if you want the region itself reachable via Tab.
156
156
  - **Native menu suppressed.** The trigger calls `event.preventDefault()` on `contextmenu` and on the keyboard activators. Set `disabled` to let the browser's native menu surface for that region.
157
+ - **Touch long-press.** The trigger runs its own long-press timer (a `touch` `pointerdown` held ~500 ms, without lifting or moving past a small tolerance, opens the menu at the touch point). This is required because iOS Safari never fires the `contextmenu` event a long-press synthesizes elsewhere; where the browser does synthesize it (Android, desktop touch emulation) the two paths stay mutually exclusive, so the menu opens exactly once. For the press to survive on iOS, suppress the native callout / text-selection on the trigger with CSS — otherwise the OS gesture cancels the press:
158
+
159
+ ```css
160
+ .context-menu-trigger {
161
+ -webkit-touch-callout: none;
162
+ user-select: none;
163
+ }
164
+ ```
165
+
157
166
  - **Mount equals open.** Same convention as the rest of the library — wrap `[forMenuContent]` in `@if (open())` and use `animate.enter` / `animate.leave` for transitions.
@@ -140,26 +140,29 @@ The library is styleless: presence in the DOM is the consumer's job (`@if (open(
140
140
 
141
141
  ### `ForDatePicker`
142
142
 
143
- | Property | Type | Description |
144
- | ------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
145
- | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
146
- | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
147
- | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
148
- | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
149
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
150
- | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured only at `granularity="day"`.<br>**Default:** `true` |
151
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; coarser-than-day off composes a time field.<br>**Default:** `'day'` |
152
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display (and typically the projected `[forTimeField]`).<br>**Default:** `null` → locale |
153
- | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
154
- | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
155
- | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
156
- | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
157
- | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
158
- | `side` / `align` | `input` | Anchored placement (popover mode only).<br>**Default:** `'bottom'` / `'start'` |
159
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
143
+ | Property | Type | Description |
144
+ | ------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
+ | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
146
+ | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
147
+ | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
148
+ | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
149
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
150
+ | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured only at `granularity="day"`.<br>**Default:** `true` |
151
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; coarser-than-day off composes a time field.<br>**Default:** `'day'` |
152
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display (and typically the projected `[forTimeField]`).<br>**Default:** `null` → locale |
153
+ | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
154
+ | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
155
+ | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
156
+ | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
157
+ | `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 |
158
+ | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
159
+ | `side` / `align` | `input` | Anchored placement (popover mode only).<br>**Default:** `'bottom'` / `'start'` |
160
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
160
161
 
161
162
  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`).
162
163
 
164
+ > **`locale` only styles the trigger display.** It drives the text rendered by `[forDatePickerValue]` (both here and on `ForDateRangePicker`); it is **not** forwarded to the projected `ForCalendar`. Bind the calendar's own `[locale]` to localize its heading / weekday / cell labels, exactly as you forward `[min]` / `[max]`.
165
+
163
166
  > **Why `minDate` / `maxDate`, not `min` / `max`?** `ForDatePicker` is a `FormValueControl`, and `FormUiControl` reserves `min` / `max` for numeric validators (`InputSignal<number | undefined>`). A date-typed `min` / `max` would break that contract, so the date bounds use the `*Date` suffix. (`ForCalendar` is not a form control, so it keeps `min` / `max`.)
164
167
 
165
168
  ### Data attributes
@@ -265,63 +268,18 @@ Bind the calendar **and** the time field **one-way** to `picker.value()` (not `[
265
268
 
266
269
  The value display (`[forDatePickerValue]`) automatically appends the time to its formatting when `granularity > 'day'` and you haven't set time fields in `formatOptions`.
267
270
 
268
- ## Range selection
269
-
270
- Set `selectionMode="range"` on both the picker root and the projected calendar and bind `[(range)]` to a `CalendarDateRange<D> | null` signal.
271
-
272
- ```ts
273
- import { type CalendarDateRange } from 'forty-cdk/calendar';
274
-
275
- readonly dateRange = signal<CalendarDateRange<CalendarDate> | null>(null);
276
- ```
277
-
278
- ```html
279
- <div
280
- forDatePicker
281
- selectionMode="range"
282
- [(range)]="dateRange"
283
- [(open)]="open"
284
- [ariaLabel]="'Choose date range'"
285
- >
286
- <button forDatePickerTrigger>
287
- <span forDatePickerValue [placeholder]="'Pick a range'"></span>
288
- </button>
289
-
290
- @if (open()) {
291
- <div forDatePickerContent>
292
- <div forCalendar selectionMode="range" [(range)]="dateRange">
293
- <!-- …header + grid… -->
294
- </div>
295
- </div>
296
- }
297
- </div>
298
- ```
299
-
300
- **`formattedValue` in range mode.** `[forDatePickerValue]` renders `start – end` using the adapter's `format` for each endpoint. The separator defaults to `' – '` and is configurable via `[rangeSeparator]`.
301
-
302
- **`closeOnSelect` in range mode.** The surface closes when a full range is committed (both endpoints set). Clicking the first cell (anchor) keeps the surface open; clicking the second (end) commits and closes. Set `[closeOnSelect]="false"` to keep it open after commit.
303
-
304
- **v1 scope.** Range mode is day-granular only (`granularity` / time is not supported in v1). The `[(range)]` model is not a `FormValueControl` target — it does not integrate with `[formField]` in v1. `minRangeLength` / `maxRangeLength` are configured on the projected `[forCalendar]` directly.
305
-
306
- | New input / model | Type | Description |
307
- | ----------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
308
- | `selectionMode` | `input<'single' \| 'range'>` | `'single'` keeps the existing `value` flow. `'range'` switches to range mode. |
309
- | `range` | `model<CalendarDateRange<D> \| null>` | Two-way bindable committed range. `(rangeChange)` fires only on commit / clear. Default `null`. |
310
- | `rangeSeparator` | `input<string>` | String placed between start and end in the formatted display. Default `' – '`. |
311
-
312
- ## Range as a Signal Forms value — `ForDateRangePicker`
271
+ ## Range selection — `ForDateRangePicker`
313
272
 
314
- `ForDatePicker[selectionMode="range"]` exposes the range through a plain two-way `[(range)]` model with **no** form contract — so a range inside a form has to be hand-wired. `ForDateRangePicker` (selector `[forDateRangePicker]`) is the form-capable sibling: it is the root **and** the form value, implementing `FormValueControl<CalendarDateRange<D> | null>`, so the committed range auto-wires with `[formField]` exactly like any other control.
273
+ 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.
315
274
 
316
275
  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).
317
276
 
318
277
  ```ts
319
- import { type CalendarDateRange } from 'forty-cdk/calendar';
320
- import { ForDateRangePicker } from 'forty-cdk/date-picker';
278
+ import { type DateRange, ForDateRangePicker } from 'forty-cdk/date-picker';
321
279
  import { form } from '@angular/forms/signals';
322
280
 
323
281
  interface Booking {
324
- stay: CalendarDateRange<CalendarDate> | null;
282
+ stay: DateRange<CalendarDate> | null;
325
283
  }
326
284
  readonly model = signal<Booking>({ stay: null });
327
285
  readonly booking = form(this.model, (p) => required(p.stay));
@@ -355,7 +313,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
355
313
  </div>
356
314
  ```
357
315
 
358
- - **Form value.** The committed `CalendarDateRange<D> | null` is the `value` model. `null` is the empty state — pair it with `required(p.stay)` so `invalid()` flips when the form demands a range and none is committed. `touched` fires on commit and on close, exactly like the single-date picker.
316
+ - **Form value.** The committed `DateRange<D> | null` is the `value` model. `null` is the empty state — pair it with `required(p.stay)` so `invalid()` flips when the form demands a range and none is committed. `touched` fires on commit and on close, exactly like the single-date picker.
359
317
  - **Validity.** `start <= end` is guaranteed by construction and is never an error. Forward `minDate` / `maxDate` to the calendar's `[min]` / `[max]`, and `minRangeLength` / `maxRangeLength` to the calendar's `[minRangeLength]` / `[maxRangeLength]` (a too-short / too-long range is rejected as a no-op by the calendar's two-click flow).
360
318
  - **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.
361
319
  - **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.
@@ -4,7 +4,7 @@ A segmented date (and optional time) range input over a pluggable date adapter:
4
4
 
5
5
  Headless, segmented, spin-editable — the keyboard-first, form-capable counterpart to [DateRangePicker](../date-picker/README.md). There is **no single WAI-ARIA APG pattern** for a range field; it is a composition of two labelled `role="group"` endpoints (start / end), each holding a row of spinbutton segments — the same machinery as [DateField](../date-field/README.md) — nested inside one outer `role="group"`. Segment **order** and separators follow the runtime locale (`MM/DD/YYYY` vs `DD.MM.YYYY` vs `YYYY/MM/DD`).
6
6
 
7
- `ForDateRangeField` implements `FormValueControl<CalendarDateRange<D> | null>` from `@angular/forms/signals` — the **same** contract as `ForDateRangePicker` — so the committed range auto-wires with `[formField]` and auto-associates inside a `[forField]` (label / description / error) with no extra markup. The value stays `null` until **both** endpoints are fully entered and ordered (`start <= end`); a half-entered or out-of-order range never reaches the form.
7
+ `ForDateRangeField` implements `FormValueControl<DateRange<D> | null>` from `@angular/forms/signals` — the **same** contract as `ForDateRangePicker` — so the committed range auto-wires with `[formField]` and auto-associates inside a `[forField]` (label / description / error) with no extra markup. The value stays `null` until **both** endpoints are fully entered and ordered (`start <= end`); a half-entered or out-of-order range never reaches the form.
8
8
 
9
9
  ## Date adapter
10
10
 
@@ -43,8 +43,8 @@ Pick one (required). All date math goes through the same pluggable `DateAdapter<
43
43
  ```ts
44
44
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
45
45
  import { CalendarDate } from '@internationalized/date';
46
- import { CalendarDateRange } from 'forty-cdk/calendar';
47
46
  import {
47
+ type DateRange,
48
48
  ForDateRangeField,
49
49
  ForDateRangeFieldEnd,
50
50
  ForDateRangeFieldLiteral,
@@ -91,7 +91,7 @@ import {
91
91
  `,
92
92
  })
93
93
  export class StayField {
94
- readonly stay = signal<CalendarDateRange<CalendarDate> | null>(null);
94
+ readonly stay = signal<DateRange<CalendarDate> | null>(null);
95
95
  }
96
96
  ```
97
97
 
@@ -101,8 +101,8 @@ export class StayField {
101
101
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
102
102
  import { form } from '@angular/forms/signals';
103
103
  import { CalendarDate } from '@internationalized/date';
104
- import { CalendarDateRange } from 'forty-cdk/calendar';
105
104
  import {
105
+ type DateRange,
106
106
  ForDateRangeField,
107
107
  ForDateRangeFieldEnd,
108
108
  ForDateRangeFieldLiteral,
@@ -149,7 +149,7 @@ import {
149
149
  `,
150
150
  })
151
151
  export class StayFormField {
152
- readonly model = signal({ stay: null as CalendarDateRange<CalendarDate> | null });
152
+ readonly model = signal({ stay: null as DateRange<CalendarDate> | null });
153
153
  readonly booking = form(this.model);
154
154
  }
155
155
  ```
@@ -160,7 +160,7 @@ export class StayFormField {
160
160
 
161
161
  | Property | Type | Description |
162
162
  | ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
163
- | `value` | `model<CalendarDateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
163
+ | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
164
164
  | `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` |
165
165
  | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
166
166
  | `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'` |
@@ -193,7 +193,7 @@ Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled
193
193
 
194
194
  ## Ordering
195
195
 
196
- 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 `CalendarDateRange` `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.
196
+ 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.
197
197
 
198
198
  ## Date-time range
199
199
 
package/dialog/README.md CHANGED
@@ -125,7 +125,7 @@ export class DemoHost {
125
125
  ConfirmDialog,
126
126
  { data: { message: 'Are you sure?' } },
127
127
  );
128
- const result = await ref.closed; // 'confirm' | 'cancel' | undefined
128
+ const { result } = await ref.closed; // result: 'confirm' | 'cancel' | undefined
129
129
  if (result === 'confirm') {
130
130
  /* ... */
131
131
  }
@@ -203,7 +203,7 @@ Set them once for a scope with `provideForDialogDefaults({ animateEnter, animate
203
203
  | `returnFocus` | — | Focus returns to the previously focused element on close.<br>**Default:** `true` |
204
204
  | `initialFocus` | — | `'first'` (first focusable inside) or `'container'` (the dialog host).<br>**Default:** `'first'` |
205
205
  | `ariaLabel` | — | Manual `aria-label` if no `[forDialogTitle]` is rendered.<br>**Default:** `null` |
206
- | `close` | `OutputEmitterRef<ForDialogCloseReason>` | Output. Dialog wants to be unmounted. Reasons: `'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`.<br>**Default:** — |
206
+ | `dismiss` | `OutputEmitterRef<ForDialogCloseReason>` | Output. Dialog wants to be unmounted. Reasons: `'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`.<br>**Default:** — |
207
207
  | `escapeKeyDown` | `OutputEmitterRef<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape while this dialog is the topmost dismissable layer.<br>**Default:** — |
208
208
  | `pointerDownOutside` | `OutputEmitterRef<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down outside the dialog.<br>**Default:** — |
209
209
  | `focusOutside` | `OutputEmitterRef<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside the dialog.<br>**Default:** — |
@@ -302,12 +302,12 @@ The dialog still installs the focus trap (so Tab cycles inside once focus enters
302
302
 
303
303
  ## Programmatic API
304
304
 
305
- | Symbol | Description |
306
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
307
- | `ForDialogManager` | Injectable. `open(component, config?)` returns a `ForDialogRef<R>`. |
308
- | `ForDialogRef<R>` | `close(result?)`, `closed: Promise<R \| undefined>`, `result: Signal<R \| undefined>`, `isClosed: Signal<boolean>`. |
309
- | `FOR_DIALOG_DATA` | Token for the `data` payload. Inject in the opened component. |
310
- | `injectDialogData<T>()` | Typed accessor for `FOR_DIALOG_DATA`. Returns `T \| null` — `null` when `open()` got no `data`. |
305
+ | Symbol | Description |
306
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
307
+ | `ForDialogManager` | Injectable. `open(component, config?)` returns a `ForDialogRef<R>`. |
308
+ | `ForDialogRef<R>` | `close(result?)`, `closed: Promise<{ reason: ForDialogCloseReason; result: R \| undefined }>`, `result: Signal<R \| undefined>`, `isClosed: Signal<boolean>`. |
309
+ | `FOR_DIALOG_DATA` | Token for the `data` payload. Inject in the opened component. |
310
+ | `injectDialogData<T>()` | Typed accessor for `FOR_DIALOG_DATA`. Returns `T \| null` — `null` when `open()` got no `data`. |
311
311
 
312
312
  ### `ForDialogOpenConfig`
313
313
 
@@ -7,14 +7,15 @@ whole dialog around by its header — see [`[forFreeDrag]`](#free-drag).
7
7
 
8
8
  ## Keyboard
9
9
 
10
- | State | Key | Action |
11
- | ------ | ----------- | ------------------------------------------ |
12
- | Idle | Arrow keys | Move roving focus between items |
13
- | Idle | Home / End | Jump to first / last item |
14
- | Idle | Space/Enter | **Lift** the focused item |
15
- | Lifted | Arrow keys | Step the logical drop position |
16
- | Lifted | Space/Enter | **Drop** (commits and emits `(dragDrop)`) |
17
- | Lifted | Escape | **Cancel** (no event, focus stays on item) |
10
+ | State | Key | Action |
11
+ | ------ | ----------- | ------------------------------------------------- |
12
+ | Idle | Arrow keys | Move roving focus between items |
13
+ | Idle | Home / End | Jump to first / last item |
14
+ | Idle | Space/Enter | **Lift** the focused item |
15
+ | Lifted | Arrow keys | Step the logical drop position |
16
+ | Lifted | Home / End | Jump the lifted item to the first / last position |
17
+ | Lifted | Space/Enter | **Drop** (commits and emits `(dragDrop)`) |
18
+ | Lifted | Escape | **Cancel** (no event, focus stays on item) |
18
19
 
19
20
  Arrow direction follows the list's `orientation` and respects RTL via `dir`. In
20
21
  `orientation="mixed"` every arrow key steps the lifted item linearly in DOM order.
@@ -347,7 +348,7 @@ for the full analysis.
347
348
  ```
348
349
 
349
350
  ```ts
350
- onDrop(event: ForDragDropEvent<MyItem>): void {
351
+ onDrop(event: ForDragDropEvent): void {
351
352
  this.items.set(
352
353
  moveItemInArray(this.items(), event.previousIndex, event.currentIndex),
353
354
  );
@@ -372,7 +373,7 @@ onDrop(event: ForDragDropEvent<MyItem>): void {
372
373
  ```
373
374
 
374
375
  ```ts
375
- onDrop(event: ForDragDropEvent<MyItem>): void {
376
+ onDrop(event: ForDragDropEvent): void {
376
377
  if (event.previousContainer === event.container) {
377
378
  this.updateList(event.container, (arr) =>
378
379
  moveItemInArray(arr, event.previousIndex, event.currentIndex),
package/drawer/README.md CHANGED
@@ -127,7 +127,7 @@ import {
127
127
  ></div>
128
128
  <div forDrawerHandle aria-hidden="true"></div>
129
129
  <h2 forDrawerTitle>Delete account?</h2>
130
- <p forDrawerDescription>{{ data.message }}</p>
130
+ <p forDrawerDescription>{{ data?.message }}</p>
131
131
  <button forDrawerClose [closeWith]="'cancel'">Cancel</button>
132
132
  <button forDrawerClose [closeWith]="'confirm'">Confirm</button>
133
133
  `,
@@ -150,7 +150,7 @@ class DemoHost {
150
150
  side: 'bottom',
151
151
  snapPoints: ['148px', 1],
152
152
  });
153
- const result = await ref.closed;
153
+ const { result } = await ref.closed;
154
154
  if (result === 'confirm') {
155
155
  // ...
156
156
  }
@@ -158,6 +158,8 @@ class DemoHost {
158
158
  }
159
159
  ```
160
160
 
161
+ `injectDrawerData<T>()` is typed `T | null`: the manager provides `null` when `open()` is called without `data`, so guard (`data?.message`) before dereferencing the payload. `await ref.closed` resolves `{ reason, result }` — the `reason` (a `ForDrawerCloseReason`) tells apart an imperative `close()` (`'programmatic'`) from Escape / backdrop / outside / swipe / close-button dismissals.
162
+
161
163
  Drawers opened by the manager join the same `ForDrawerStack` as declarative ones, so mixed stacking (a programmatic drawer over a declarative parent, or vice versa) reflects correct `data-depth` / `data-state-nested` and routes Escape through the LIFO dismissable layer.
162
164
 
163
165
  **Styling the programmatic overlay root.** The manager creates the `[forDrawer]` host for you and it is class-less. Pass `class` / `classList` to style it — the tokens land on the real host alongside `data-side` / `data-state` / the `--for-drawer-translate` custom property, so positioning CSS keyed on `data-side` works:
@@ -186,22 +188,22 @@ this.#drawers.open(ConfirmDrawer, {
186
188
 
187
189
  `class` is a single or space-separated string; `classList` is an array or space-separated string; both merge and de-dup and never clobber the host attributes. This replaces the old `inject(FOR_DRAWER_CONTEXT).hostElement.classList.add('my-drawer')` workaround.
188
190
 
189
- **Observing drag / release / active snap point.** A snap-point drawer opened imperatively has the same observability as the declarative `(dragMove)` / `(release)` / `(activeSnapPointChange)` outputs via the `onDrag` / `onRelease` / `onActiveSnapPointChange` config callbacks:
191
+ **Observing drag / release / active snap point.** A snap-point drawer opened imperatively has the same observability as the declarative `(dragMove)` / `(release)` / `(activeSnapPointChange)` outputs, via config callbacks of the same name:
190
192
 
191
193
  ```ts
192
194
  this.#drawers.open(ConfirmDrawer, {
193
195
  data,
194
196
  snapPoints: ['148px', '50%', 1],
195
197
  defaultSnapPoint: '148px',
196
- onDrag: ({ percentageDragged }) => this.dragProgress.set(percentageDragged),
197
- onRelease: ({ willClose, nextSnapPoint }) => {
198
+ dragMove: ({ percentageDragged }) => this.dragProgress.set(percentageDragged),
199
+ release: ({ willClose, nextSnapPoint }) => {
198
200
  /* … */
199
201
  },
200
- onActiveSnapPointChange: (snap) => this.activeSnap.set(snap),
202
+ activeSnapPointChange: (snap) => this.activeSnap.set(snap),
201
203
  });
202
204
  ```
203
205
 
204
- `onActiveSnapPointChange` fires with the landed snap on the mount-time default and every drag release — the read-back the declarative API exposes through `[(activeSnapPoint)]`. All three subscriptions are released automatically when the drawer closes.
206
+ `activeSnapPointChange` fires with the landed snap on the mount-time default and every drag release — the read-back the declarative API exposes through `[(activeSnapPoint)]`. All three subscriptions are released automatically when the drawer closes.
205
207
 
206
208
  ### Per-channel dismissal (Escape-only drawers)
207
209