@cdevhub/ngx-tw 0.5.0 → 0.6.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 (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
  49. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  51. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  53. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  54. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  55. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  57. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  59. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  61. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  63. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  65. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  67. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  68. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  69. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  71. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  73. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  75. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  77. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  79. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  81. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  83. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  84. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  85. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  86. package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
  87. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  88. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
  91. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  93. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  95. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  97. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  99. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  101. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  103. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  105. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  107. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  109. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  111. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  112. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  113. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  115. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  117. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  119. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  120. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  121. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  122. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  123. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  124. package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
  125. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  126. package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
  127. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  128. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  129. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  131. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  132. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  133. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  134. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  135. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  136. package/index.json +1 -1
  137. package/package.json +63 -2
  138. package/theme/_base.css +13 -0
  139. package/theme/_dark.css +110 -36
  140. package/theme/_high-contrast-dark.css +367 -0
  141. package/theme/_high-contrast.css +13 -2
  142. package/theme/_light.css +276 -0
  143. package/theme/_semantic.css +47 -12
  144. package/theme/_typography.css +17 -0
  145. package/theme/index.css +86 -7
  146. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  147. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  148. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  149. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  150. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  151. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  152. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  153. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  154. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  155. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  156. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  157. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  158. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  159. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  160. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  161. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  162. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  163. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  164. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  165. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  166. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  167. package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
  168. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  169. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  170. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  171. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  172. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  173. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  174. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  175. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  176. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  177. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  178. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  179. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  180. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  181. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  182. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  183. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  184. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  185. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  186. package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
  187. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  188. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  189. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  190. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  191. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  192. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  193. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  194. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  195. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  196. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  197. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  198. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  199. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  200. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  201. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  202. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  203. package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
  204. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  205. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  206. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  207. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
  208. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
  209. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs.map +0 -1
@@ -1,12 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ElementRef, signal, Directive, viewChild, viewChildren, computed, afterRenderEffect, ChangeDetectionStrategy, Component, input, booleanAttribute, model, output, ViewContainerRef, Injector, DestroyRef, linkedSignal, effect, untracked, afterNextRender, isDevMode, forwardRef } from '@angular/core';
3
- import { NgForm, FormGroupDirective, Validators, NgControl, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
3
+ import { NgControl, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import { Overlay } from '@angular/cdk/overlay';
5
5
  import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
6
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
- import { merge } from 'rxjs';
8
6
  import { tv } from 'tailwind-variants';
9
- import { PickerOverlayCoordinator, TW_ERROR_STATE_MATCHER, resolveSelectScrollStrategy, buildSelectLikePositions } from '@cdevhub/ngx-tw/core';
7
+ import { PickerOverlayCoordinator, wireErrorState, resolveSelectScrollStrategy, buildSelectLikePositions } from '@cdevhub/ngx-tw/core';
10
8
  import { FormFieldControl, TW_FORM_FIELD, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
11
9
  import { TwDateRange, CalendarComponent, provideRangeSelectionStrategy, DATE_ADAPTER, calendarValidator } from '@cdevhub/ngx-tw/calendar';
12
10
  import { ButtonDirective } from '@cdevhub/ngx-tw/button';
@@ -74,6 +72,9 @@ class DateRangePickerOverlayComponent {
74
72
  /** @internal */
75
73
  startView = signal('day', /* @ts-ignore */
76
74
  ...(ngDevMode ? [{ debugName: "startView" }] : /* istanbul ignore next */ []));
75
+ /** @internal Mirrors the host's `startAt`. Anchors the calendar when no range is pending. */
76
+ startAt = signal(null, /* @ts-ignore */
77
+ ...(ngDevMode ? [{ debugName: "startAt" }] : /* istanbul ignore next */ []));
77
78
  /** @internal */
78
79
  numberOfMonths = signal(2, /* @ts-ignore */
79
80
  ...(ngDevMode ? [{ debugName: "numberOfMonths" }] : /* istanbul ignore next */ []));
@@ -213,10 +214,18 @@ class DateRangePickerOverlayComponent {
213
214
  return v ? { start: v.start, end: v.end } : { start: null, end: null };
214
215
  }, /* @ts-ignore */
215
216
  ...(ngDevMode ? [{ debugName: "pendingRangeAsRange" }] : /* istanbul ignore next */ []));
216
- /** @internal Seeds the calendar's active month on first attach so it anchors to the current value rather than today. */
217
+ /**
218
+ * @internal Seeds the calendar's active month on first attach so it anchors to
219
+ * the current value rather than today.
220
+ *
221
+ * A pending range wins: once the user has a selection, the calendar should
222
+ * follow it. `startAt` is the fallback for an empty picker, which is the case
223
+ * the host input exists for — it was declared but never forwarded here, so a
224
+ * consumer setting it got silence and the calendar always opened on today.
225
+ */
217
226
  initialStartAt = computed(() => {
218
227
  const v = this.pendingRange();
219
- return v?.start ?? null;
228
+ return v?.start ?? this.startAt();
220
229
  }, /* @ts-ignore */
221
230
  ...(ngDevMode ? [{ debugName: "initialStartAt" }] : /* istanbul ignore next */ []));
222
231
  // ── Template handlers ──
@@ -461,7 +470,7 @@ class DateRangePickerOverlayComponent {
461
470
  </div>
462
471
  }
463
472
  </div>
464
- `, isInline: true, dependencies: [{ kind: "component", type: CalendarComponent, selector: "tw-calendar", inputs: ["mode", "value", "startAt", "minDate", "maxDate", "dateFilter", "disabledDates", "disabledDaysOfWeek", "constraints", "minRangeLength", "maxRangeLength", "maxSelections", "maxSelectionBehavior", "errorAriaDescribedBy", "rangeClickBehavior", "rangeBehavior", "dateClass", "bordered", "startView", "firstDayOfWeek", "monthColumns", "disabled", "readonly", "resetBehavior", "cellTemplate", "locale", "intl"], outputs: ["valueChange", "selectionStart", "rangePreview", "selectionComplete", "selectionRestart", "selectionCleared", "selectionLimitReached", "presetChange", "viewChange", "activeDateChange", "monthChange", "yearChange", "cellClick", "cellHover", "modeChange"] }, { kind: "component", type: TimePickerComponent, selector: "tw-time-picker", inputs: ["id", "disabled", "required", "readonly", "size", "color", "variant", "format", "showSeconds", "hourStep", "minuteStep", "secondStep", "minTime", "maxTime", "referenceDate", "placeholder", "showSteppers", "showClear", "clearLabel", "errorStateMatcher", "aria-label", "aria-labelledby", "aria-describedby", "value"], outputs: ["valueChange", "timeInput", "timeChange"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "directive", type: DateRangePresetOptionDirective, selector: "button[twDateRangePresetOption]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
473
+ `, isInline: true, dependencies: [{ kind: "component", type: CalendarComponent, selector: "tw-calendar", inputs: ["mode", "value", "startAt", "minDate", "maxDate", "dateFilter", "disabledDates", "disabledDaysOfWeek", "constraints", "minRangeLength", "maxRangeLength", "maxSelections", "maxSelectionBehavior", "errorAriaDescribedBy", "aria-describedby", "rangeClickBehavior", "rangeBehavior", "dateClass", "bordered", "startView", "firstDayOfWeek", "monthColumns", "disabled", "readonly", "resetBehavior", "cellTemplate", "locale", "intl"], outputs: ["valueChange", "selectionStart", "rangePreview", "selectionComplete", "selectionRestart", "selectionCleared", "selectionLimitReached", "presetChange", "viewChange", "activeDateChange", "monthChange", "yearChange", "cellClick", "cellHover", "modeChange"] }, { kind: "component", type: TimePickerComponent, selector: "tw-time-picker", inputs: ["id", "disabled", "required", "readonly", "size", "color", "variant", "format", "showSeconds", "hourStep", "minuteStep", "secondStep", "minTime", "maxTime", "referenceDate", "placeholder", "showSteppers", "showClear", "clearLabel", "errorStateMatcher", "aria-label", "aria-labelledby", "aria-describedby", "value"], outputs: ["valueChange", "timeInput", "timeChange"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "directive", type: DateRangePresetOptionDirective, selector: "button[twDateRangePresetOption]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
465
474
  }
466
475
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: DateRangePickerOverlayComponent, decorators: [{
467
476
  type: Component,
@@ -626,7 +635,11 @@ function rangesEqual(a, b, adapter) {
626
635
  const dateRangePickerVariants = tv({
627
636
  slots: {
628
637
  root: 'relative inline-flex items-center w-full text-fg transition-[color,border-color,box-shadow] duration-normal motion-reduce:transition-none',
629
- trigger: 'flex-1 inline-flex items-center gap-2 min-w-0 bg-transparent text-left text-fg outline-none border-0 p-0 m-0 cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:cursor-not-allowed',
638
+ // `self-stretch` so the trigger the component's only interactive surface
639
+ // fills the pinned control height. Without it the button is exactly its line
640
+ // box tall and everything above/below it inside the shell is dead click area
641
+ // (up to 22px at xl).
642
+ trigger: 'flex-1 inline-flex items-center gap-2 min-w-0 self-stretch bg-transparent text-left text-fg outline-none border-0 p-0 m-0 cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:cursor-not-allowed',
630
643
  startText: 'truncate',
631
644
  separator: 'shrink-0 text-fg-subtle',
632
645
  endText: 'truncate',
@@ -634,6 +647,11 @@ const dateRangePickerVariants = tv({
634
647
  triggerIconWrapper: 'inline-flex items-center justify-center shrink-0 text-fg-muted',
635
648
  triggerIcon: 'shrink-0 text-fg-muted transition-colors duration-normal motion-reduce:transition-none',
636
649
  // size-6 (24×24 CSS px) is the WCAG AA minimum interactive target — bumping past size-5 keeps the affordance hittable without growing the inline row.
650
+ // Held flat across every size. It fits the 30/34/42/46px content boxes at sm..xl. At xs the
651
+ // pinned root is h-6 (24px border-box) so its content box is only 22px: the button is kept at
652
+ // 24×24 anyway — docs/vertical-rhythm.md calls 24px a hard floor — and therefore coincides with
653
+ // the root's *border*-box, overlapping the 1px border row top and bottom. The root's height is
654
+ // unaffected (h-6 is a definite height); only the hover fill meets the border at that density.
637
655
  clearButton: 'inline-flex items-center justify-center shrink-0 rounded-md text-fg-muted hover:text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none size-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
638
656
  },
639
657
  variants: {
@@ -663,7 +681,10 @@ const dateRangePickerVariants = tv({
663
681
  },
664
682
  variant: {
665
683
  default: {
666
- root: 'rounded-md border border-border bg-surface px-3 py-2 hover:border-border-strong',
684
+ // No `py-*`: the height is pinned per size in compoundVariants below.
685
+ // See docs/vertical-rhythm.md §3 — padding and a pinned height fight,
686
+ // and under border-box the taller one silently wins.
687
+ root: 'rounded-md border border-border bg-surface px-3 hover:border-border-strong',
667
688
  },
668
689
  naked: {
669
690
  root: 'bg-transparent border-0 rounded-none p-0 focus-within:outline-none',
@@ -688,6 +709,16 @@ const dateRangePickerVariants = tv({
688
709
  },
689
710
  },
690
711
  compoundVariants: [
712
+ // ── Pinned control height (docs/vertical-rhythm.md §1) ──
713
+ // Only the `default` variant carries a height. `naked` means somebody else
714
+ // owns the box — `<tw-form-field>` auto-nakeds this picker and supplies its
715
+ // own bordered, padded controlWrapper, so pinning naked would stack 36px on
716
+ // top of the form-field's `py-2` and inflate a wrapped picker at every size.
717
+ { variant: 'default', size: 'xs', class: { root: 'h-6' } },
718
+ { variant: 'default', size: 'sm', class: { root: 'h-8' } },
719
+ { variant: 'default', size: 'md', class: { root: 'h-9' } },
720
+ { variant: 'default', size: 'lg', class: { root: 'h-11' } },
721
+ { variant: 'default', size: 'xl', class: { root: 'h-12' } },
691
722
  { variant: 'default', focused: true, color: 'primary', class: { root: 'border-primary-500' } },
692
723
  { variant: 'default', focused: true, color: 'secondary', class: { root: 'border-secondary-500' } },
693
724
  { variant: 'default', focused: true, color: 'accent', class: { root: 'border-accent-500' } },
@@ -732,13 +763,13 @@ class DateRangePickerComponent extends FormFieldControl {
732
763
  /** Latest selectable date for either endpoint. Presets and calendar cells later than this are rejected or disabled. Defaults to `null`. */
733
764
  maxDate = input(null, /* @ts-ignore */
734
765
  ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
735
- /** Per-date predicate — return `false` to disable. Applied in both calendars. Presets that fall on a filtered date are skipped. */
766
+ /** Per-date predicate — return `false` to disable. Applied in both calendars. Presets that fall on a filtered date are skipped. Defaults to `null`. */
736
767
  dateFilter = input(null, /* @ts-ignore */
737
768
  ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
738
769
  /** Which calendar view opens first — `'day'`, `'month'`, or `'year'`. Defaults to `'day'`. */
739
770
  startView = input('day', /* @ts-ignore */
740
771
  ...(ngDevMode ? [{ debugName: "startView" }] : /* istanbul ignore next */ []));
741
- /** Date the left calendar focuses on when opened with no value. Falls back to today. Ignored when a value is already set. */
772
+ /** Date the left calendar focuses on when opened with no value. Falls back to today. Ignored when a value is already set. Defaults to `null`. */
742
773
  startAt = input(null, /* @ts-ignore */
743
774
  ...(ngDevMode ? [{ debugName: "startAt" }] : /* istanbul ignore next */ []));
744
775
  /** Display format for each endpoint, passed to `DateAdapter.format()`. When `showTime` is true and this is left at the default, hour/minute (and optional seconds) are folded in automatically. */
@@ -747,13 +778,13 @@ class DateRangePickerComponent extends FormFieldControl {
747
778
  /** Separator rendered between the two formatted endpoints in the trigger. Defaults to `" – "`. */
748
779
  rangeSeparator = input(' – ', /* @ts-ignore */
749
780
  ...(ngDevMode ? [{ debugName: "rangeSeparator" }] : /* istanbul ignore next */ []));
750
- /** Placeholder text shown in the trigger for an empty `start` endpoint. */
781
+ /** Placeholder text shown in the trigger for an empty `start` endpoint. Defaults to `'Start date'`. */
751
782
  emptyStartLabel = input('Start date', /* @ts-ignore */
752
783
  ...(ngDevMode ? [{ debugName: "emptyStartLabel" }] : /* istanbul ignore next */ []));
753
- /** Placeholder text shown in the trigger for an empty `end` endpoint. */
784
+ /** Placeholder text shown in the trigger for an empty `end` endpoint. Defaults to `'End date'`. */
754
785
  emptyEndLabel = input('End date', /* @ts-ignore */
755
786
  ...(ngDevMode ? [{ debugName: "emptyEndLabel" }] : /* istanbul ignore next */ []));
756
- /** When set, overrides the composed `${emptyStartLabel}${rangeSeparator}${emptyEndLabel}` placeholder with a single string. */
787
+ /** When set, overrides the composed `${emptyStartLabel}${rangeSeparator}${emptyEndLabel}` placeholder with a single string. Defaults to `undefined`. */
757
788
  placeholder = input(undefined, /* @ts-ignore */
758
789
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
759
790
  /** When true, the trigger cannot open the overlay and `aria-disabled="true"` is set. Defaults to `false`. */
@@ -774,11 +805,10 @@ class DateRangePickerComponent extends FormFieldControl {
774
805
  /** How many months the overlay shows side-by-side. `2` is the standard range-picker layout; use `1` for compact contexts. Defaults to `2`. */
775
806
  numberOfMonths = input(2, /* @ts-ignore */
776
807
  ...(ngDevMode ? [{ debugName: "numberOfMonths" }] : /* istanbul ignore next */ []));
777
- /** Optional quick-select presets rendered as a vertical list before the calendars. Each preset provides a label and a factory returning a `TwDateRange<D>`. An empty array hides the preset panel. */
808
+ /** Optional quick-select presets rendered as a vertical list before the calendars. Each preset provides a label and a factory returning a `TwDateRange<D>`. An empty array hides the preset panel. Defaults to an empty array. */
778
809
  presets = input([], /* @ts-ignore */
779
810
  ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
780
- /** Whether to show a clear-button affordance inside the trigger when a value is set. Defaults to `true`. */
781
- // TRUE-default: a picker without a clear affordance forces consumers to wire one — most form pickers expect the inline clear, matching `<tw-input>`'s clear button.
811
+ /** Whether to show a clear-button affordance inside the trigger when a value is set. Defaults to `true` — most form pickers expect the inline clear, matching `<tw-input>`'s clear button; without it consumers must wire one themselves. */
782
812
  showClear = input(true, /* @ts-ignore */
783
813
  ...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
784
814
  /** Minimum range length in days, inclusive. Commits shorter than this are rejected and surface `calendarRangeTooShort` on the bound `NgControl`. `null` = no minimum. Defaults to `null`. */
@@ -800,13 +830,13 @@ class DateRangePickerComponent extends FormFieldControl {
800
830
  /** Override first day of week (0=Sun, 1=Mon) on the embedded calendar. Falls back to the adapter's default. */
801
831
  firstDayOfWeek = input(null, /* @ts-ignore */
802
832
  ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
803
- /** Per-instance locale override. Forwarded to the embedded calendar and the underlying `DateAdapter` so the trigger display tracks the picker's locale. Falls back to Angular `LOCALE_ID` when `null`. */
833
+ /** Per-instance locale override. Forwarded to the embedded calendar and the underlying `DateAdapter` so the trigger display tracks the picker's locale. Falls back to Angular `LOCALE_ID` when `null`. Defaults to `null`. */
804
834
  locale = input(null, /* @ts-ignore */
805
835
  ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
806
- /** Function producing per-cell CSS classes on the embedded calendar. */
836
+ /** Function producing per-cell CSS classes on the embedded calendar. Defaults to `null`. */
807
837
  dateClass = input(null, /* @ts-ignore */
808
838
  ...(ngDevMode ? [{ debugName: "dateClass" }] : /* istanbul ignore next */ []));
809
- /** Optional cell-content template, forwarded to the embedded calendar. Use to customize cell visuals beyond `dateClass`. */
839
+ /** Optional cell-content template, forwarded to the embedded calendar. Use to customize cell visuals beyond `dateClass`. Defaults to `null`. */
810
840
  cellTemplate = input(null, /* @ts-ignore */
811
841
  ...(ngDevMode ? [{ debugName: "cellTemplate" }] : /* istanbul ignore next */ []));
812
842
  /** When true, renders a `Today / Clear / Cancel / Apply` action bar at the bottom of the overlay. The calendar commits on the second click by default — turn this on for touch-heavy contexts. Defaults to `false`. */
@@ -830,19 +860,19 @@ class DateRangePickerComponent extends FormFieldControl {
830
860
  /** Step for the embedded time-pickers' second fields. Defaults to `1`. */
831
861
  secondStep = input(1, /* @ts-ignore */
832
862
  ...(ngDevMode ? [{ debugName: "secondStep" }] : /* istanbul ignore next */ []));
833
- /** Label for the `Today` action in the overlay's action bar. */
863
+ /** Label for the `Today` action in the overlay's action bar. Defaults to `'Today'`. */
834
864
  todayLabel = input('Today', /* @ts-ignore */
835
865
  ...(ngDevMode ? [{ debugName: "todayLabel" }] : /* istanbul ignore next */ []));
836
- /** Label for the `Clear` action in the overlay's action bar. */
866
+ /** Label for the `Clear` action in the overlay's action bar. Defaults to `'Clear'`. */
837
867
  clearLabel = input('Clear', /* @ts-ignore */
838
868
  ...(ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []));
839
- /** Label for the `Cancel` action in the overlay's action bar. */
869
+ /** Label for the `Cancel` action in the overlay's action bar. Defaults to `'Cancel'`. */
840
870
  cancelLabel = input('Cancel', /* @ts-ignore */
841
871
  ...(ngDevMode ? [{ debugName: "cancelLabel" }] : /* istanbul ignore next */ []));
842
- /** Label for the `Apply` action in the overlay's action bar. */
872
+ /** Label for the `Apply` action in the overlay's action bar. Defaults to `'Apply'`. */
843
873
  applyLabel = input('Apply', /* @ts-ignore */
844
874
  ...(ngDevMode ? [{ debugName: "applyLabel" }] : /* istanbul ignore next */ []));
845
- /** Extra class(es) applied to the overlay panel element. `twMerge` resolves conflicts with internal classes. */
875
+ /** Extra class(es) applied to the overlay panel element. `twMerge` resolves conflicts with internal classes. Defaults to an empty string. */
846
876
  panelClass = input('', /* @ts-ignore */
847
877
  ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
848
878
  /** CDK scroll strategy for the overlay. Defaults to `'reposition'`. */
@@ -854,20 +884,20 @@ class DateRangePickerComponent extends FormFieldControl {
854
884
  /** Accessible label for the clear button. Defaults to `'Clear date range'`. */
855
885
  clearAriaLabel = input('Clear date range', /* @ts-ignore */
856
886
  ...(ngDevMode ? [{ debugName: "clearAriaLabel" }] : /* istanbul ignore next */ []));
857
- /** Per-instance override of the `ErrorStateMatcher`. When omitted, uses the injected `TW_ERROR_STATE_MATCHER`. */
887
+ /** Per-instance override of the `ErrorStateMatcher`. When omitted, uses the injected `TW_ERROR_STATE_MATCHER`. Defaults to `undefined`. */
858
888
  errorStateMatcher = input(undefined, /* @ts-ignore */
859
889
  ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
860
- /** Accessible name for the trigger. Required when no visible label is supplied via `tw-form-field` or an external `aria-labelledby`. Alias: `aria-label`. */
890
+ /** Accessible name for the trigger. Required when no visible label is supplied via `tw-form-field` or an external `aria-labelledby`. Alias: `aria-label`. Defaults to `undefined`. */
861
891
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
862
- /** ID of an external element that labels the trigger. Alias: `aria-labelledby`. */
892
+ /** ID of an external element that labels the trigger. Alias: `aria-labelledby`. Defaults to `undefined`. */
863
893
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
864
- /** Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`. */
894
+ /** Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`. Defaults to `undefined`. */
865
895
  userAriaDescribedByInput = input(undefined, { ...(ngDevMode ? { debugName: "userAriaDescribedByInput" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
866
896
  // ── Models (two-way) ──
867
- /** Two-way bound selected range. `null` when no selection. Setting programmatically updates the trigger display and the calendar selection; it does NOT trigger `onChange`. */
897
+ /** Two-way bound selected range. `null` when no selection. Setting programmatically updates the trigger display and the calendar selection; it does NOT trigger `onChange`. Defaults to `null`. */
868
898
  value = model(null, /* @ts-ignore */
869
899
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
870
- /** Two-way bound open state of the overlay. Setting to `true` opens; setting to `false` closes. */
900
+ /** Two-way bound open state of the overlay. Setting to `true` opens; setting to `false` closes. Defaults to `false`. */
871
901
  open = model(false, /* @ts-ignore */
872
902
  ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
873
903
  // ── Outputs ──
@@ -895,9 +925,6 @@ class DateRangePickerComponent extends FormFieldControl {
895
925
  // constructor pulls the validator set — eager `inject(NgControl, {self})`
896
926
  // would resolve to this component while it is still being created).
897
927
  ngControl = null;
898
- parentForm = inject(NgForm, { optional: true });
899
- parentFormGroup = inject(FormGroupDirective, { optional: true });
900
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
901
928
  // ── View refs ──
902
929
  triggerRef = viewChild.required('trigger', /* @ts-ignore */
903
930
  ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
@@ -924,29 +951,41 @@ class DateRangePickerComponent extends FormFieldControl {
924
951
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
925
952
  describedByIdsSignal = signal([], /* @ts-ignore */
926
953
  ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
927
- closingSignal = signal(false, /* @ts-ignore */
928
- ...(ngDevMode ? [{ debugName: "closingSignal" }] : /* istanbul ignore next */ []));
929
954
  lastValueBeforeOpen = signal(null, /* @ts-ignore */
930
955
  ...(ngDevMode ? [{ debugName: "lastValueBeforeOpen" }] : /* istanbul ignore next */ []));
931
956
  pendingRange = signal(null, /* @ts-ignore */
932
957
  ...(ngDevMode ? [{ debugName: "pendingRange" }] : /* istanbul ignore next */ []));
933
958
  currentView = signal('day', /* @ts-ignore */
934
959
  ...(ngDevMode ? [{ debugName: "currentView" }] : /* istanbul ignore next */ []));
935
- _ngControlRev = signal(0, /* @ts-ignore */
936
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
937
- _formSubmitRev = signal(0, /* @ts-ignore */
938
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
960
+ /** @internal Shared `errorState` / `required` / `errors` derivation — see `wireErrorState`. */
961
+ errorWiring = wireErrorState({
962
+ ngControl: () => this.ngControl,
963
+ matcher: () => this.errorStateMatcher(),
964
+ required: () => this.requiredInput(),
965
+ track: [() => this.focusedSignal()],
966
+ // An unparseable entry or an out-of-range date is an error regardless of validator status.
967
+ errorStateOverride: () => (this.parseError() || this.rangeError() ? true : undefined),
968
+ });
939
969
  onChange = () => { };
940
970
  onTouched = () => { };
941
- overlayInstanceSignal = signal(null, /* @ts-ignore */
942
- ...(ngDevMode ? [{ debugName: "overlayInstanceSignal" }] : /* istanbul ignore next */ []));
943
- get overlayInstance() {
944
- return this.overlayInstanceSignal();
945
- }
971
+ // Overlay lifecycle bookkeeping. Both are PLAIN FIELDS, not signals, and that
972
+ // is load-bearing: the lifecycle effect in the constructor both reads and
973
+ // writes them, so as signals they would form a read → write → re-trigger
974
+ // cycle of exactly the shape CLAUDE.md forbids. Demoting them is the fix
975
+ // `popover.ts` and `command-palette.ts` already use — see the comment on that
976
+ // effect. Do not promote either back to `signal()`.
977
+ overlayInstance = null;
978
+ closing = false;
979
+ // The one piece of overlay state that must stay a signal: it is the *only*
980
+ // dependency that re-runs the overlay state-push effect when the panel
981
+ // attaches. Written in openOverlay / closeOverlay, never read by the
982
+ // lifecycle effect, so it cannot cycle. Mirrors `command-palette.ts`.
983
+ isAttached = signal(false, /* @ts-ignore */
984
+ ...(ngDevMode ? [{ debugName: "isAttached" }] : /* istanbul ignore next */ []));
946
985
  // ── Derived state ──
947
986
  /** @internal */
948
987
  isDisabled = computed(() => {
949
- this._ngControlRev();
988
+ this.errorWiring.rev();
950
989
  return this.disabledInput() || this.cvaDisabled() || !!this.ngControl?.disabled;
951
990
  }, /* @ts-ignore */
952
991
  ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
@@ -1106,26 +1145,19 @@ class DateRangePickerComponent extends FormFieldControl {
1106
1145
  /** @internal */
1107
1146
  disabled = this.isDisabled;
1108
1147
  /** @internal */
1109
- required = computed(() => {
1110
- this._ngControlRev();
1111
- if (this.requiredInput())
1112
- return true;
1113
- return !!this.ngControl?.control?.hasValidator(Validators.required);
1114
- }, /* @ts-ignore */
1115
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
1116
- /** @internal */
1117
- errorState = computed(() => {
1118
- this._ngControlRev();
1119
- this._formSubmitRev();
1120
- this.focusedSignal();
1121
- if (this.parseError() || this.rangeError())
1122
- return true;
1123
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
1124
- const form = this.parentFormGroup ??
1125
- this.parentForm;
1126
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
1127
- }, /* @ts-ignore */
1128
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
1148
+ required = this.errorWiring.required;
1149
+ /** @internal */
1150
+ errorState = this.errorWiring.errorState;
1151
+ /**
1152
+ * @internal Active validation errors map from the bound `NgControl` (or `null` when the
1153
+ * control reports none / is unbound). This is what `[twError match="…"]` filters on inside a
1154
+ * `<tw-form-field>`; without it every `match`-targeted message including the
1155
+ * `calendarMinDate` / `calendarMaxDate` / `calendarInvalidRange` / `calendarRangeTooShort` /
1156
+ * `calendarRangeTooLong` codes this component's `NG_VALIDATORS` apparatus exists to produce —
1157
+ * stays permanently hidden. (Authoritative key list: `CalendarValidationErrors` in
1158
+ * `calendar.types.ts`.)
1159
+ */
1160
+ errors = this.errorWiring.errors;
1129
1161
  /** @internal */
1130
1162
  controlType = 'date-range-picker';
1131
1163
  /** @internal */
@@ -1135,24 +1167,54 @@ class DateRangePickerComponent extends FormFieldControl {
1135
1167
  constructor() {
1136
1168
  super();
1137
1169
  // Mirror the `open` model into the overlay lifecycle.
1170
+ //
1171
+ // The only tracked reads here are `open()` and `isDisabled()`. Everything
1172
+ // this effect *writes* — `overlayInstance`, `closing` — is a plain field,
1173
+ // never a signal, so opening or closing cannot re-trigger the effect that
1174
+ // ordered it. That is the whole reason those two are declared as fields
1175
+ // (see their declaration); promoting either back to `signal()` recreates
1176
+ // the read → write → re-trigger cycle CLAUDE.md forbids.
1177
+ //
1178
+ // `openOverlay()` / `closeOverlay()` do write `isAttached`, but this effect
1179
+ // never reads it, so that write is a one-way hand-off to the state-push
1180
+ // effect below and terminates there.
1181
+ //
1182
+ // The body is wrapped in `untracked()` because demoting the two fields is
1183
+ // NOT sufficient on its own. `openOverlay()` reads roughly twenty-five
1184
+ // inputs while pushing them into the freshly attached panel, and it writes
1185
+ // two signals it then reads straight back. Called from the tracked phase,
1186
+ // every one of those reads becomes a dependency of THIS effect, which both
1187
+ // recreates the forbidden write-then-read-back shape and re-runs the whole
1188
+ // lifecycle on any of those twenty-five inputs changing. Tracked reads are
1189
+ // now exactly `open()` and `isDisabled()`, which is what the paragraph
1190
+ // above claims — and only now true.
1138
1191
  effect(() => {
1139
1192
  const shouldOpen = this.open();
1140
1193
  const disabled = this.isDisabled();
1141
- if (disabled && this.overlayInstance) {
1142
- this.closeOverlay('programmatic');
1143
- return;
1144
- }
1145
- if (shouldOpen && !this.overlayInstance && !disabled && !this.closingSignal()) {
1146
- this.openOverlay();
1147
- }
1148
- else if (!shouldOpen && this.overlayInstance && !this.closingSignal()) {
1149
- this.closeOverlay('programmatic');
1150
- }
1194
+ untracked(() => {
1195
+ if (disabled && this.overlayInstance) {
1196
+ this.closeOverlay('programmatic');
1197
+ return;
1198
+ }
1199
+ if (shouldOpen && !this.overlayInstance && !disabled && !this.closing) {
1200
+ this.openOverlay();
1201
+ }
1202
+ else if (!shouldOpen && this.overlayInstance && !this.closing) {
1203
+ this.closeOverlay('programmatic');
1204
+ }
1205
+ });
1151
1206
  });
1152
1207
  // Push config into the overlay whenever anything relevant changes.
1153
1208
  // Reads happen in the tracked phase; writes to the overlay instance are
1154
1209
  // wrapped in `untracked()` so they never feed back into this effect.
1210
+ //
1211
+ // `isAttached()` MUST be read first and unconditionally: it is this
1212
+ // effect's only dependency on the overlay's existence, so it is what makes
1213
+ // the initial push happen when the panel attaches (`overlayInstance` is a
1214
+ // plain field and is invisible to the signal graph). Do not reorder it
1215
+ // behind the null check — the effect would then never re-run on open.
1155
1216
  effect(() => {
1217
+ this.isAttached();
1156
1218
  const instance = this.overlayInstance;
1157
1219
  if (!instance)
1158
1220
  return;
@@ -1162,6 +1224,7 @@ class DateRangePickerComponent extends FormFieldControl {
1162
1224
  const maxDate = this.maxDate();
1163
1225
  const dateFilter = this.dateFilter();
1164
1226
  const startView = this.startView();
1227
+ const startAt = this.startAt();
1165
1228
  const numberOfMonths = this.numberOfMonths();
1166
1229
  const pendingRange = this.pendingRange();
1167
1230
  const currentView = this.currentView();
@@ -1196,6 +1259,7 @@ class DateRangePickerComponent extends FormFieldControl {
1196
1259
  instance.maxDate.set(maxDate);
1197
1260
  instance.dateFilter.set(dateFilter);
1198
1261
  instance.startView.set(startView);
1262
+ instance.startAt.set(startAt);
1199
1263
  instance.numberOfMonths.set(numberOfMonths);
1200
1264
  instance.pendingRange.set(pendingRange);
1201
1265
  instance.currentView.set(currentView);
@@ -1263,15 +1327,16 @@ class DateRangePickerComponent extends FormFieldControl {
1263
1327
  this.focusedSignal.set(!!origin);
1264
1328
  if (wasFocused && !origin) {
1265
1329
  this.onTouched();
1266
- this._ngControlRev.update((n) => n + 1);
1330
+ this.errorWiring.bump();
1267
1331
  }
1268
1332
  });
1269
1333
  this.destroyRef.onDestroy(() => {
1270
1334
  monitorSub.unsubscribe();
1271
1335
  this.focusMonitor.stopMonitoring(this.elementRef);
1272
1336
  // The coordinator's own DestroyRef.onDestroy disposes the overlay and
1273
- // clears its timers; we only need to drop the local instance signal.
1274
- this.overlayInstanceSignal.set(null);
1337
+ // clears its timers; we only need to drop the local instance reference.
1338
+ this.overlayInstance = null;
1339
+ this.isAttached.set(false);
1275
1340
  });
1276
1341
  }
1277
1342
  ngOnInit() {
@@ -1281,27 +1346,33 @@ class DateRangePickerComponent extends FormFieldControl {
1281
1346
  self: true,
1282
1347
  optional: true,
1283
1348
  });
1284
- // Wire this component as the NgControl's value accessor. NG_VALUE_ACCESSOR
1285
- // is not registered as a provider to keep `inject(NgControl, { self })`
1286
- // possible we assign it here instead.
1287
- if (this.ngControl) {
1288
- this.ngControl.valueAccessor = this;
1289
- }
1290
- const ctrl = this.ngControl?.control;
1291
- if (ctrl) {
1292
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
1293
- if (streams.length) {
1294
- merge(...streams)
1295
- .pipe(takeUntilDestroyed(this.destroyRef))
1296
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
1297
- }
1298
- }
1299
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
1300
- if (submit) {
1301
- submit
1302
- .pipe(takeUntilDestroyed(this.destroyRef))
1303
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
1304
- }
1349
+ // There is deliberately NO `ngControl.valueAccessor = this` here, and none is needed:
1350
+ // registration happens through the static NG_VALUE_ACCESSOR provider on this component,
1351
+ // which `FormControlName` / `NgModel` resolve while constructing on the same element —
1352
+ // long before ngOnInit. An assignment at this point would be a pure no-op on both the
1353
+ // classic and the signal-forms path.
1354
+ //
1355
+ // Do NOT read the absence of a runtime assignment as evidence the static provider is
1356
+ // optional. It is the only registration this component has: delete it and `validate()`
1357
+ // is never invoked, so every calendar error code silently disappears with no test failure
1358
+ // outside the guard spec. See the provider's own comment above.
1359
+ //
1360
+ // Bump the revision so any control-derived signal that was already read while
1361
+ // `ngControl` was still `null` recomputes. `date-picker` and `time-picker` have
1362
+ // always done this; this component's omission was an asymmetry preserved
1363
+ // verbatim through the `wireErrorState` extraction.
1364
+ //
1365
+ // Honest scope, measured rather than assumed: **no test distinguishes this
1366
+ // line.** Removing it leaves the whole suite green, including the
1367
+ // `aria-required from the bound control` guard below. `required` and its
1368
+ // siblings are `computed()`, and a `computed()` is lazy nothing reads them
1369
+ // before `ngOnInit` runs, so in every path exercised today there is no stale
1370
+ // cache to invalidate and the bump is a no-op. It is kept for symmetry with
1371
+ // the two sibling pickers, and as a guard against a future eager read (a
1372
+ // parent querying `required` during its own first pass would cache the
1373
+ // `null`-era answer permanently). Do not cite it as a bug fix.
1374
+ this.errorWiring.bump();
1375
+ this.errorWiring.connect();
1305
1376
  }
1306
1377
  // ── Public imperative API ──
1307
1378
  /** Opens the overlay. No-op when disabled or already open. */
@@ -1524,7 +1595,10 @@ class DateRangePickerComponent extends FormFieldControl {
1524
1595
  instance.locale.set(this.locale());
1525
1596
  instance.dateClass.set(this.dateClass());
1526
1597
  instance.cellTemplate.set(this.cellTemplate());
1527
- this.overlayInstanceSignal.set(instance);
1598
+ this.overlayInstance = instance;
1599
+ // Must follow the assignment above: this is what wakes the state-push
1600
+ // effect, and it reads `overlayInstance` as a plain field.
1601
+ this.isAttached.set(true);
1528
1602
  // Flush the initial config into the overlay's first render so view children
1529
1603
  // (calendar, time pickers, action bar) observe the picker's current inputs.
1530
1604
  result.componentRef.changeDetectorRef.detectChanges();
@@ -1553,9 +1627,9 @@ class DateRangePickerComponent extends FormFieldControl {
1553
1627
  });
1554
1628
  }
1555
1629
  closeOverlay(reason, restore = false) {
1556
- if (this.closingSignal() || !this.overlayInstance)
1630
+ if (this.closing || !this.overlayInstance)
1557
1631
  return;
1558
- this.closingSignal.set(true);
1632
+ this.closing = true;
1559
1633
  this.overlayInstance.leaving.set(true);
1560
1634
  if (restore) {
1561
1635
  const previous = this.lastValueBeforeOpen();
@@ -1569,10 +1643,11 @@ class DateRangePickerComponent extends FormFieldControl {
1569
1643
  }
1570
1644
  this.triggerRef().nativeElement.focus();
1571
1645
  this.coordinator.close(() => {
1572
- this.overlayInstanceSignal.set(null);
1646
+ this.overlayInstance = null;
1647
+ this.isAttached.set(false);
1573
1648
  this.pendingRange.set(null);
1574
1649
  untracked(() => this.open.set(false));
1575
- this.closingSignal.set(false);
1650
+ this.closing = false;
1576
1651
  this.closed.emit(reason);
1577
1652
  });
1578
1653
  }
@@ -1818,7 +1893,7 @@ class DateRangePickerComponent extends FormFieldControl {
1818
1893
  [attr.aria-label]="triggerAccessibleName() || null"
1819
1894
  [attr.aria-labelledby]="ariaLabelledby() || null"
1820
1895
  [attr.aria-describedby]="describedBy() || null"
1821
- [attr.aria-required]="requiredInput() || null"
1896
+ [attr.aria-required]="required() || null"
1822
1897
  [attr.aria-invalid]="errorState() || null"
1823
1898
  [attr.aria-disabled]="isDisabled() || null"
1824
1899
  [attr.data-variant]="resolvedVariant()"
@@ -1917,7 +1992,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1917
1992
  [attr.aria-label]="triggerAccessibleName() || null"
1918
1993
  [attr.aria-labelledby]="ariaLabelledby() || null"
1919
1994
  [attr.aria-describedby]="describedBy() || null"
1920
- [attr.aria-required]="requiredInput() || null"
1995
+ [attr.aria-required]="required() || null"
1921
1996
  [attr.aria-invalid]="errorState() || null"
1922
1997
  [attr.aria-disabled]="isDisabled() || null"
1923
1998
  [attr.data-variant]="resolvedVariant()"