@cdevhub/ngx-tw 0.2.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,4482 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, computed, ChangeDetectionStrategy, Component, Injectable, InjectionToken, viewChild, inject, signal, afterEveryRender, Directive, viewChildren, ElementRef, Injector, DestroyRef, LOCALE_ID, runInInjectionContext, model, linkedSignal, untracked, effect, isDevMode, forwardRef, makeEnvironmentProviders } from '@angular/core';
3
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { Validators, NgControl, FormResetEvent, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
6
+ import { tv } from 'tailwind-variants';
7
+ import { NgTemplateOutlet } from '@angular/common';
8
+
9
+ const headerVariants = tv({
10
+ slots: {
11
+ root: 'flex items-center justify-between px-2 py-1 mb-2',
12
+ navButton: 'inline-flex items-center justify-center select-none h-9 w-9 rounded-full text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:cursor-not-allowed disabled:text-fg-subtle disabled:hover:bg-transparent',
13
+ periodButton: 'inline-flex items-center justify-center select-none px-3 py-1.5 text-sm font-semibold rounded-md text-fg hover:bg-surface-muted transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:cursor-not-allowed disabled:text-fg-subtle disabled:hover:bg-transparent',
14
+ },
15
+ variants: {},
16
+ defaultVariants: {},
17
+ }, { twMerge: true });
18
+ /**
19
+ * Calendar navigation header — previous / period / next button row.
20
+ * The period label button toggles between views (month → year → multi-year).
21
+ */
22
+ class CalendarHeaderComponent {
23
+ /** Display text for the period button (e.g. "January 2026"). */
24
+ periodLabel = input.required(...(ngDevMode ? [{ debugName: "periodLabel" }] : /* istanbul ignore next */ []));
25
+ /** Accessible label for the period button. */
26
+ periodAriaLabel = input.required(...(ngDevMode ? [{ debugName: "periodAriaLabel" }] : /* istanbul ignore next */ []));
27
+ /** Accessible label for the previous button. */
28
+ prevAriaLabel = input.required(...(ngDevMode ? [{ debugName: "prevAriaLabel" }] : /* istanbul ignore next */ []));
29
+ /** Accessible label for the next button. */
30
+ nextAriaLabel = input.required(...(ngDevMode ? [{ debugName: "nextAriaLabel" }] : /* istanbul ignore next */ []));
31
+ /** Disables the previous button. */
32
+ prevDisabled = input(false, ...(ngDevMode ? [{ debugName: "prevDisabled" }] : /* istanbul ignore next */ []));
33
+ /** Disables the next button. */
34
+ nextDisabled = input(false, ...(ngDevMode ? [{ debugName: "nextDisabled" }] : /* istanbul ignore next */ []));
35
+ /** When false, the period button is disabled (e.g. when already at the top view). */
36
+ // TRUE-default: the period button is the calendar header's primary drill-up
37
+ // affordance; disabling it is reserved for the top view where there's nowhere
38
+ // to drill up to.
39
+ canSwitchView = input(true, ...(ngDevMode ? [{ debugName: "canSwitchView" }] : /* istanbul ignore next */ []));
40
+ /** Fires on previous button click. */
41
+ prevClicked = output();
42
+ /** Fires on next button click. */
43
+ nextClicked = output();
44
+ /** Fires on period button click (switch to the next higher view). */
45
+ periodClicked = output();
46
+ classes = computed(() => {
47
+ const v = headerVariants();
48
+ return { root: v.root(), navButton: v.navButton(), periodButton: v.periodButton() };
49
+ }, ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
50
+ onPeriodClick() {
51
+ if (this.canSwitchView())
52
+ this.periodClicked.emit();
53
+ }
54
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
55
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: CalendarHeaderComponent, isStandalone: true, selector: "tw-calendar-header", inputs: { periodLabel: { classPropertyName: "periodLabel", publicName: "periodLabel", isSignal: true, isRequired: true, transformFunction: null }, periodAriaLabel: { classPropertyName: "periodAriaLabel", publicName: "periodAriaLabel", isSignal: true, isRequired: true, transformFunction: null }, prevAriaLabel: { classPropertyName: "prevAriaLabel", publicName: "prevAriaLabel", isSignal: true, isRequired: true, transformFunction: null }, nextAriaLabel: { classPropertyName: "nextAriaLabel", publicName: "nextAriaLabel", isSignal: true, isRequired: true, transformFunction: null }, prevDisabled: { classPropertyName: "prevDisabled", publicName: "prevDisabled", isSignal: true, isRequired: false, transformFunction: null }, nextDisabled: { classPropertyName: "nextDisabled", publicName: "nextDisabled", isSignal: true, isRequired: false, transformFunction: null }, canSwitchView: { classPropertyName: "canSwitchView", publicName: "canSwitchView", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { prevClicked: "prevClicked", nextClicked: "nextClicked", periodClicked: "periodClicked" }, host: { classAttribute: "block" }, ngImport: i0, template: `
56
+ <div [class]="classes().root">
57
+ <button
58
+ type="button"
59
+ [class]="classes().navButton"
60
+ [disabled]="prevDisabled()"
61
+ [attr.aria-label]="prevAriaLabel()"
62
+ (click)="prevClicked.emit()"
63
+ >
64
+ <svg
65
+ class="size-5"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ aria-hidden="true"
70
+ >
71
+ <path
72
+ d="M15 18l-6-6 6-6"
73
+ stroke-width="2"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ />
77
+ </svg>
78
+ </button>
79
+
80
+ <button
81
+ type="button"
82
+ [class]="classes().periodButton"
83
+ [attr.aria-label]="periodAriaLabel()"
84
+ [disabled]="!canSwitchView()"
85
+ (click)="onPeriodClick()"
86
+ >
87
+ {{ periodLabel() }}
88
+ </button>
89
+
90
+ <button
91
+ type="button"
92
+ [class]="classes().navButton"
93
+ [disabled]="nextDisabled()"
94
+ [attr.aria-label]="nextAriaLabel()"
95
+ (click)="nextClicked.emit()"
96
+ >
97
+ <svg
98
+ class="size-5"
99
+ viewBox="0 0 24 24"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ aria-hidden="true"
103
+ >
104
+ <path
105
+ d="M9 18l6-6-6-6"
106
+ stroke-width="2"
107
+ stroke-linecap="round"
108
+ stroke-linejoin="round"
109
+ />
110
+ </svg>
111
+ </button>
112
+ </div>
113
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
114
+ }
115
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarHeaderComponent, decorators: [{
116
+ type: Component,
117
+ args: [{
118
+ selector: 'tw-calendar-header',
119
+ changeDetection: ChangeDetectionStrategy.OnPush,
120
+ host: { class: 'block' },
121
+ template: `
122
+ <div [class]="classes().root">
123
+ <button
124
+ type="button"
125
+ [class]="classes().navButton"
126
+ [disabled]="prevDisabled()"
127
+ [attr.aria-label]="prevAriaLabel()"
128
+ (click)="prevClicked.emit()"
129
+ >
130
+ <svg
131
+ class="size-5"
132
+ viewBox="0 0 24 24"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ aria-hidden="true"
136
+ >
137
+ <path
138
+ d="M15 18l-6-6 6-6"
139
+ stroke-width="2"
140
+ stroke-linecap="round"
141
+ stroke-linejoin="round"
142
+ />
143
+ </svg>
144
+ </button>
145
+
146
+ <button
147
+ type="button"
148
+ [class]="classes().periodButton"
149
+ [attr.aria-label]="periodAriaLabel()"
150
+ [disabled]="!canSwitchView()"
151
+ (click)="onPeriodClick()"
152
+ >
153
+ {{ periodLabel() }}
154
+ </button>
155
+
156
+ <button
157
+ type="button"
158
+ [class]="classes().navButton"
159
+ [disabled]="nextDisabled()"
160
+ [attr.aria-label]="nextAriaLabel()"
161
+ (click)="nextClicked.emit()"
162
+ >
163
+ <svg
164
+ class="size-5"
165
+ viewBox="0 0 24 24"
166
+ fill="none"
167
+ stroke="currentColor"
168
+ aria-hidden="true"
169
+ >
170
+ <path
171
+ d="M9 18l6-6-6-6"
172
+ stroke-width="2"
173
+ stroke-linecap="round"
174
+ stroke-linejoin="round"
175
+ />
176
+ </svg>
177
+ </button>
178
+ </div>
179
+ `,
180
+ }]
181
+ }], propDecorators: { periodLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "periodLabel", required: true }] }], periodAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "periodAriaLabel", required: true }] }], prevAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevAriaLabel", required: true }] }], nextAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextAriaLabel", required: true }] }], prevDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevDisabled", required: false }] }], nextDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextDisabled", required: false }] }], canSwitchView: [{ type: i0.Input, args: [{ isSignal: true, alias: "canSwitchView", required: false }] }], prevClicked: [{ type: i0.Output, args: ["prevClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }], periodClicked: [{ type: i0.Output, args: ["periodClicked"] }] } });
182
+
183
+ /**
184
+ * Localized strings + ARIA announcements consumed by the calendar (§19.4).
185
+ *
186
+ * Override per-field via Angular DI — provide a custom instance, or pass a
187
+ * `Partial<CalendarIntl>` into the calendar component's `intl` input. Unset
188
+ * fields fall through to the English defaults shipped here.
189
+ *
190
+ * All members take primitives so consumers can write straight string
191
+ * literals or template helpers without depending on internal types.
192
+ */
193
+ class CalendarIntl {
194
+ /**
195
+ * Plural rules used for the four plural-marked keys in §19.4: range length,
196
+ * "X dates selected", "Skipped {N} periods", and the year-range button label.
197
+ * Resolved lazily so SSR / non-`Intl` environments don't crash at construction.
198
+ */
199
+ _plural = null;
200
+ getPlural() {
201
+ if (this._plural !== null)
202
+ return this._plural;
203
+ this._plural =
204
+ typeof Intl !== 'undefined' && typeof Intl.PluralRules === 'function'
205
+ ? new Intl.PluralRules('en')
206
+ : { select: (n) => (n === 1 ? 'one' : 'other') };
207
+ return this._plural;
208
+ }
209
+ // ---------------------------------------------------------------------------
210
+ // Button labels (§19.4)
211
+ // ---------------------------------------------------------------------------
212
+ /** Aria label for the "previous month" navigation button (day view). */
213
+ previousMonthLabel = 'Previous month';
214
+ /** Aria label for the "next month" navigation button (day view). */
215
+ nextMonthLabel = 'Next month';
216
+ /** Aria label for the "previous year" navigation button (month view). */
217
+ previousYearLabel = 'Previous year';
218
+ /** Aria label for the "next year" navigation button (month view). */
219
+ nextYearLabel = 'Next year';
220
+ /** Aria label for the "previous N years" navigation button (year view). */
221
+ previousYearsLabel(yearsPerPage) {
222
+ const cat = this.getPlural().select(yearsPerPage);
223
+ return cat === 'one' ? `Previous ${yearsPerPage} year` : `Previous ${yearsPerPage} years`;
224
+ }
225
+ /** Aria label for the "next N years" navigation button (year view). */
226
+ nextYearsLabel(yearsPerPage) {
227
+ const cat = this.getPlural().select(yearsPerPage);
228
+ return cat === 'one' ? `Next ${yearsPerPage} year` : `Next ${yearsPerPage} years`;
229
+ }
230
+ /** Aria label for the "today" shortcut button (footer). */
231
+ todayLabel = 'Today';
232
+ /** Aria label for the "clear" button (footer). */
233
+ clearLabel = 'Clear';
234
+ /** Aria label for the "apply" button (footer). */
235
+ applyLabel = 'Apply';
236
+ /** Aria label for the trigger/open-calendar button. */
237
+ openCalendarLabel = 'Open calendar';
238
+ /** Aria label for the "choose date" trigger. */
239
+ chooseDateLabel = 'Choose date';
240
+ // ---------------------------------------------------------------------------
241
+ // View labels (§19.4)
242
+ // ---------------------------------------------------------------------------
243
+ /** Aria label applied to the calendar root element. */
244
+ calendarLabel = 'Calendar';
245
+ /** Aria label appended to the period button while in the day view. */
246
+ monthViewLabel = 'month view';
247
+ /** Aria label appended to the period button while in the month view. */
248
+ yearViewLabel = 'year view';
249
+ /** Aria label for the year (decade) view. */
250
+ decadeViewLabel = 'decade view';
251
+ /** Tooltip / aria text for the "switch to month view" affordance. */
252
+ switchToMonthViewLabel(period) {
253
+ return `${period}, click to switch to month view`;
254
+ }
255
+ /** Tooltip / aria text for the "switch to year view" affordance. */
256
+ switchToYearViewLabel(period) {
257
+ return `${period}, click to switch to year view`;
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Cell accessible names (§15.6, §19.4)
261
+ // ---------------------------------------------------------------------------
262
+ /**
263
+ * Returns the accessible name a screen reader announces for any cell.
264
+ * Default implementation appends "today" / "selected" / "disabled" suffixes
265
+ * to the supplied `display` string.
266
+ */
267
+ cellAccessibleName(ctx) {
268
+ const base = ctx.display ?? '';
269
+ const tags = [];
270
+ if (ctx.today)
271
+ tags.push('today');
272
+ if (ctx.selected)
273
+ tags.push('selected');
274
+ if (ctx.disabled)
275
+ tags.push('disabled');
276
+ return tags.length === 0 ? base : `${base}, ${tags.join(', ')}`;
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // ARIA announcements (§15.7, §19.4)
280
+ // ---------------------------------------------------------------------------
281
+ /** Live-region message after navigating the displayed period. */
282
+ navigatedTo(direction, period) {
283
+ return `Navigated to ${direction} ${period}`;
284
+ }
285
+ /** Live-region message after a single-mode selection commits. */
286
+ selectedAnnouncement(value) {
287
+ return `Selected ${value}`;
288
+ }
289
+ /** Live-region message after the start of a range is picked. */
290
+ rangeStartAnnouncement(start) {
291
+ return `Start date selected: ${start}. Now select end date.`;
292
+ }
293
+ /** Live-region message after a range commits. Plural-marked on `lengthDays`. */
294
+ rangeUpdateAnnouncement(start, end, lengthDays) {
295
+ const cat = this.getPlural().select(lengthDays);
296
+ const noun = cat === 'one' ? 'day' : 'days';
297
+ return `Range selected: ${start} to ${end}, ${lengthDays} ${noun}`;
298
+ }
299
+ /** Live-region message after a `mode: 'multiple'` selection commits. Plural-marked. */
300
+ multipleSelectionAnnouncement(count) {
301
+ const cat = this.getPlural().select(count);
302
+ const noun = cat === 'one' ? 'date' : 'dates';
303
+ return `${count} ${noun} selected`;
304
+ }
305
+ /** Live-region message after the active view changes. */
306
+ viewSwitched(view, period) {
307
+ const label = view === 'day' ? this.monthViewLabel.replace(/ view$/, '') : view === 'month' ? this.yearViewLabel.replace(/ view$/, '') : this.decadeViewLabel.replace(/ view$/, '');
308
+ return `${label} view, ${period}`;
309
+ }
310
+ /** Live-region template used while a range pick is in progress. */
311
+ inProgressTemplate(state) {
312
+ return `Selecting: ${state}, now select end date`;
313
+ }
314
+ /** Live-region message announcing how many empty periods auto-skip jumped over. Plural-marked. */
315
+ skippedPeriods(count, destination) {
316
+ const cat = this.getPlural().select(count);
317
+ const noun = cat === 'one' ? 'period' : 'periods';
318
+ return `Skipped ${count} ${noun} to ${destination}`;
319
+ }
320
+ /** Live-region message announced when a click is rejected and the cell flashes (e.g. `'require-clear'` rejection, disabled-cross range commit). */
321
+ selectionRejectedAnnouncement = 'Selection rejected';
322
+ /**
323
+ * When `true`, the calendar suppresses live-region announcements for the
324
+ * affected user actions. Override in tests or low-noise environments.
325
+ */
326
+ skipAnnouncement = false;
327
+ // ---------------------------------------------------------------------------
328
+ // Error messages (§10.2, §19.4)
329
+ // ---------------------------------------------------------------------------
330
+ /** Message for the `calendarRequired` validation error. */
331
+ requiredError = 'Please select a date';
332
+ /** Message for the `calendarMinDate` validation error. */
333
+ minDateError(date) {
334
+ return `Date must be on or after ${date}`;
335
+ }
336
+ /** Message for the `calendarMaxDate` validation error. */
337
+ maxDateError(date) {
338
+ return `Date must be on or before ${date}`;
339
+ }
340
+ /** Message for the `calendarInvalidValue` validation error. */
341
+ invalidValueError = 'Invalid date value';
342
+ /** Message for the `calendarRangeTooShort` validation error. Plural-marked on `min`. */
343
+ rangeTooShortError(min) {
344
+ const cat = this.getPlural().select(min);
345
+ const noun = cat === 'one' ? 'day' : 'days';
346
+ return `Range must be at least ${min} ${noun}`;
347
+ }
348
+ /** Message for the `calendarRangeTooLong` validation error. Plural-marked on `max`. */
349
+ rangeTooLongError(max) {
350
+ const cat = this.getPlural().select(max);
351
+ const noun = cat === 'one' ? 'day' : 'days';
352
+ return `Range must be at most ${max} ${noun}`;
353
+ }
354
+ /** Message for the `calendarInvalidRange` validation error. */
355
+ invalidRangeError = 'Invalid date range';
356
+ /** Message for the `calendarDisabledDate` validation error. */
357
+ dateFilterError = 'This date is not available';
358
+ /** Message for the `calendarMaxSelections` validation error. */
359
+ maxSelectionsError(limit) {
360
+ const cat = this.getPlural().select(limit);
361
+ const noun = cat === 'one' ? 'date' : 'dates';
362
+ return `You may select at most ${limit} ${noun}`;
363
+ }
364
+ // ---------------------------------------------------------------------------
365
+ // Parse-error label (§10.2, §19.4)
366
+ // ---------------------------------------------------------------------------
367
+ /** Label announced when a typed date fails to parse. */
368
+ parseErrorLabel = 'Invalid date format';
369
+ // ---------------------------------------------------------------------------
370
+ // Keyboard help (§16.4, §19.4)
371
+ // ---------------------------------------------------------------------------
372
+ /** Multi-line description of the keyboard contract for screen readers / help dialogs. */
373
+ keyboardHelpText = [
374
+ 'Use arrow keys to move between dates.',
375
+ 'Press Enter or Space to select the focused date.',
376
+ 'Use Page Up and Page Down to move between months.',
377
+ 'Use Shift + Page Up and Shift + Page Down to move between years.',
378
+ 'Use Home and End to jump to the start or end of the week.',
379
+ 'Press Escape to close the calendar.',
380
+ ].join(' ');
381
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarIntl, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
382
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarIntl });
383
+ }
384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarIntl, decorators: [{
385
+ type: Injectable
386
+ }] });
387
+ /**
388
+ * Provides a custom `CalendarIntl` instance. Place at any injector level —
389
+ * the calendar resolves the closest one and merges any per-instance `intl`
390
+ * input on top of it (§19.4 per-field merge semantics).
391
+ */
392
+ function provideCalendarIntl(custom) {
393
+ return {
394
+ provide: CalendarIntl,
395
+ useFactory: () => Object.assign(new CalendarIntl(), custom),
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Calendar shared types. Aligned with `docs/requirements/calendar-component-requirements.md` v2.6
401
+ * (§7.1, §7.4, §8, §10.2, §22.5, §33).
402
+ */
403
+ /** Mode-agnostic empty value — `null` for single, `[]` for multiple, `{ start: null, end: null }` for range. */
404
+ function emptyCalendarValue(mode) {
405
+ if (mode === 'single')
406
+ return null;
407
+ if (mode === 'multiple')
408
+ return [];
409
+ return { start: null, end: null };
410
+ }
411
+ /** Factory for a plain `DateRange<D>`. */
412
+ function createDateRange(start, end) {
413
+ return { start, end };
414
+ }
415
+ /** Factory for a `CalendarCell` with boolean state fields zeroed out. */
416
+ function createCalendarCell(config) {
417
+ return {
418
+ value: config.value,
419
+ displayValue: config.displayValue,
420
+ ariaLabel: config.ariaLabel,
421
+ enabled: config.enabled ?? true,
422
+ cssClasses: config.cssClasses ?? '',
423
+ compareValue: config.compareValue,
424
+ isToday: false,
425
+ isSelected: false,
426
+ isRangeStart: false,
427
+ isRangeMiddle: false,
428
+ isRangeEnd: false,
429
+ isPreviewStart: false,
430
+ isPreviewMiddle: false,
431
+ isPreviewEnd: false,
432
+ isOutOfMonth: false,
433
+ isWeekend: false,
434
+ isInvalidPreview: false,
435
+ isInvalidFlash: false,
436
+ };
437
+ }
438
+ /** Days in a week. */
439
+ const DAYS_PER_WEEK = 7;
440
+ /** Rows in a month grid. */
441
+ const WEEKS_PER_MONTH = 6;
442
+ /** Default years shown per page in the year view. Phase 7 replaces this constant with the `yearsPerPage` input (default 20 per §33.1). */
443
+ const YEARS_PER_PAGE = 24;
444
+ /** Years per row in the year view. */
445
+ const YEARS_PER_ROW = 4;
446
+ /** Months per row in the month (of year) view. */
447
+ const MONTHS_PER_ROW = 4;
448
+
449
+ /** Pure helpers for CalendarComponent's ControlValueAccessor implementation. Extracted to keep `calendar.ts` focused on the class wiring; behavior is unchanged. */
450
+ /**
451
+ * `true` when the incoming value's shape matches the shape required by `mode`.
452
+ * `null` / `undefined` is handled upstream by callers and is NOT considered here.
453
+ *
454
+ * - `single`: anything that is not an array and not a `{ start, end }` object.
455
+ * - `multiple`: any array.
456
+ * - `range`: a non-array object with both `start` and `end` keys.
457
+ */
458
+ function matchesModeShape(mode, value) {
459
+ if (mode === 'single') {
460
+ if (Array.isArray(value))
461
+ return false;
462
+ if (typeof value === 'object' && value !== null) {
463
+ const r = value;
464
+ if ('start' in r && 'end' in r)
465
+ return false;
466
+ }
467
+ return true;
468
+ }
469
+ if (mode === 'multiple') {
470
+ return Array.isArray(value);
471
+ }
472
+ // range
473
+ if (typeof value !== 'object' || value === null)
474
+ return false;
475
+ if (Array.isArray(value))
476
+ return false;
477
+ const range = value;
478
+ return 'start' in range && 'end' in range;
479
+ }
480
+ /**
481
+ * `true` when the incoming value is a partial range (`{ start: D, end: null }`).
482
+ * Per §7.2, only EMPTY or COMPLETE shapes are valid form values for `mode: 'range'`,
483
+ * so partial ranges arriving via `writeValue` are rejected.
484
+ *
485
+ * Caller must already have confirmed `mode === 'range'` and `matchesModeShape` passed.
486
+ */
487
+ function isPartialRangeValue(value) {
488
+ if (typeof value !== 'object' || value === null)
489
+ return false;
490
+ const r = value;
491
+ return r.start != null && r.end == null;
492
+ }
493
+ /** `true` when `value` is the mode-agnostic empty value (null, `[]`, or `{ start: null, end: null }`). */
494
+ function isEmptyCalendarValue(value) {
495
+ if (value == null)
496
+ return true;
497
+ if (Array.isArray(value))
498
+ return value.length === 0;
499
+ if (typeof value === 'object' && 'start' in value && 'end' in value) {
500
+ const r = value;
501
+ return r.start == null && r.end == null;
502
+ }
503
+ return false;
504
+ }
505
+ /** Derives selection state from a value shape. Used by `writeValue` to reconcile post-write state. */
506
+ function deriveSelectionStateFromValue(value) {
507
+ return isEmptyCalendarValue(value) ? 'EMPTY' : 'COMPLETE';
508
+ }
509
+
510
+ /** Arranges a flat array of items into a 2D grid with `columns` items per row. */
511
+ function createGrid(items, columns) {
512
+ const grid = [];
513
+ for (let i = 0; i < items.length; i += columns) {
514
+ grid.push(items.slice(i, i + columns));
515
+ }
516
+ return grid;
517
+ }
518
+ /** Returns the new flat index after an arrow-key navigation step, or `null` on out of bounds. */
519
+ function navigateGrid(currentIndex, direction, totalItems, columns) {
520
+ let next;
521
+ switch (direction) {
522
+ case 'left':
523
+ next = currentIndex - 1;
524
+ break;
525
+ case 'right':
526
+ next = currentIndex + 1;
527
+ break;
528
+ case 'up':
529
+ next = currentIndex - columns;
530
+ break;
531
+ case 'down':
532
+ next = currentIndex + columns;
533
+ break;
534
+ }
535
+ if (next < 0 || next >= totalItems)
536
+ return null;
537
+ return next;
538
+ }
539
+ /** Builds seven weekday headers rotated to start at `firstDayOfWeek` (or adapter default). */
540
+ function getWeekdayHeaders(adapter, style = 'long', firstDayOfWeek) {
541
+ const start = firstDayOfWeek ?? adapter.getFirstDayOfWeek();
542
+ const longNames = adapter.getDayOfWeekNames(style);
543
+ const narrowNames = adapter.getDayOfWeekNames('narrow');
544
+ const out = [];
545
+ for (let i = 0; i < 7; i++) {
546
+ const idx = (start + i) % 7;
547
+ out.push({ label: longNames[idx] ?? '', narrow: narrowNames[idx] ?? '' });
548
+ }
549
+ return out;
550
+ }
551
+ /**
552
+ * True when `date` falls outside `[minDate, maxDate]` or fails the filter.
553
+ *
554
+ * Phase 4 superset: also OR-checks `disabledDates` (array OR predicate) and
555
+ * `disabledDaysOfWeek`. Legacy callers passing only the first four arguments
556
+ * keep working — the new sources default to neutral values.
557
+ */
558
+ function isDateDisabled(date, minDate, maxDate, dateFilter, adapter, disabledDates = null, disabledDaysOfWeek = []) {
559
+ if (minDate && adapter.compare(date, minDate) < 0)
560
+ return true;
561
+ if (maxDate && adapter.compare(date, maxDate) > 0)
562
+ return true;
563
+ if (dateFilter && !dateFilter(date))
564
+ return true;
565
+ if (disabledDaysOfWeek.length > 0) {
566
+ if (disabledDaysOfWeek.includes(adapter.getDayOfWeek(date)))
567
+ return true;
568
+ }
569
+ if (disabledDates) {
570
+ if (typeof disabledDates === 'function') {
571
+ if (disabledDates(date))
572
+ return true;
573
+ }
574
+ else {
575
+ for (const d of disabledDates) {
576
+ if (adapter.sameDate(date, d))
577
+ return true;
578
+ }
579
+ }
580
+ }
581
+ return false;
582
+ }
583
+ /**
584
+ * Phase 4 single-call constraint resolver — `true` when ANY source disables `date`.
585
+ * Equivalent to `isDateDisabled(date, c.minDate, c.maxDate, c.dateFilter, adapter, c.disabledDates, c.disabledDaysOfWeek)`
586
+ * but accepts the spec-shaped `CalendarConstraints<D>` aggregate.
587
+ */
588
+ function resolveDateDisabled(date, constraints, adapter) {
589
+ return isDateDisabled(date, constraints.minDate ?? null, constraints.maxDate ?? null, constraints.dateFilter ?? null, adapter, constraints.disabledDates ?? null, constraints.disabledDaysOfWeek ?? []);
590
+ }
591
+ /**
592
+ * True when `date` is a weekend day (Saturday or Sunday by default — `adapter.getDayOfWeek`
593
+ * returns `0` for Sunday and `6` for Saturday in the Gregorian contract). The optional
594
+ * `weekendDays` parameter lets a consumer override per locale (e.g. `[5, 6]` for ar-SA).
595
+ */
596
+ function isWeekend(date, adapter, weekendDays = [0, 6]) {
597
+ return weekendDays.includes(adapter.getDayOfWeek(date));
598
+ }
599
+ /**
600
+ * Inclusive day-count between `start` and `end` — used for `minRangeLength` /
601
+ * `maxRangeLength` validation and the `rangeUpdateAnnouncement` length argument.
602
+ * Assumes `start <= end` (call-sites normalize first).
603
+ */
604
+ function rangeLengthDays(start, end, adapter) {
605
+ let n = 1;
606
+ let cursor = start;
607
+ // Walk day-by-day to keep the math adapter-agnostic and DST-safe.
608
+ // For ranges over a few months this is still microseconds.
609
+ while (adapter.compare(cursor, end) < 0) {
610
+ cursor = adapter.addDays(cursor, 1);
611
+ n++;
612
+ }
613
+ return n;
614
+ }
615
+ /**
616
+ * `true` when ANY date strictly between `start` and `end` (exclusive of the
617
+ * endpoints when `includeEndpoints` is `false`) is disabled by `constraints`.
618
+ * Used by `disableRangesCrossingDisabledDates` and the `invalidPreview` flag.
619
+ */
620
+ function rangeCrossesDisabled(start, end, constraints, adapter, includeEndpoints = true) {
621
+ let cursor = includeEndpoints ? start : adapter.addDays(start, 1);
622
+ const limit = includeEndpoints ? end : adapter.addDays(end, -1);
623
+ while (adapter.compare(cursor, limit) <= 0) {
624
+ if (resolveDateDisabled(cursor, constraints, adapter))
625
+ return true;
626
+ cursor = adapter.addDays(cursor, 1);
627
+ }
628
+ return false;
629
+ }
630
+ /** True when the whole month falls outside `[minDate, maxDate]`. `month` is 0-based (matches `adapter.getMonth`). */
631
+ function isMonthDisabled(year, month, minDate, maxDate, adapter) {
632
+ const firstOfMonth = adapter.create(year, month + 1, 1);
633
+ const lastOfMonth = adapter.create(year, month + 1, adapter.getNumDaysInMonth(firstOfMonth));
634
+ if (minDate && adapter.compare(lastOfMonth, minDate) < 0)
635
+ return true;
636
+ if (maxDate && adapter.compare(firstOfMonth, maxDate) > 0)
637
+ return true;
638
+ return false;
639
+ }
640
+ /** True when the whole year falls outside `[minDate, maxDate]`. */
641
+ function isYearDisabled(year, minDate, maxDate, adapter) {
642
+ const firstOfYear = adapter.create(year, 1, 1);
643
+ const lastOfYear = adapter.create(year, 12, 31);
644
+ if (minDate && adapter.compare(lastOfYear, minDate) < 0)
645
+ return true;
646
+ if (maxDate && adapter.compare(firstOfYear, maxDate) > 0)
647
+ return true;
648
+ return false;
649
+ }
650
+ /** Aligns `activeYear` to the start of its `yearsPerPage`-block. */
651
+ function getMultiYearStartingYear(activeYear, yearsPerPage = 24) {
652
+ return Math.floor(activeYear / yearsPerPage) * yearsPerPage;
653
+ }
654
+ /** True when `date` sits inside `[start, end]` (inclusive). */
655
+ function isDateInRange(date, start, end, adapter) {
656
+ if (!start || !end)
657
+ return false;
658
+ return adapter.compare(date, start) >= 0 && adapter.compare(date, end) <= 0;
659
+ }
660
+ /** Returns the first day of the month for `date`. */
661
+ function getFirstDayOfMonth(date, adapter) {
662
+ return adapter.create(adapter.getYear(date), adapter.getMonth(date) + 1, 1);
663
+ }
664
+
665
+ /** `true` when `value` matches the mode-specific empty state (§7.1, §6.4). */
666
+ function isCalendarValueEmpty(mode, value) {
667
+ if (mode === 'single')
668
+ return value == null;
669
+ if (mode === 'multiple')
670
+ return !Array.isArray(value) || value.length === 0;
671
+ // range
672
+ if (value == null)
673
+ return true;
674
+ const range = value;
675
+ return range.start == null && range.end == null;
676
+ }
677
+ /**
678
+ * Built-in synchronous validator (§6.1, §10.2). Emits the full v1 error code set:
679
+ *
680
+ * Phase 3 codes:
681
+ * - `calendarRequired` when the hosting control carries `Validators.required`
682
+ * and the calendar value is mode-specific empty.
683
+ * - `calendarInvalidValue` when the most recent `writeValue` was rejected as
684
+ * wrong-shape (§7.2) — the raw value is held on the component's
685
+ * `lastInvalidFormValue` until a valid write clears it.
686
+ *
687
+ * Phase 4 codes (active when `ctx.constraints` + `ctx.adapter` are supplied):
688
+ * - `calendarMinDate` / `calendarMaxDate` — value (or any range endpoint /
689
+ * array entry) falls outside `[minDate, maxDate]`.
690
+ * - `calendarDisabledDate` — value matches `dateFilter` / `disabledDates` /
691
+ * `disabledDaysOfWeek` (constraint resolver).
692
+ * - `calendarRangeTooShort` / `calendarRangeTooLong` — `mode: 'range'` length
693
+ * violates `minRangeLength` / `maxRangeLength`.
694
+ * - `calendarMaxSelections` — `mode: 'multiple'` array exceeds `maxSelections`.
695
+ * - `calendarInvalidRange` — committed range has `start > end` (the orchestrator
696
+ * normalizes via auto-swap; this only triggers on programmatic writeValue).
697
+ *
698
+ * Each code's payload mirrors §10.2 — `{ min, actual }`, `{ max, actual }`,
699
+ * `{ length, min }`, etc.
700
+ */
701
+ function calendarValidator(ctx) {
702
+ return (control) => {
703
+ const errors = {};
704
+ if (ctx.lastInvalidFormValue !== null && ctx.lastInvalidFormValue !== undefined) {
705
+ errors.calendarInvalidValue = {
706
+ expected: ctx.mode,
707
+ actual: ctx.lastInvalidFormValue,
708
+ reason: 'shape',
709
+ };
710
+ }
711
+ if (control.hasValidator(Validators.required)) {
712
+ const value = control.value;
713
+ if (isCalendarValueEmpty(ctx.mode, value)) {
714
+ errors.calendarRequired = true;
715
+ }
716
+ }
717
+ // Phase 4 — constraint codes (only when the host wires constraints + adapter).
718
+ if (ctx.constraints && ctx.adapter) {
719
+ const value = control.value;
720
+ if (value !== null &&
721
+ value !== undefined &&
722
+ !isCalendarValueEmpty(ctx.mode, value)) {
723
+ applyConstraintCodes(ctx, value, errors);
724
+ }
725
+ }
726
+ return Object.keys(errors).length > 0 ? errors : null;
727
+ };
728
+ }
729
+ /**
730
+ * Mode-aware constraint validator. Mutates `errors` in place — the caller
731
+ * decides whether the resulting object is `null` (no codes) or non-null.
732
+ *
733
+ * @internal
734
+ */
735
+ function applyConstraintCodes(ctx, value, errors) {
736
+ const adapter = ctx.adapter;
737
+ const constraints = ctx.constraints;
738
+ const min = constraints.minDate;
739
+ const max = constraints.maxDate;
740
+ const checkSingle = (date) => {
741
+ if (min && adapter.compare(date, min) < 0) {
742
+ errors.calendarMinDate = { min, actual: date };
743
+ }
744
+ if (max && adapter.compare(date, max) > 0) {
745
+ errors.calendarMaxDate = { max, actual: date };
746
+ }
747
+ if (resolveDateDisabled(date, constraints, adapter)) {
748
+ // Suppress when min/max already flagged the same date — `calendarDisabledDate`
749
+ // is the catch-all for the non-range constraint sources.
750
+ if (!errors.calendarMinDate && !errors.calendarMaxDate) {
751
+ errors.calendarDisabledDate = { actual: date };
752
+ }
753
+ }
754
+ };
755
+ if (ctx.mode === 'single') {
756
+ checkSingle(value);
757
+ return;
758
+ }
759
+ if (ctx.mode === 'multiple') {
760
+ const arr = value;
761
+ for (const d of arr) {
762
+ checkSingle(d);
763
+ if (errors.calendarMinDate || errors.calendarMaxDate || errors.calendarDisabledDate)
764
+ break;
765
+ }
766
+ const limit = ctx.maxSelections;
767
+ if (limit !== null && limit !== undefined && arr.length > limit) {
768
+ errors.calendarMaxSelections = { limit, actual: arr.length };
769
+ }
770
+ return;
771
+ }
772
+ // range
773
+ const range = value;
774
+ if (range.start)
775
+ checkSingle(range.start);
776
+ if (range.end)
777
+ checkSingle(range.end);
778
+ if (range.start && range.end) {
779
+ if (adapter.compare(range.start, range.end) > 0) {
780
+ errors.calendarInvalidRange = { start: range.start, end: range.end };
781
+ return;
782
+ }
783
+ if (rangeCrossesDisabled(range.start, range.end, constraints, adapter, false)) {
784
+ errors.calendarDisabledDate = { actual: range };
785
+ }
786
+ const length = rangeLengthDays(range.start, range.end, adapter);
787
+ const minLen = ctx.minRangeLength;
788
+ const maxLen = ctx.maxRangeLength;
789
+ if (minLen !== null && minLen !== undefined && length < minLen) {
790
+ errors.calendarRangeTooShort = { length, min: minLen };
791
+ }
792
+ if (maxLen !== null && maxLen !== undefined && length > maxLen) {
793
+ errors.calendarRangeTooLong = { length, max: maxLen };
794
+ }
795
+ }
796
+ }
797
+ /**
798
+ * Standalone required-validator helper for consumers who want to compose
799
+ * calendar-required on a control that is **not** bound directly to a
800
+ * `CalendarComponent`. The rule is identical: mode-specific empty ⇒ error.
801
+ */
802
+ function calendarRequiredValidator(mode) {
803
+ return (control) => {
804
+ const value = control.value;
805
+ return isCalendarValueEmpty(mode, value) ? { calendarRequired: true } : null;
806
+ };
807
+ }
808
+
809
+ /**
810
+ * Injection token for a per-instance timezone override (§7.4, §4.3).
811
+ * When set, TZ-aware adapters resolve `today()` and date-range math against
812
+ * the supplied IANA timezone. Floating (naive) adapters ignore the token.
813
+ */
814
+ const TZ_OVERRIDE = new InjectionToken('tw-calendar/TZ_OVERRIDE');
815
+ /**
816
+ * Injection token for the default value transformer (§7.4, §7.6). Phase 14 wires
817
+ * the component-level `valueTransformer` input to override this DI default.
818
+ * Shipped as a token shell now so later phases can bind against it without a
819
+ * breaking change.
820
+ */
821
+ const DATE_SERIALIZATION = new InjectionToken('tw-calendar/DATE_SERIALIZATION');
822
+ /** DI token carrying the default `DateFormats`. The calendar's `dateFormats` input overrides. */
823
+ const DATE_FORMATS = new InjectionToken('tw-calendar/DateFormats');
824
+ /**
825
+ * Abstract date adapter. Ship a subclass to swap the underlying date library
826
+ * (Luxon, date-fns, Temporal). The calendar consumes only the methods defined
827
+ * here — nothing else.
828
+ *
829
+ * `D` is the native date type understood by the underlying library.
830
+ *
831
+ * Method contract aligned with spec §20.1 / §20.2: `addYears`/`addMonths`/
832
+ * `addDays`, `compare`, 1-based-month `create`, `toIso`/`fromIso`, and
833
+ * optional TZ virtuals.
834
+ */
835
+ class DateAdapter {
836
+ /** Days per week. Override only for non-Gregorian adapters (WONT v1). */
837
+ getDaysInWeek() {
838
+ return 7;
839
+ }
840
+ /** Returns 1-based day-of-month names (`["1", "2", ..., "31"]`) for locales that localize numerals. Default implementation returns Latin digits. */
841
+ getDateNames(_style = 'short') {
842
+ return Array.from({ length: 31 }, (_, i) => String(i + 1));
843
+ }
844
+ /** Time arithmetic. Default implementation derives hours/minutes from day arithmetic via `addDays` — adapters with better precision should override. */
845
+ addHours(date, hours) {
846
+ const minutes = hours * 60;
847
+ return this.addMinutes(date, minutes);
848
+ }
849
+ /** ISO 8601 (`YYYY-MM-DD`) deserialization counterpart to `toIso`. */
850
+ fromIso(iso) {
851
+ return this.parse(iso);
852
+ }
853
+ /** First day of the week containing `date`. `firstDayOfWeek` defaults to `getFirstDayOfWeek()`. */
854
+ startOfWeek(date, firstDayOfWeek) {
855
+ const first = firstDayOfWeek ?? this.getFirstDayOfWeek();
856
+ const dow = this.getDayOfWeek(date);
857
+ const diff = (dow - first + this.getDaysInWeek()) % this.getDaysInWeek();
858
+ return this.addDays(date, -diff);
859
+ }
860
+ /** Last day of the week containing `date`. */
861
+ endOfWeek(date, firstDayOfWeek) {
862
+ return this.addDays(this.startOfWeek(date, firstDayOfWeek), this.getDaysInWeek() - 1);
863
+ }
864
+ /** Equality on year + month. */
865
+ sameMonth(first, second) {
866
+ return (this.getYear(first) === this.getYear(second) &&
867
+ this.getMonth(first) === this.getMonth(second));
868
+ }
869
+ /** Equality on year only. */
870
+ sameYear(first, second) {
871
+ return this.getYear(first) === this.getYear(second);
872
+ }
873
+ /** Clamp a date into the [min, max] window. */
874
+ clampDate(date, min, max) {
875
+ if (min && this.compare(date, min) < 0)
876
+ return min;
877
+ if (max && this.compare(date, max) > 0)
878
+ return max;
879
+ return date;
880
+ }
881
+ // ---------------------------------------------------------------------------
882
+ // Optional TZ-aware virtuals (§4.2). Floating adapters implement as
883
+ // pass-through; TZ-aware adapters (Luxon, Temporal) override.
884
+ // ---------------------------------------------------------------------------
885
+ /** Returns the adapter's configured timezone, or `null` for floating adapters. */
886
+ getTimezone() {
887
+ return null;
888
+ }
889
+ /** Returns a copy of `date` reinterpreted in `tz`. Floating adapters pass through. */
890
+ withTimezone(date, _tz) {
891
+ return date;
892
+ }
893
+ /** Returns `true` when `date` sits inside a DST-transition. Floating adapters return `false`. */
894
+ isDST(_date) {
895
+ return false;
896
+ }
897
+ /** Resolves an ambiguous wall-clock time (DST fall-back) to one of the two candidates. Floating adapters pass through. */
898
+ resolveAmbiguous(date, _prefer) {
899
+ return date;
900
+ }
901
+ }
902
+ /**
903
+ * Injection token for the calendar's `DateAdapter`. Prefer `provideNativeDateAdapter()`
904
+ * or `provideTwCalendar({ adapter })` over binding this token directly.
905
+ */
906
+ const DATE_ADAPTER = new InjectionToken('tw-calendar/DateAdapter');
907
+
908
+ const cellVariants = tv({
909
+ slots: {
910
+ wrapper: 'relative flex items-center justify-center',
911
+ button: 'relative flex items-center justify-center cursor-pointer select-none transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:cursor-not-allowed',
912
+ },
913
+ variants: {
914
+ view: {
915
+ day: { button: 'h-9 w-9 text-sm rounded-full' },
916
+ month: { button: 'h-10 w-16 text-sm rounded-md' },
917
+ year: { button: 'h-10 w-14 text-sm rounded-md' },
918
+ },
919
+ // Slot tokens own light/dark contrast — no `dark:`, no shade picks.
920
+ // The range wash uses `primary-soft-hover` (one step deeper than `-soft`)
921
+ // so the in-range band reads slightly stronger than the popover surface
922
+ // without competing with the selected endpoints.
923
+ state: {
924
+ default: { button: 'text-fg hover:bg-surface-muted' },
925
+ today: {
926
+ button: 'text-primary-fg font-semibold ring-1 ring-primary-border-strong hover:bg-primary-soft',
927
+ },
928
+ selected: {
929
+ button: 'bg-primary-solid text-primary-solid-fg font-semibold hover:bg-primary-solid-hover',
930
+ },
931
+ 'range-start': {
932
+ button: 'bg-primary-solid text-primary-solid-fg font-semibold rounded-l-full rounded-r-none hover:bg-primary-solid-hover',
933
+ },
934
+ 'range-middle': {
935
+ button: 'bg-primary-soft-hover text-primary-soft-fg rounded-none hover:bg-primary-soft',
936
+ },
937
+ 'range-end': {
938
+ button: 'bg-primary-solid text-primary-solid-fg font-semibold rounded-r-full rounded-l-none hover:bg-primary-solid-hover',
939
+ },
940
+ 'preview-start': {
941
+ button: 'bg-primary-soft-hover text-primary-soft-fg rounded-l-full rounded-r-none ring-1 ring-primary-border-strong opacity-70',
942
+ },
943
+ 'preview-middle': {
944
+ button: 'bg-primary-soft-hover text-primary-soft-fg rounded-none opacity-70',
945
+ },
946
+ 'preview-end': {
947
+ button: 'bg-primary-soft-hover text-primary-soft-fg rounded-r-full rounded-l-none ring-1 ring-primary-border-strong opacity-70',
948
+ },
949
+ disabled: { button: 'text-fg-subtle cursor-not-allowed hover:bg-transparent' },
950
+ },
951
+ outside: {
952
+ true: { button: 'text-fg-muted' },
953
+ false: {},
954
+ },
955
+ range: {
956
+ none: {},
957
+ start: { wrapper: 'bg-primary-soft-hover rounded-l-full' },
958
+ middle: { wrapper: 'bg-primary-soft-hover' },
959
+ end: { wrapper: 'bg-primary-soft-hover rounded-r-full' },
960
+ single: {},
961
+ },
962
+ },
963
+ compoundVariants: [
964
+ { state: 'disabled', outside: true, class: { button: 'text-fg-subtle' } },
965
+ { state: 'selected', class: { button: 'ring-0' } },
966
+ ],
967
+ defaultVariants: { view: 'day', state: 'default', outside: false, range: 'none' },
968
+ }, { twMerge: true });
969
+ /**
970
+ * Renders a single cell in any calendar view.
971
+ *
972
+ * The cell is a stateless presentation component — selection / preview /
973
+ * today state is all derived from the `CalendarCell<D>` data object. It
974
+ * emits events for clicks, keyboard navigation, mouse hover, and focus.
975
+ */
976
+ class CalendarCellComponent {
977
+ buttonRef = viewChild('cellButton', ...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
978
+ /** The cell data object to render. */
979
+ cell = input.required(...(ngDevMode ? [{ debugName: "cell" }] : /* istanbul ignore next */ []));
980
+ /** The active calendar view — drives cell dimensions and radius. */
981
+ view = input('day', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
982
+ /** `true` when the cell's date is outside the currently displayed month. */
983
+ outside = input(false, ...(ngDevMode ? [{ debugName: "outside" }] : /* istanbul ignore next */ []));
984
+ /** Tab index for roving focus (`0` for the active cell, `-1` otherwise). */
985
+ tabindex = input(-1, ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
986
+ /** Optional template rendered inside the button — receives `{ $implicit: CalendarCell<D> }`. */
987
+ cellTemplate = input(null, ...(ngDevMode ? [{ debugName: "cellTemplate" }] : /* istanbul ignore next */ []));
988
+ /** Emitted when the user activates the cell (click, Enter, or Space). */
989
+ selected = output();
990
+ /** Emitted when the cell gains focus. */
991
+ focused = output();
992
+ /** Emitted on pointer hover — parent uses this to drive range preview. */
993
+ previewed = output();
994
+ /** Emitted on navigation keys (arrows, Home/End, PageUp/PageDown). */
995
+ keyNav = output();
996
+ cellState = computed(() => {
997
+ const c = this.cell();
998
+ if (!c.enabled)
999
+ return 'disabled';
1000
+ if (c.isRangeStart)
1001
+ return 'range-start';
1002
+ if (c.isRangeEnd)
1003
+ return 'range-end';
1004
+ if (c.isRangeMiddle)
1005
+ return 'range-middle';
1006
+ if (c.isPreviewStart)
1007
+ return 'preview-start';
1008
+ if (c.isPreviewEnd)
1009
+ return 'preview-end';
1010
+ if (c.isPreviewMiddle)
1011
+ return 'preview-middle';
1012
+ if (c.isSelected)
1013
+ return 'selected';
1014
+ if (c.isToday)
1015
+ return 'today';
1016
+ return 'default';
1017
+ }, ...(ngDevMode ? [{ debugName: "cellState" }] : /* istanbul ignore next */ []));
1018
+ wrapperRange = computed(() => {
1019
+ const c = this.cell();
1020
+ if (c.isRangeStart && c.isRangeEnd)
1021
+ return 'single';
1022
+ if (c.isRangeStart)
1023
+ return 'start';
1024
+ if (c.isRangeEnd)
1025
+ return 'end';
1026
+ if (c.isRangeMiddle)
1027
+ return 'middle';
1028
+ return 'none';
1029
+ }, ...(ngDevMode ? [{ debugName: "wrapperRange" }] : /* istanbul ignore next */ []));
1030
+ variantResult = computed(() => cellVariants({
1031
+ view: this.view(),
1032
+ state: this.cellState(),
1033
+ outside: this.outside(),
1034
+ range: this.wrapperRange(),
1035
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
1036
+ wrapperClasses = computed(() => this.variantResult().wrapper(), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []));
1037
+ buttonClasses = computed(() => {
1038
+ const base = this.variantResult().button();
1039
+ const extra = this.cell().cssClasses;
1040
+ return extra ? `${base} ${extra}` : base;
1041
+ }, ...(ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []));
1042
+ onClick() {
1043
+ const c = this.cell();
1044
+ if (c.enabled)
1045
+ this.selected.emit(c);
1046
+ }
1047
+ onKeydown(event) {
1048
+ const navKeys = {
1049
+ ArrowLeft: 'left',
1050
+ ArrowRight: 'right',
1051
+ ArrowUp: 'up',
1052
+ ArrowDown: 'down',
1053
+ Home: 'home',
1054
+ End: 'end',
1055
+ PageUp: 'pageUp',
1056
+ PageDown: 'pageDown',
1057
+ };
1058
+ const direction = navKeys[event.key];
1059
+ if (direction) {
1060
+ event.preventDefault();
1061
+ this.keyNav.emit({ direction, shiftKey: event.shiftKey, cell: this.cell() });
1062
+ return;
1063
+ }
1064
+ if (event.key === 'Enter' || event.key === ' ') {
1065
+ event.preventDefault();
1066
+ const c = this.cell();
1067
+ if (c.enabled)
1068
+ this.selected.emit(c);
1069
+ }
1070
+ }
1071
+ onMouseEnter() {
1072
+ this.previewed.emit(this.cell());
1073
+ }
1074
+ onFocus() {
1075
+ this.focused.emit(this.cell());
1076
+ }
1077
+ /** Imperatively focuses this cell's button. */
1078
+ focusButton() {
1079
+ this.buttonRef()?.nativeElement.focus();
1080
+ }
1081
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1082
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CalendarCellComponent, isStandalone: true, selector: "tw-calendar-cell", inputs: { cell: { classPropertyName: "cell", publicName: "cell", isSignal: true, isRequired: true, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, outside: { classPropertyName: "outside", publicName: "outside", isSignal: true, isRequired: false, transformFunction: null }, tabindex: { classPropertyName: "tabindex", publicName: "tabindex", isSignal: true, isRequired: false, transformFunction: null }, cellTemplate: { classPropertyName: "cellTemplate", publicName: "cellTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected", focused: "focused", previewed: "previewed", keyNav: "keyNav" }, host: { attributes: { "role": "gridcell" }, properties: { "attr.data-state-today": "cell().isToday ? '' : null", "attr.data-state-selected": "cell().isSelected && !cell().isRangeStart && !cell().isRangeEnd && !cell().isRangeMiddle ? '' : null", "attr.data-state-range-start": "cell().isRangeStart ? '' : null", "attr.data-state-range-end": "cell().isRangeEnd ? '' : null", "attr.data-state-in-range": "cell().isRangeMiddle ? '' : null", "attr.data-state-range-preview-start": "cell().isPreviewStart ? '' : null", "attr.data-state-range-preview-end": "cell().isPreviewEnd ? '' : null", "attr.data-state-in-range-preview": "cell().isPreviewMiddle ? '' : null", "attr.data-state-invalid-preview": "cell().isInvalidPreview ? '' : null", "attr.data-state-invalid-flash": "cell().isInvalidFlash ? '' : null", "attr.data-state-disabled": "!cell().enabled ? '' : null", "attr.data-state-out-of-month": "outside() ? '' : null", "attr.data-state-weekend": "cell().isWeekend ? '' : null", "attr.data-state-focused": "tabindex() === 0 ? '' : null" } }, viewQueries: [{ propertyName: "buttonRef", first: true, predicate: ["cellButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
1083
+ <div [class]="wrapperClasses()">
1084
+ <button
1085
+ #cellButton
1086
+ type="button"
1087
+ [class]="buttonClasses()"
1088
+ [attr.aria-label]="cell().ariaLabel"
1089
+ [attr.aria-selected]="cell().isSelected || null"
1090
+ [attr.aria-disabled]="!cell().enabled || null"
1091
+ [attr.aria-current]="cell().isToday ? 'date' : null"
1092
+ [attr.tabindex]="tabindex()"
1093
+ [disabled]="!cell().enabled"
1094
+ (click)="onClick()"
1095
+ (keydown)="onKeydown($event)"
1096
+ (mouseenter)="onMouseEnter()"
1097
+ (focus)="onFocus()"
1098
+ >
1099
+ @if (cellTemplate()) {
1100
+ <ng-container
1101
+ [ngTemplateOutlet]="cellTemplate()!"
1102
+ [ngTemplateOutletContext]="{ $implicit: cell() }"
1103
+ />
1104
+ } @else {
1105
+ {{ cell().displayValue }}
1106
+ }
1107
+ </button>
1108
+ </div>
1109
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1110
+ }
1111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarCellComponent, decorators: [{
1112
+ type: Component,
1113
+ args: [{
1114
+ selector: 'tw-calendar-cell',
1115
+ imports: [NgTemplateOutlet],
1116
+ changeDetection: ChangeDetectionStrategy.OnPush,
1117
+ host: {
1118
+ role: 'gridcell',
1119
+ // Phase 4 — `data-state-*` styling contract (§34.5). Each attribute is `''`
1120
+ // when its boolean state is true and absent (`null`) otherwise so consumers
1121
+ // can target via `tw-calendar-cell[data-state-today]` selectors.
1122
+ '[attr.data-state-today]': "cell().isToday ? '' : null",
1123
+ '[attr.data-state-selected]': "cell().isSelected && !cell().isRangeStart && !cell().isRangeEnd && !cell().isRangeMiddle ? '' : null",
1124
+ '[attr.data-state-range-start]': "cell().isRangeStart ? '' : null",
1125
+ '[attr.data-state-range-end]': "cell().isRangeEnd ? '' : null",
1126
+ '[attr.data-state-in-range]': "cell().isRangeMiddle ? '' : null",
1127
+ '[attr.data-state-range-preview-start]': "cell().isPreviewStart ? '' : null",
1128
+ '[attr.data-state-range-preview-end]': "cell().isPreviewEnd ? '' : null",
1129
+ '[attr.data-state-in-range-preview]': "cell().isPreviewMiddle ? '' : null",
1130
+ '[attr.data-state-invalid-preview]': "cell().isInvalidPreview ? '' : null",
1131
+ '[attr.data-state-invalid-flash]': "cell().isInvalidFlash ? '' : null",
1132
+ '[attr.data-state-disabled]': "!cell().enabled ? '' : null",
1133
+ '[attr.data-state-out-of-month]': "outside() ? '' : null",
1134
+ '[attr.data-state-weekend]': "cell().isWeekend ? '' : null",
1135
+ '[attr.data-state-focused]': "tabindex() === 0 ? '' : null",
1136
+ },
1137
+ template: `
1138
+ <div [class]="wrapperClasses()">
1139
+ <button
1140
+ #cellButton
1141
+ type="button"
1142
+ [class]="buttonClasses()"
1143
+ [attr.aria-label]="cell().ariaLabel"
1144
+ [attr.aria-selected]="cell().isSelected || null"
1145
+ [attr.aria-disabled]="!cell().enabled || null"
1146
+ [attr.aria-current]="cell().isToday ? 'date' : null"
1147
+ [attr.tabindex]="tabindex()"
1148
+ [disabled]="!cell().enabled"
1149
+ (click)="onClick()"
1150
+ (keydown)="onKeydown($event)"
1151
+ (mouseenter)="onMouseEnter()"
1152
+ (focus)="onFocus()"
1153
+ >
1154
+ @if (cellTemplate()) {
1155
+ <ng-container
1156
+ [ngTemplateOutlet]="cellTemplate()!"
1157
+ [ngTemplateOutletContext]="{ $implicit: cell() }"
1158
+ />
1159
+ } @else {
1160
+ {{ cell().displayValue }}
1161
+ }
1162
+ </button>
1163
+ </div>
1164
+ `,
1165
+ }]
1166
+ }], propDecorators: { buttonRef: [{ type: i0.ViewChild, args: ['cellButton', { isSignal: true }] }], cell: [{ type: i0.Input, args: [{ isSignal: true, alias: "cell", required: true }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }], outside: [{ type: i0.Input, args: [{ isSignal: true, alias: "outside", required: false }] }], tabindex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabindex", required: false }] }], cellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplate", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }], focused: [{ type: i0.Output, args: ["focused"] }], previewed: [{ type: i0.Output, args: ["previewed"] }], keyNav: [{ type: i0.Output, args: ["keyNav"] }] } });
1167
+
1168
+ /**
1169
+ * Abstract base for the three calendar views. Holds shared inputs/outputs
1170
+ * (selection, preview, min/max, filters), cell-hit helpers, and roving-focus
1171
+ * machinery. Subclasses implement `cells`, `gridLabel`, `onKeyNav`, and
1172
+ * `getActiveCompareValue`.
1173
+ */
1174
+ class CalendarViewBase {
1175
+ dateAdapter = inject(DATE_ADAPTER);
1176
+ /** The date that anchors the grid being rendered. */
1177
+ activeDate = input.required(...(ngDevMode ? [{ debugName: "activeDate" }] : /* istanbul ignore next */ []));
1178
+ /** Current selection — scalar, array (multi), or range. */
1179
+ selected = input(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1180
+ /** Minimum selectable date. */
1181
+ minDate = input(null, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
1182
+ /** Maximum selectable date. */
1183
+ maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
1184
+ /** Per-date predicate — return `false` to disable. */
1185
+ dateFilter = input(null, ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
1186
+ /** Explicitly disabled dates — array (compared via `adapter.sameDate`) or predicate (returns `true` for disabled). */
1187
+ disabledDates = input(null, ...(ngDevMode ? [{ debugName: "disabledDates" }] : /* istanbul ignore next */ []));
1188
+ /** Days of the week to disable (0=Sun … 6=Sat). Empty array = no day-of-week disabling. */
1189
+ disabledDaysOfWeek = input([], ...(ngDevMode ? [{ debugName: "disabledDaysOfWeek" }] : /* istanbul ignore next */ []));
1190
+ /** Per-cell class override. */
1191
+ dateClass = input(null, ...(ngDevMode ? [{ debugName: "dateClass" }] : /* istanbul ignore next */ []));
1192
+ /** Hover-preview start (set while user is mid-range pick). */
1193
+ previewStart = input(null, ...(ngDevMode ? [{ debugName: "previewStart" }] : /* istanbul ignore next */ []));
1194
+ /** Hover-preview end. */
1195
+ previewEnd = input(null, ...(ngDevMode ? [{ debugName: "previewEnd" }] : /* istanbul ignore next */ []));
1196
+ /** Phase 6 — date that briefly flashes as invalid (rejected commit). Cleared by the orchestrator. */
1197
+ invalidFlashDate = input(null, ...(ngDevMode ? [{ debugName: "invalidFlashDate" }] : /* istanbul ignore next */ []));
1198
+ /** `true` when the parent calendar allows more than one cell to be selected at once. Drives `aria-multiselectable` on the grid host. */
1199
+ multiSelectable = input(false, ...(ngDevMode ? [{ debugName: "multiSelectable" }] : /* istanbul ignore next */ []));
1200
+ /** `true` when the parent calendar is in read-only mode. Drives `aria-readonly` on the grid host. */
1201
+ readonlyGrid = input(false, ...(ngDevMode ? [{ debugName: "readonlyGrid" }] : /* istanbul ignore next */ []));
1202
+ /** Optional cell-content override. */
1203
+ cellTemplate = input(null, ...(ngDevMode ? [{ debugName: "cellTemplate" }] : /* istanbul ignore next */ []));
1204
+ /** Fires when the user activates an enabled cell (click, Enter, or Space). Payload is the activated cell's date. */
1205
+ selectedChange = output();
1206
+ /** Fires when keyboard navigation or programmatic focus moves the roving cursor to a new cell. Payload is the new active date. */
1207
+ activeDateChange = output();
1208
+ /** Fires on pointer hover with the hovered date, and again with `null` when the pointer leaves the grid. The parent orchestrator drives any range-preview state from this stream. */
1209
+ previewChange = output();
1210
+ /** Cell that should own the roving-tabindex cursor. `null` means "use `activeDate`". */
1211
+ focusedCellValue = signal(null, ...(ngDevMode ? [{ debugName: "focusedCellValue" }] : /* istanbul ignore next */ []));
1212
+ /** Pending focus target — drained after the next render. */
1213
+ focusPending = signal(null, ...(ngDevMode ? [{ debugName: "focusPending" }] : /* istanbul ignore next */ []));
1214
+ constructor() {
1215
+ afterEveryRender(() => {
1216
+ const pending = this.focusPending();
1217
+ if (pending !== null) {
1218
+ const target = this.cellComponents().find((c) => c.cell().compareValue === pending);
1219
+ target?.focusButton();
1220
+ this.focusPending.set(null);
1221
+ }
1222
+ });
1223
+ }
1224
+ /** Today's date — memoized per view instance. */
1225
+ today = computed(() => this.dateAdapter.today(), ...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
1226
+ /** True when `date` is part of the committed selection. */
1227
+ isSelected(date) {
1228
+ const sel = this.selected();
1229
+ if (!sel)
1230
+ return false;
1231
+ if (Array.isArray(sel)) {
1232
+ return sel.some((d) => this.dateAdapter.sameDate(date, d));
1233
+ }
1234
+ if (this.isDateRange(sel)) {
1235
+ const r = sel;
1236
+ if (r.start && this.dateAdapter.sameDate(date, r.start))
1237
+ return true;
1238
+ if (r.end && this.dateAdapter.sameDate(date, r.end))
1239
+ return true;
1240
+ return false;
1241
+ }
1242
+ return this.dateAdapter.sameDate(date, sel);
1243
+ }
1244
+ isRangeStart(date) {
1245
+ const sel = this.selected();
1246
+ if (!sel || !this.isDateRange(sel))
1247
+ return false;
1248
+ return sel.start !== null && this.dateAdapter.sameDate(date, sel.start);
1249
+ }
1250
+ isRangeEnd(date) {
1251
+ const sel = this.selected();
1252
+ if (!sel || !this.isDateRange(sel))
1253
+ return false;
1254
+ return sel.end !== null && this.dateAdapter.sameDate(date, sel.end);
1255
+ }
1256
+ isRangeMiddle(date) {
1257
+ const sel = this.selected();
1258
+ if (!sel || !this.isDateRange(sel))
1259
+ return false;
1260
+ if (!sel.start || !sel.end)
1261
+ return false;
1262
+ return (isDateInRange(date, sel.start, sel.end, this.dateAdapter) &&
1263
+ !this.isRangeStart(date) &&
1264
+ !this.isRangeEnd(date));
1265
+ }
1266
+ isPreviewStart(date) {
1267
+ const s = this.previewStart();
1268
+ return s !== null && this.dateAdapter.sameDate(date, s);
1269
+ }
1270
+ isPreviewEnd(date) {
1271
+ const e = this.previewEnd();
1272
+ return e !== null && this.dateAdapter.sameDate(date, e);
1273
+ }
1274
+ isPreviewMiddle(date) {
1275
+ const s = this.previewStart();
1276
+ const e = this.previewEnd();
1277
+ if (!s || !e)
1278
+ return false;
1279
+ return (isDateInRange(date, s, e, this.dateAdapter) &&
1280
+ !this.isPreviewStart(date) &&
1281
+ !this.isPreviewEnd(date));
1282
+ }
1283
+ isDateRange(value) {
1284
+ return value !== null && typeof value === 'object' && 'start' in value && 'end' in value;
1285
+ }
1286
+ /** Routes a cell activation up to the parent. */
1287
+ onCellSelected(cell) {
1288
+ if (cell.enabled)
1289
+ this.selectedChange.emit(cell.value);
1290
+ }
1291
+ /** Routes hover events up to the parent (parent drives preview state). */
1292
+ onCellPreviewed(cell) {
1293
+ this.previewChange.emit(cell.value);
1294
+ }
1295
+ /** Clears the preview when the pointer leaves the grid. */
1296
+ onGridMouseLeave() {
1297
+ this.previewChange.emit(null);
1298
+ }
1299
+ /** Returns `true` when the cell owns the roving cursor. */
1300
+ isActiveCell(cell) {
1301
+ const focused = this.focusedCellValue();
1302
+ if (focused !== null)
1303
+ return cell.compareValue === focused;
1304
+ return this.getActiveCompareValue() === cell.compareValue;
1305
+ }
1306
+ /** Imperatively focuses the cell matching `compareValue` after the next render. */
1307
+ focusCell(compareValue) {
1308
+ this.focusedCellValue.set(compareValue);
1309
+ this.focusPending.set(compareValue);
1310
+ }
1311
+ /** Focuses the cell that currently owns the roving cursor. */
1312
+ focusActiveCell() {
1313
+ const target = this.focusedCellValue() ?? this.getActiveCompareValue();
1314
+ this.focusCell(target);
1315
+ }
1316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarViewBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1317
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: CalendarViewBase, isStandalone: true, inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, disabledDaysOfWeek: { classPropertyName: "disabledDaysOfWeek", publicName: "disabledDaysOfWeek", isSignal: true, isRequired: false, transformFunction: null }, dateClass: { classPropertyName: "dateClass", publicName: "dateClass", isSignal: true, isRequired: false, transformFunction: null }, previewStart: { classPropertyName: "previewStart", publicName: "previewStart", isSignal: true, isRequired: false, transformFunction: null }, previewEnd: { classPropertyName: "previewEnd", publicName: "previewEnd", isSignal: true, isRequired: false, transformFunction: null }, invalidFlashDate: { classPropertyName: "invalidFlashDate", publicName: "invalidFlashDate", isSignal: true, isRequired: false, transformFunction: null }, multiSelectable: { classPropertyName: "multiSelectable", publicName: "multiSelectable", isSignal: true, isRequired: false, transformFunction: null }, readonlyGrid: { classPropertyName: "readonlyGrid", publicName: "readonlyGrid", isSignal: true, isRequired: false, transformFunction: null }, cellTemplate: { classPropertyName: "cellTemplate", publicName: "cellTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", previewChange: "previewChange" }, ngImport: i0 });
1318
+ }
1319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarViewBase, decorators: [{
1320
+ type: Directive
1321
+ }], ctorParameters: () => [], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], disabledDaysOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDaysOfWeek", required: false }] }], dateClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateClass", required: false }] }], previewStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewStart", required: false }] }], previewEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewEnd", required: false }] }], invalidFlashDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidFlashDate", required: false }] }], multiSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelectable", required: false }] }], readonlyGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonlyGrid", required: false }] }], cellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplate", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], previewChange: [{ type: i0.Output, args: ["previewChange"] }] } });
1322
+
1323
+ const TOTAL_CELLS = DAYS_PER_WEEK * WEEKS_PER_MONTH;
1324
+ /**
1325
+ * Month view — renders a 7×6 grid of day cells, with leading/trailing days
1326
+ * from adjacent months marked as "outside".
1327
+ */
1328
+ class MonthViewComponent extends CalendarViewBase {
1329
+ view = 'day';
1330
+ cellComponents = viewChildren(CalendarCellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : /* istanbul ignore next */ []));
1331
+ /** Override first day of week (0=Sun, 1=Mon). */
1332
+ firstDayOfWeek = input(0, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
1333
+ /** Index within a multi-month row (0=left, 1+=right) — used by the orchestrator to disambiguate focus. */
1334
+ gridIndex = input(0, ...(ngDevMode ? [{ debugName: "gridIndex" }] : /* istanbul ignore next */ []));
1335
+ weekdayHeaders = computed(() => getWeekdayHeaders(this.dateAdapter, 'long', this.firstDayOfWeek()), ...(ngDevMode ? [{ debugName: "weekdayHeaders" }] : /* istanbul ignore next */ []));
1336
+ displayMonth = computed(() => this.dateAdapter.getMonth(this.activeDate()), ...(ngDevMode ? [{ debugName: "displayMonth" }] : /* istanbul ignore next */ []));
1337
+ displayYear = computed(() => this.dateAdapter.getYear(this.activeDate()), ...(ngDevMode ? [{ debugName: "displayYear" }] : /* istanbul ignore next */ []));
1338
+ cells = computed(() => {
1339
+ const active = this.activeDate();
1340
+ const firstOfMonth = getFirstDayOfMonth(active, this.dateAdapter);
1341
+ const firstDayOfWeek = this.firstDayOfWeek();
1342
+ const firstDayOfMonthWeekday = this.dateAdapter.getDayOfWeek(firstOfMonth);
1343
+ const offset = (firstDayOfMonthWeekday - firstDayOfWeek + DAYS_PER_WEEK) % DAYS_PER_WEEK;
1344
+ const startDate = this.dateAdapter.addDays(firstOfMonth, -offset);
1345
+ const today = this.today();
1346
+ const minDate = this.minDate();
1347
+ const maxDate = this.maxDate();
1348
+ const dateFilter = this.dateFilter();
1349
+ const disabledDates = this.disabledDates();
1350
+ const disabledDaysOfWeek = this.disabledDaysOfWeek();
1351
+ const dateClass = this.dateClass();
1352
+ const displayedMonth = this.displayMonth();
1353
+ const days = [];
1354
+ for (let i = 0; i < TOTAL_CELLS; i++) {
1355
+ const date = this.dateAdapter.addDays(startDate, i);
1356
+ const dayOfMonth = this.dateAdapter.getDate(date);
1357
+ const cellMonth = this.dateAdapter.getMonth(date);
1358
+ const cellYear = this.dateAdapter.getYear(date);
1359
+ const compareValue = cellYear * 10000 + cellMonth * 100 + dayOfMonth;
1360
+ const cell = createCalendarCell({
1361
+ value: date,
1362
+ displayValue: String(dayOfMonth),
1363
+ ariaLabel: this.dateAdapter.format(date, { dateTimeFormat: { dateStyle: 'full' } }),
1364
+ enabled: !isDateDisabled(date, minDate, maxDate, dateFilter, this.dateAdapter, disabledDates, disabledDaysOfWeek),
1365
+ cssClasses: dateClass ? dateClass(date, this.view) : '',
1366
+ compareValue,
1367
+ });
1368
+ cell.isToday = this.dateAdapter.sameDate(date, today);
1369
+ cell.isSelected = this.isSelected(date);
1370
+ cell.isRangeStart = this.isRangeStart(date);
1371
+ cell.isRangeEnd = this.isRangeEnd(date);
1372
+ cell.isRangeMiddle = this.isRangeMiddle(date);
1373
+ cell.isPreviewStart = this.isPreviewStart(date);
1374
+ cell.isPreviewEnd = this.isPreviewEnd(date);
1375
+ cell.isPreviewMiddle = this.isPreviewMiddle(date);
1376
+ cell.isOutOfMonth = cellMonth !== displayedMonth;
1377
+ cell.isWeekend = isWeekend(date, this.dateAdapter);
1378
+ const flashDate = this.invalidFlashDate();
1379
+ cell.isInvalidFlash = flashDate !== null && this.dateAdapter.sameDate(date, flashDate);
1380
+ days.push(cell);
1381
+ }
1382
+ return createGrid(days, DAYS_PER_WEEK);
1383
+ }, ...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
1384
+ gridLabel = computed(() => {
1385
+ const months = this.dateAdapter.getMonthNames('long');
1386
+ return `${months[this.displayMonth()]} ${this.displayYear()}`;
1387
+ }, ...(ngDevMode ? [{ debugName: "gridLabel" }] : /* istanbul ignore next */ []));
1388
+ getActiveCompareValue() {
1389
+ const d = this.activeDate();
1390
+ return (this.dateAdapter.getYear(d) * 10000 +
1391
+ this.dateAdapter.getMonth(d) * 100 +
1392
+ this.dateAdapter.getDate(d));
1393
+ }
1394
+ /** True when the cell's date sits in a neighbouring month (leading/trailing). */
1395
+ isOutsideMonth(cell) {
1396
+ return this.dateAdapter.getMonth(cell.value) !== this.displayMonth();
1397
+ }
1398
+ onKeyNav(event) {
1399
+ const currentDate = event.cell.value;
1400
+ let newDate = null;
1401
+ switch (event.direction) {
1402
+ case 'left':
1403
+ newDate = this.dateAdapter.addDays(currentDate, -1);
1404
+ break;
1405
+ case 'right':
1406
+ newDate = this.dateAdapter.addDays(currentDate, 1);
1407
+ break;
1408
+ case 'up':
1409
+ newDate = this.dateAdapter.addDays(currentDate, -DAYS_PER_WEEK);
1410
+ break;
1411
+ case 'down':
1412
+ newDate = this.dateAdapter.addDays(currentDate, DAYS_PER_WEEK);
1413
+ break;
1414
+ case 'home': {
1415
+ const dow = this.dateAdapter.getDayOfWeek(currentDate);
1416
+ const first = this.firstDayOfWeek();
1417
+ const diff = (dow - first + DAYS_PER_WEEK) % DAYS_PER_WEEK;
1418
+ newDate = this.dateAdapter.addDays(currentDate, -diff);
1419
+ break;
1420
+ }
1421
+ case 'end': {
1422
+ const dow = this.dateAdapter.getDayOfWeek(currentDate);
1423
+ const first = this.firstDayOfWeek();
1424
+ const daysUntilEnd = (6 - dow + first + DAYS_PER_WEEK) % DAYS_PER_WEEK;
1425
+ newDate = this.dateAdapter.addDays(currentDate, daysUntilEnd);
1426
+ break;
1427
+ }
1428
+ case 'pageUp':
1429
+ newDate = event.shiftKey
1430
+ ? this.dateAdapter.addYears(currentDate, -1)
1431
+ : this.dateAdapter.addMonths(currentDate, -1);
1432
+ break;
1433
+ case 'pageDown':
1434
+ newDate = event.shiftKey
1435
+ ? this.dateAdapter.addYears(currentDate, 1)
1436
+ : this.dateAdapter.addMonths(currentDate, 1);
1437
+ break;
1438
+ }
1439
+ if (newDate) {
1440
+ const year = this.dateAdapter.getYear(newDate);
1441
+ const month = this.dateAdapter.getMonth(newDate);
1442
+ const day = this.dateAdapter.getDate(newDate);
1443
+ const compareValue = year * 10000 + month * 100 + day;
1444
+ this.focusCell(compareValue);
1445
+ this.activeDateChange.emit(newDate);
1446
+ }
1447
+ }
1448
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MonthViewComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1449
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: MonthViewComponent, isStandalone: true, selector: "tw-calendar-month-view", inputs: { firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, gridIndex: { classPropertyName: "gridIndex", publicName: "gridIndex", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "cellComponents", predicate: CalendarCellComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
1450
+ <div
1451
+ role="grid"
1452
+ [attr.aria-label]="gridLabel()"
1453
+ [attr.aria-multiselectable]="multiSelectable() || null"
1454
+ [attr.aria-readonly]="readonlyGrid() || null"
1455
+ (mouseleave)="onGridMouseLeave()"
1456
+ >
1457
+ <!-- gap-0: weekday headers align column-by-column with the day grid below, which itself must be gap-0 for contiguous range backgrounds. -->
1458
+ <div role="row" class="grid grid-cols-7 gap-0">
1459
+ @for (header of weekdayHeaders(); track $index) {
1460
+ <div
1461
+ role="columnheader"
1462
+ class="flex items-center justify-center h-9 text-2xs font-medium text-fg-muted"
1463
+ [attr.aria-label]="header.label"
1464
+ >
1465
+ {{ header.narrow }}
1466
+ </div>
1467
+ }
1468
+ </div>
1469
+ @for (row of cells(); track $index) {
1470
+ <!-- gap-0: cells must touch so range-selection wrapper backgrounds (bg-primary-100 + rounded-l/r-full) form a continuous bar across adjacent days. -->
1471
+ <div role="row" class="grid grid-cols-7 gap-0">
1472
+ @for (cell of row; track cell.compareValue) {
1473
+ <tw-calendar-cell
1474
+ [cell]="cell"
1475
+ [view]="view"
1476
+ [outside]="isOutsideMonth(cell)"
1477
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1478
+ [cellTemplate]="cellTemplate()"
1479
+ [attr.data-compare-value]="cell.compareValue"
1480
+ (selected)="onCellSelected($event)"
1481
+ (previewed)="onCellPreviewed($event)"
1482
+ (keyNav)="onKeyNav($event)"
1483
+ />
1484
+ }
1485
+ </div>
1486
+ }
1487
+ </div>
1488
+ `, isInline: true, dependencies: [{ kind: "component", type: CalendarCellComponent, selector: "tw-calendar-cell", inputs: ["cell", "view", "outside", "tabindex", "cellTemplate"], outputs: ["selected", "focused", "previewed", "keyNav"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1489
+ }
1490
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MonthViewComponent, decorators: [{
1491
+ type: Component,
1492
+ args: [{
1493
+ selector: 'tw-calendar-month-view',
1494
+ imports: [CalendarCellComponent],
1495
+ changeDetection: ChangeDetectionStrategy.OnPush,
1496
+ host: { class: 'block' },
1497
+ template: `
1498
+ <div
1499
+ role="grid"
1500
+ [attr.aria-label]="gridLabel()"
1501
+ [attr.aria-multiselectable]="multiSelectable() || null"
1502
+ [attr.aria-readonly]="readonlyGrid() || null"
1503
+ (mouseleave)="onGridMouseLeave()"
1504
+ >
1505
+ <!-- gap-0: weekday headers align column-by-column with the day grid below, which itself must be gap-0 for contiguous range backgrounds. -->
1506
+ <div role="row" class="grid grid-cols-7 gap-0">
1507
+ @for (header of weekdayHeaders(); track $index) {
1508
+ <div
1509
+ role="columnheader"
1510
+ class="flex items-center justify-center h-9 text-2xs font-medium text-fg-muted"
1511
+ [attr.aria-label]="header.label"
1512
+ >
1513
+ {{ header.narrow }}
1514
+ </div>
1515
+ }
1516
+ </div>
1517
+ @for (row of cells(); track $index) {
1518
+ <!-- gap-0: cells must touch so range-selection wrapper backgrounds (bg-primary-100 + rounded-l/r-full) form a continuous bar across adjacent days. -->
1519
+ <div role="row" class="grid grid-cols-7 gap-0">
1520
+ @for (cell of row; track cell.compareValue) {
1521
+ <tw-calendar-cell
1522
+ [cell]="cell"
1523
+ [view]="view"
1524
+ [outside]="isOutsideMonth(cell)"
1525
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1526
+ [cellTemplate]="cellTemplate()"
1527
+ [attr.data-compare-value]="cell.compareValue"
1528
+ (selected)="onCellSelected($event)"
1529
+ (previewed)="onCellPreviewed($event)"
1530
+ (keyNav)="onKeyNav($event)"
1531
+ />
1532
+ }
1533
+ </div>
1534
+ }
1535
+ </div>
1536
+ `,
1537
+ }]
1538
+ }], propDecorators: { cellComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => CalendarCellComponent), { isSignal: true }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], gridIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridIndex", required: false }] }] } });
1539
+
1540
+ const MONTHS_PER_YEAR = 12;
1541
+ /**
1542
+ * Year view — 4×3 grid of month cells inside the active year.
1543
+ */
1544
+ class YearViewComponent extends CalendarViewBase {
1545
+ view = 'month';
1546
+ cellComponents = viewChildren(CalendarCellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : /* istanbul ignore next */ []));
1547
+ displayYear = computed(() => this.dateAdapter.getYear(this.activeDate()), ...(ngDevMode ? [{ debugName: "displayYear" }] : /* istanbul ignore next */ []));
1548
+ cells = computed(() => {
1549
+ const year = this.displayYear();
1550
+ const monthNames = this.dateAdapter.getMonthNames('short');
1551
+ const today = this.today();
1552
+ const todayYear = this.dateAdapter.getYear(today);
1553
+ const todayMonth = this.dateAdapter.getMonth(today);
1554
+ const minDate = this.minDate();
1555
+ const maxDate = this.maxDate();
1556
+ const dateClass = this.dateClass();
1557
+ const months = [];
1558
+ for (let m = 0; m < MONTHS_PER_YEAR; m++) {
1559
+ const date = this.dateAdapter.create(year, m + 1, 1);
1560
+ const name = monthNames[m] ?? '';
1561
+ const cell = createCalendarCell({
1562
+ value: date,
1563
+ displayValue: name,
1564
+ ariaLabel: `${name} ${year}`,
1565
+ enabled: !isMonthDisabled(year, m, minDate, maxDate, this.dateAdapter),
1566
+ cssClasses: dateClass ? dateClass(date, this.view) : '',
1567
+ compareValue: m,
1568
+ });
1569
+ cell.isToday = year === todayYear && m === todayMonth;
1570
+ cell.isSelected = this.isMonthSelected(year, m);
1571
+ cell.isRangeStart = this.isMonthRangeStart(year, m);
1572
+ cell.isRangeEnd = this.isMonthRangeEnd(year, m);
1573
+ cell.isRangeMiddle = this.isMonthRangeMiddle(year, m);
1574
+ cell.isPreviewStart = this.isMonthPreviewStart(year, m);
1575
+ cell.isPreviewEnd = this.isMonthPreviewEnd(year, m);
1576
+ cell.isPreviewMiddle = this.isMonthPreviewMiddle(year, m);
1577
+ months.push(cell);
1578
+ }
1579
+ return createGrid(months, MONTHS_PER_ROW);
1580
+ }, ...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
1581
+ gridLabel = computed(() => `${this.displayYear()}`, ...(ngDevMode ? [{ debugName: "gridLabel" }] : /* istanbul ignore next */ []));
1582
+ getActiveCompareValue() {
1583
+ return this.dateAdapter.getMonth(this.activeDate());
1584
+ }
1585
+ onKeyNav(event) {
1586
+ const currentMonth = this.dateAdapter.getMonth(event.cell.value);
1587
+ const year = this.displayYear();
1588
+ let newDate = null;
1589
+ switch (event.direction) {
1590
+ case 'left':
1591
+ if (currentMonth > 0) {
1592
+ newDate = this.dateAdapter.addMonths(event.cell.value, -1);
1593
+ this.focusCell(currentMonth - 1);
1594
+ }
1595
+ break;
1596
+ case 'right':
1597
+ if (currentMonth < 11) {
1598
+ newDate = this.dateAdapter.addMonths(event.cell.value, 1);
1599
+ this.focusCell(currentMonth + 1);
1600
+ }
1601
+ break;
1602
+ case 'up':
1603
+ if (currentMonth >= MONTHS_PER_ROW) {
1604
+ newDate = this.dateAdapter.addMonths(event.cell.value, -MONTHS_PER_ROW);
1605
+ this.focusCell(currentMonth - MONTHS_PER_ROW);
1606
+ }
1607
+ break;
1608
+ case 'down':
1609
+ if (currentMonth < MONTHS_PER_YEAR - MONTHS_PER_ROW) {
1610
+ newDate = this.dateAdapter.addMonths(event.cell.value, MONTHS_PER_ROW);
1611
+ this.focusCell(currentMonth + MONTHS_PER_ROW);
1612
+ }
1613
+ break;
1614
+ case 'home':
1615
+ newDate = this.dateAdapter.create(year, 1, 1);
1616
+ this.focusCell(0);
1617
+ break;
1618
+ case 'end':
1619
+ newDate = this.dateAdapter.create(year, 12, 1);
1620
+ this.focusCell(11);
1621
+ break;
1622
+ case 'pageUp':
1623
+ newDate = this.dateAdapter.addYears(event.cell.value, event.shiftKey ? -10 : -1);
1624
+ break;
1625
+ case 'pageDown':
1626
+ newDate = this.dateAdapter.addYears(event.cell.value, event.shiftKey ? 10 : 1);
1627
+ break;
1628
+ }
1629
+ if (newDate)
1630
+ this.activeDateChange.emit(newDate);
1631
+ }
1632
+ isMonthSelected(year, month) {
1633
+ const sel = this.selected();
1634
+ if (!sel)
1635
+ return false;
1636
+ if (Array.isArray(sel)) {
1637
+ return sel.some((d) => this.dateAdapter.getYear(d) === year && this.dateAdapter.getMonth(d) === month);
1638
+ }
1639
+ if (!this.isDateRange(sel)) {
1640
+ return this.dateAdapter.getYear(sel) === year && this.dateAdapter.getMonth(sel) === month;
1641
+ }
1642
+ const r = sel;
1643
+ if (r.start && this.dateAdapter.getYear(r.start) === year && this.dateAdapter.getMonth(r.start) === month) {
1644
+ return true;
1645
+ }
1646
+ if (r.end && this.dateAdapter.getYear(r.end) === year && this.dateAdapter.getMonth(r.end) === month) {
1647
+ return true;
1648
+ }
1649
+ return false;
1650
+ }
1651
+ isMonthRangeStart(year, month) {
1652
+ const sel = this.selected();
1653
+ if (!sel || !this.isDateRange(sel) || !sel.start)
1654
+ return false;
1655
+ return this.dateAdapter.getYear(sel.start) === year && this.dateAdapter.getMonth(sel.start) === month;
1656
+ }
1657
+ isMonthRangeEnd(year, month) {
1658
+ const sel = this.selected();
1659
+ if (!sel || !this.isDateRange(sel) || !sel.end)
1660
+ return false;
1661
+ return this.dateAdapter.getYear(sel.end) === year && this.dateAdapter.getMonth(sel.end) === month;
1662
+ }
1663
+ isMonthRangeMiddle(year, month) {
1664
+ const sel = this.selected();
1665
+ if (!sel || !this.isDateRange(sel) || !sel.start || !sel.end)
1666
+ return false;
1667
+ const monthDate = this.dateAdapter.create(year, month + 1, 1);
1668
+ const startMonth = this.dateAdapter.create(this.dateAdapter.getYear(sel.start), this.dateAdapter.getMonth(sel.start) + 1, 1);
1669
+ const endMonth = this.dateAdapter.create(this.dateAdapter.getYear(sel.end), this.dateAdapter.getMonth(sel.end) + 1, 1);
1670
+ return (this.dateAdapter.compare(monthDate, startMonth) > 0 &&
1671
+ this.dateAdapter.compare(monthDate, endMonth) < 0);
1672
+ }
1673
+ isMonthPreviewStart(year, month) {
1674
+ const s = this.previewStart();
1675
+ if (!s)
1676
+ return false;
1677
+ return this.dateAdapter.getYear(s) === year && this.dateAdapter.getMonth(s) === month;
1678
+ }
1679
+ isMonthPreviewEnd(year, month) {
1680
+ const e = this.previewEnd();
1681
+ if (!e)
1682
+ return false;
1683
+ return this.dateAdapter.getYear(e) === year && this.dateAdapter.getMonth(e) === month;
1684
+ }
1685
+ isMonthPreviewMiddle(year, month) {
1686
+ const s = this.previewStart();
1687
+ const e = this.previewEnd();
1688
+ if (!s || !e)
1689
+ return false;
1690
+ const monthDate = this.dateAdapter.create(year, month + 1, 1);
1691
+ const startMonth = this.dateAdapter.create(this.dateAdapter.getYear(s), this.dateAdapter.getMonth(s) + 1, 1);
1692
+ const endMonth = this.dateAdapter.create(this.dateAdapter.getYear(e), this.dateAdapter.getMonth(e) + 1, 1);
1693
+ return (this.dateAdapter.compare(monthDate, startMonth) > 0 &&
1694
+ this.dateAdapter.compare(monthDate, endMonth) < 0);
1695
+ }
1696
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: YearViewComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1697
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: YearViewComponent, isStandalone: true, selector: "tw-calendar-year-view", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "cellComponents", predicate: CalendarCellComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
1698
+ <div
1699
+ role="grid"
1700
+ [attr.aria-label]="gridLabel()"
1701
+ [attr.aria-multiselectable]="multiSelectable() || null"
1702
+ [attr.aria-readonly]="readonlyGrid() || null"
1703
+ (mouseleave)="onGridMouseLeave()"
1704
+ >
1705
+ @for (row of cells(); track $index) {
1706
+ <div role="row" class="grid grid-cols-4 gap-1">
1707
+ @for (cell of row; track cell.compareValue) {
1708
+ <tw-calendar-cell
1709
+ [cell]="cell"
1710
+ [view]="view"
1711
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1712
+ [cellTemplate]="cellTemplate()"
1713
+ [attr.data-compare-value]="cell.compareValue"
1714
+ (selected)="onCellSelected($event)"
1715
+ (previewed)="onCellPreviewed($event)"
1716
+ (keyNav)="onKeyNav($event)"
1717
+ />
1718
+ }
1719
+ </div>
1720
+ }
1721
+ </div>
1722
+ `, isInline: true, dependencies: [{ kind: "component", type: CalendarCellComponent, selector: "tw-calendar-cell", inputs: ["cell", "view", "outside", "tabindex", "cellTemplate"], outputs: ["selected", "focused", "previewed", "keyNav"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1723
+ }
1724
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: YearViewComponent, decorators: [{
1725
+ type: Component,
1726
+ args: [{
1727
+ selector: 'tw-calendar-year-view',
1728
+ imports: [CalendarCellComponent],
1729
+ changeDetection: ChangeDetectionStrategy.OnPush,
1730
+ host: { class: 'block' },
1731
+ template: `
1732
+ <div
1733
+ role="grid"
1734
+ [attr.aria-label]="gridLabel()"
1735
+ [attr.aria-multiselectable]="multiSelectable() || null"
1736
+ [attr.aria-readonly]="readonlyGrid() || null"
1737
+ (mouseleave)="onGridMouseLeave()"
1738
+ >
1739
+ @for (row of cells(); track $index) {
1740
+ <div role="row" class="grid grid-cols-4 gap-1">
1741
+ @for (cell of row; track cell.compareValue) {
1742
+ <tw-calendar-cell
1743
+ [cell]="cell"
1744
+ [view]="view"
1745
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1746
+ [cellTemplate]="cellTemplate()"
1747
+ [attr.data-compare-value]="cell.compareValue"
1748
+ (selected)="onCellSelected($event)"
1749
+ (previewed)="onCellPreviewed($event)"
1750
+ (keyNav)="onKeyNav($event)"
1751
+ />
1752
+ }
1753
+ </div>
1754
+ }
1755
+ </div>
1756
+ `,
1757
+ }]
1758
+ }], propDecorators: { cellComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => CalendarCellComponent), { isSignal: true }] }] } });
1759
+
1760
+ /** Default years per page — kept until Phase 7 introduces the `yearsPerPage` input. */
1761
+ const yearsPerPage = YEARS_PER_PAGE;
1762
+ /**
1763
+ * Year view — 4×6 grid (24 years by default). A user clicks a year to drill
1764
+ * down to the month-of-year view. In the spec vocabulary this is the
1765
+ * `'year'` view state (§7.4, §22).
1766
+ */
1767
+ class YearsViewComponent extends CalendarViewBase {
1768
+ view = 'year';
1769
+ cellComponents = viewChildren(CalendarCellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : /* istanbul ignore next */ []));
1770
+ startYear = computed(() => getMultiYearStartingYear(this.dateAdapter.getYear(this.activeDate()), YEARS_PER_PAGE), ...(ngDevMode ? [{ debugName: "startYear" }] : /* istanbul ignore next */ []));
1771
+ endYear = computed(() => this.startYear() + YEARS_PER_PAGE - 1, ...(ngDevMode ? [{ debugName: "endYear" }] : /* istanbul ignore next */ []));
1772
+ cells = computed(() => {
1773
+ const start = this.startYear();
1774
+ const today = this.today();
1775
+ const todayYear = this.dateAdapter.getYear(today);
1776
+ const minDate = this.minDate();
1777
+ const maxDate = this.maxDate();
1778
+ const dateClass = this.dateClass();
1779
+ const years = [];
1780
+ for (let i = 0; i < YEARS_PER_PAGE; i++) {
1781
+ const year = start + i;
1782
+ const date = this.dateAdapter.create(year, 1, 1);
1783
+ const label = this.dateAdapter.getYearName(date);
1784
+ const cell = createCalendarCell({
1785
+ value: date,
1786
+ displayValue: label,
1787
+ ariaLabel: label,
1788
+ enabled: !isYearDisabled(year, minDate, maxDate, this.dateAdapter),
1789
+ cssClasses: dateClass ? dateClass(date, this.view) : '',
1790
+ compareValue: year,
1791
+ });
1792
+ cell.isToday = year === todayYear;
1793
+ cell.isSelected = this.isYearSelected(year);
1794
+ cell.isRangeStart = this.isYearRangeStart(year);
1795
+ cell.isRangeEnd = this.isYearRangeEnd(year);
1796
+ cell.isRangeMiddle = this.isYearRangeMiddle(year);
1797
+ cell.isPreviewStart = this.isYearPreviewStart(year);
1798
+ cell.isPreviewEnd = this.isYearPreviewEnd(year);
1799
+ cell.isPreviewMiddle = this.isYearPreviewMiddle(year);
1800
+ years.push(cell);
1801
+ }
1802
+ return createGrid(years, YEARS_PER_ROW);
1803
+ }, ...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
1804
+ gridLabel = computed(() => `${this.startYear()} – ${this.endYear()}`, ...(ngDevMode ? [{ debugName: "gridLabel" }] : /* istanbul ignore next */ []));
1805
+ getActiveCompareValue() {
1806
+ return this.dateAdapter.getYear(this.activeDate());
1807
+ }
1808
+ onKeyNav(event) {
1809
+ const currentYear = this.dateAdapter.getYear(event.cell.value);
1810
+ const start = this.startYear();
1811
+ const end = this.endYear();
1812
+ const indexInPage = currentYear - start;
1813
+ let newDate = null;
1814
+ switch (event.direction) {
1815
+ case 'left':
1816
+ if (currentYear > start) {
1817
+ newDate = this.dateAdapter.addYears(event.cell.value, -1);
1818
+ this.focusCell(currentYear - 1);
1819
+ }
1820
+ break;
1821
+ case 'right':
1822
+ if (currentYear < end) {
1823
+ newDate = this.dateAdapter.addYears(event.cell.value, 1);
1824
+ this.focusCell(currentYear + 1);
1825
+ }
1826
+ break;
1827
+ case 'up':
1828
+ if (indexInPage >= YEARS_PER_ROW) {
1829
+ newDate = this.dateAdapter.addYears(event.cell.value, -YEARS_PER_ROW);
1830
+ this.focusCell(currentYear - YEARS_PER_ROW);
1831
+ }
1832
+ break;
1833
+ case 'down':
1834
+ if (indexInPage < YEARS_PER_PAGE - YEARS_PER_ROW) {
1835
+ newDate = this.dateAdapter.addYears(event.cell.value, YEARS_PER_ROW);
1836
+ this.focusCell(currentYear + YEARS_PER_ROW);
1837
+ }
1838
+ break;
1839
+ case 'home':
1840
+ newDate = this.dateAdapter.create(start, 1, 1);
1841
+ this.focusCell(start);
1842
+ break;
1843
+ case 'end':
1844
+ newDate = this.dateAdapter.create(end, 1, 1);
1845
+ this.focusCell(end);
1846
+ break;
1847
+ case 'pageUp':
1848
+ newDate = this.dateAdapter.addYears(event.cell.value, event.shiftKey ? -YEARS_PER_PAGE * 10 : -YEARS_PER_PAGE);
1849
+ break;
1850
+ case 'pageDown':
1851
+ newDate = this.dateAdapter.addYears(event.cell.value, event.shiftKey ? YEARS_PER_PAGE * 10 : YEARS_PER_PAGE);
1852
+ break;
1853
+ }
1854
+ if (newDate)
1855
+ this.activeDateChange.emit(newDate);
1856
+ }
1857
+ isYearSelected(year) {
1858
+ const sel = this.selected();
1859
+ if (!sel)
1860
+ return false;
1861
+ if (Array.isArray(sel)) {
1862
+ return sel.some((d) => this.dateAdapter.getYear(d) === year);
1863
+ }
1864
+ if (!this.isDateRange(sel)) {
1865
+ return this.dateAdapter.getYear(sel) === year;
1866
+ }
1867
+ const r = sel;
1868
+ if (r.start && this.dateAdapter.getYear(r.start) === year)
1869
+ return true;
1870
+ if (r.end && this.dateAdapter.getYear(r.end) === year)
1871
+ return true;
1872
+ return false;
1873
+ }
1874
+ isYearRangeStart(year) {
1875
+ const sel = this.selected();
1876
+ if (!sel || !this.isDateRange(sel) || !sel.start)
1877
+ return false;
1878
+ return this.dateAdapter.getYear(sel.start) === year;
1879
+ }
1880
+ isYearRangeEnd(year) {
1881
+ const sel = this.selected();
1882
+ if (!sel || !this.isDateRange(sel) || !sel.end)
1883
+ return false;
1884
+ return this.dateAdapter.getYear(sel.end) === year;
1885
+ }
1886
+ isYearRangeMiddle(year) {
1887
+ const sel = this.selected();
1888
+ if (!sel || !this.isDateRange(sel) || !sel.start || !sel.end)
1889
+ return false;
1890
+ const startYear = this.dateAdapter.getYear(sel.start);
1891
+ const endYear = this.dateAdapter.getYear(sel.end);
1892
+ return year > startYear && year < endYear;
1893
+ }
1894
+ isYearPreviewStart(year) {
1895
+ const s = this.previewStart();
1896
+ return s !== null && this.dateAdapter.getYear(s) === year;
1897
+ }
1898
+ isYearPreviewEnd(year) {
1899
+ const e = this.previewEnd();
1900
+ return e !== null && this.dateAdapter.getYear(e) === year;
1901
+ }
1902
+ isYearPreviewMiddle(year) {
1903
+ const s = this.previewStart();
1904
+ const e = this.previewEnd();
1905
+ if (!s || !e)
1906
+ return false;
1907
+ return year > this.dateAdapter.getYear(s) && year < this.dateAdapter.getYear(e);
1908
+ }
1909
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: YearsViewComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1910
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: YearsViewComponent, isStandalone: true, selector: "tw-calendar-years-view", host: { classAttribute: "block" }, viewQueries: [{ propertyName: "cellComponents", predicate: CalendarCellComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
1911
+ <div
1912
+ role="grid"
1913
+ [attr.aria-label]="gridLabel()"
1914
+ [attr.aria-multiselectable]="multiSelectable() || null"
1915
+ [attr.aria-readonly]="readonlyGrid() || null"
1916
+ (mouseleave)="onGridMouseLeave()"
1917
+ >
1918
+ @for (row of cells(); track $index) {
1919
+ <div role="row" class="grid grid-cols-4 gap-1">
1920
+ @for (cell of row; track cell.compareValue) {
1921
+ <tw-calendar-cell
1922
+ [cell]="cell"
1923
+ [view]="view"
1924
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1925
+ [cellTemplate]="cellTemplate()"
1926
+ [attr.data-compare-value]="cell.compareValue"
1927
+ (selected)="onCellSelected($event)"
1928
+ (previewed)="onCellPreviewed($event)"
1929
+ (keyNav)="onKeyNav($event)"
1930
+ />
1931
+ }
1932
+ </div>
1933
+ }
1934
+ </div>
1935
+ `, isInline: true, dependencies: [{ kind: "component", type: CalendarCellComponent, selector: "tw-calendar-cell", inputs: ["cell", "view", "outside", "tabindex", "cellTemplate"], outputs: ["selected", "focused", "previewed", "keyNav"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1936
+ }
1937
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: YearsViewComponent, decorators: [{
1938
+ type: Component,
1939
+ args: [{
1940
+ selector: 'tw-calendar-years-view',
1941
+ imports: [CalendarCellComponent],
1942
+ changeDetection: ChangeDetectionStrategy.OnPush,
1943
+ host: { class: 'block' },
1944
+ template: `
1945
+ <div
1946
+ role="grid"
1947
+ [attr.aria-label]="gridLabel()"
1948
+ [attr.aria-multiselectable]="multiSelectable() || null"
1949
+ [attr.aria-readonly]="readonlyGrid() || null"
1950
+ (mouseleave)="onGridMouseLeave()"
1951
+ >
1952
+ @for (row of cells(); track $index) {
1953
+ <div role="row" class="grid grid-cols-4 gap-1">
1954
+ @for (cell of row; track cell.compareValue) {
1955
+ <tw-calendar-cell
1956
+ [cell]="cell"
1957
+ [view]="view"
1958
+ [tabindex]="isActiveCell(cell) ? 0 : -1"
1959
+ [cellTemplate]="cellTemplate()"
1960
+ [attr.data-compare-value]="cell.compareValue"
1961
+ (selected)="onCellSelected($event)"
1962
+ (previewed)="onCellPreviewed($event)"
1963
+ (keyNav)="onKeyNav($event)"
1964
+ />
1965
+ }
1966
+ </div>
1967
+ }
1968
+ </div>
1969
+ `,
1970
+ }]
1971
+ }], propDecorators: { cellComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => CalendarCellComponent), { isSignal: true }] }] } });
1972
+
1973
+ /**
1974
+ * Pluggable selection behaviour. Provide via `CALENDAR_SELECTION_STRATEGY` to
1975
+ * customise how clicks mutate the calendar's `selected` value.
1976
+ *
1977
+ * `D` is the date type understood by the active `DateAdapter`.
1978
+ * `S` is the shape of the `selected` value — `D | null` for single,
1979
+ * `DateRange<D> | null` for range / week, `readonly D[]` for multi.
1980
+ */
1981
+ class CalendarSelectionStrategy {
1982
+ }
1983
+ /** Injection token for the active selection strategy. */
1984
+ const CALENDAR_SELECTION_STRATEGY = new InjectionToken('tw-calendar/SelectionStrategy');
1985
+
1986
+ /**
1987
+ * Single-date selection. Every click commits a new selection immediately.
1988
+ * No preview range. `selected` has shape `D | null`.
1989
+ */
1990
+ class SingleSelectionStrategy extends CalendarSelectionStrategy {
1991
+ dateAdapter = inject(DATE_ADAPTER);
1992
+ select(date, _current) {
1993
+ return { selection: date, isComplete: true };
1994
+ }
1995
+ createPreview(_active, _current) {
1996
+ return null;
1997
+ }
1998
+ isSelected(date, current) {
1999
+ if (!current)
2000
+ return false;
2001
+ return this.dateAdapter.sameDate(date, current);
2002
+ }
2003
+ isRangeStart(_date, _current) {
2004
+ return false;
2005
+ }
2006
+ isRangeEnd(_date, _current) {
2007
+ return false;
2008
+ }
2009
+ isRangeMiddle(_date, _current) {
2010
+ return false;
2011
+ }
2012
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SingleSelectionStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2013
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SingleSelectionStrategy });
2014
+ }
2015
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SingleSelectionStrategy, decorators: [{
2016
+ type: Injectable
2017
+ }] });
2018
+
2019
+ /**
2020
+ * Date-range selection.
2021
+ *
2022
+ * - First click sets the start (selection incomplete).
2023
+ * - Second click sets the end (selection complete).
2024
+ * - If the second date is before the first, endpoints are swapped.
2025
+ * - Hovering after the first click produces a preview range.
2026
+ *
2027
+ * `selected` has shape `DateRange<D> | null`.
2028
+ */
2029
+ class RangeSelectionStrategy extends CalendarSelectionStrategy {
2030
+ dateAdapter = inject(DATE_ADAPTER);
2031
+ select(date, current) {
2032
+ // No start yet, or the previous range is already complete — start over.
2033
+ if (!current || (current.start && current.end)) {
2034
+ return { selection: { start: date, end: null }, isComplete: false };
2035
+ }
2036
+ const start = current.start;
2037
+ let rangeStart;
2038
+ let rangeEnd;
2039
+ if (this.dateAdapter.compare(date, start) < 0) {
2040
+ rangeStart = date;
2041
+ rangeEnd = start;
2042
+ }
2043
+ else {
2044
+ rangeStart = start;
2045
+ rangeEnd = date;
2046
+ }
2047
+ return { selection: { start: rangeStart, end: rangeEnd }, isComplete: true };
2048
+ }
2049
+ createPreview(active, current) {
2050
+ if (!active || !current?.start || current.end)
2051
+ return null;
2052
+ const s = current.start;
2053
+ if (this.dateAdapter.compare(active, s) < 0) {
2054
+ return { start: active, end: s };
2055
+ }
2056
+ return { start: s, end: active };
2057
+ }
2058
+ isSelected(date, current) {
2059
+ if (!current)
2060
+ return false;
2061
+ if (current.start && this.dateAdapter.sameDate(date, current.start))
2062
+ return true;
2063
+ if (current.end && this.dateAdapter.sameDate(date, current.end))
2064
+ return true;
2065
+ return false;
2066
+ }
2067
+ isRangeStart(date, current) {
2068
+ if (!current?.start)
2069
+ return false;
2070
+ return this.dateAdapter.sameDate(date, current.start);
2071
+ }
2072
+ isRangeEnd(date, current) {
2073
+ if (!current?.end)
2074
+ return false;
2075
+ return this.dateAdapter.sameDate(date, current.end);
2076
+ }
2077
+ isRangeMiddle(date, current) {
2078
+ if (!current?.start || !current?.end)
2079
+ return false;
2080
+ return (isDateInRange(date, current.start, current.end, this.dateAdapter) &&
2081
+ !this.isRangeStart(date, current) &&
2082
+ !this.isRangeEnd(date, current));
2083
+ }
2084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RangeSelectionStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2085
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RangeSelectionStrategy });
2086
+ }
2087
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RangeSelectionStrategy, decorators: [{
2088
+ type: Injectable
2089
+ }] });
2090
+
2091
+ /**
2092
+ * Multi-date selection.
2093
+ *
2094
+ * - Each click toggles the date in/out of the selection array.
2095
+ * - Each click commits immediately (no intermediate preview).
2096
+ * - `selected` has shape `D[]`.
2097
+ */
2098
+ class MultiSelectionStrategy extends CalendarSelectionStrategy {
2099
+ dateAdapter = inject(DATE_ADAPTER);
2100
+ select(date, current) {
2101
+ const existing = current ?? [];
2102
+ const index = existing.findIndex((d) => this.dateAdapter.sameDate(d, date));
2103
+ const next = index >= 0
2104
+ ? [...existing.slice(0, index), ...existing.slice(index + 1)]
2105
+ : [...existing, date];
2106
+ return { selection: next, isComplete: true };
2107
+ }
2108
+ createPreview(_active, _current) {
2109
+ return null;
2110
+ }
2111
+ isSelected(date, current) {
2112
+ return (current ?? []).some((d) => this.dateAdapter.sameDate(d, date));
2113
+ }
2114
+ isRangeStart(_date, _current) {
2115
+ return false;
2116
+ }
2117
+ isRangeEnd(_date, _current) {
2118
+ return false;
2119
+ }
2120
+ isRangeMiddle(_date, _current) {
2121
+ return false;
2122
+ }
2123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MultiSelectionStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2124
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MultiSelectionStrategy });
2125
+ }
2126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MultiSelectionStrategy, decorators: [{
2127
+ type: Injectable
2128
+ }] });
2129
+
2130
+ /**
2131
+ * Week selection.
2132
+ *
2133
+ * - Any click selects the entire week containing that day.
2134
+ * - Week bounds use `DateAdapter.getFirstDayOfWeek()`.
2135
+ * - Each click commits immediately.
2136
+ * - Hovering previews the week the cursor is in.
2137
+ *
2138
+ * `selected` has shape `DateRange<D> | null`.
2139
+ */
2140
+ class WeekSelectionStrategy extends CalendarSelectionStrategy {
2141
+ dateAdapter = inject(DATE_ADAPTER);
2142
+ select(date, _current) {
2143
+ return { selection: this.weekRange(date), isComplete: true };
2144
+ }
2145
+ createPreview(active, _current) {
2146
+ if (!active)
2147
+ return null;
2148
+ return this.weekRange(active);
2149
+ }
2150
+ isSelected(date, current) {
2151
+ if (!current?.start || !current?.end)
2152
+ return false;
2153
+ return isDateInRange(date, current.start, current.end, this.dateAdapter);
2154
+ }
2155
+ isRangeStart(date, current) {
2156
+ if (!current?.start)
2157
+ return false;
2158
+ return this.dateAdapter.sameDate(date, current.start);
2159
+ }
2160
+ isRangeEnd(date, current) {
2161
+ if (!current?.end)
2162
+ return false;
2163
+ return this.dateAdapter.sameDate(date, current.end);
2164
+ }
2165
+ isRangeMiddle(date, current) {
2166
+ if (!current?.start || !current?.end)
2167
+ return false;
2168
+ return (this.isSelected(date, current) &&
2169
+ !this.isRangeStart(date, current) &&
2170
+ !this.isRangeEnd(date, current));
2171
+ }
2172
+ weekRange(date) {
2173
+ const dow = this.dateAdapter.getDayOfWeek(date);
2174
+ const first = this.dateAdapter.getFirstDayOfWeek();
2175
+ const daysToSubtract = (dow - first + 7) % 7;
2176
+ const start = this.dateAdapter.addDays(date, -daysToSubtract);
2177
+ const end = this.dateAdapter.addDays(start, 6);
2178
+ return { start, end };
2179
+ }
2180
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WeekSelectionStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2181
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WeekSelectionStrategy });
2182
+ }
2183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WeekSelectionStrategy, decorators: [{
2184
+ type: Injectable
2185
+ }] });
2186
+
2187
+ /** Provides single-date selection (default behaviour). */
2188
+ function provideSingleSelectionStrategy() {
2189
+ return { provide: CALENDAR_SELECTION_STRATEGY, useClass: SingleSelectionStrategy };
2190
+ }
2191
+ /** Provides two-click date-range selection. */
2192
+ function provideRangeSelectionStrategy() {
2193
+ return { provide: CALENDAR_SELECTION_STRATEGY, useClass: RangeSelectionStrategy };
2194
+ }
2195
+ /** Provides toggle-based multi-date selection. */
2196
+ function provideMultiSelectionStrategy() {
2197
+ return { provide: CALENDAR_SELECTION_STRATEGY, useClass: MultiSelectionStrategy };
2198
+ }
2199
+ /** Provides week-at-a-time selection. */
2200
+ function provideWeekSelectionStrategy() {
2201
+ return { provide: CALENDAR_SELECTION_STRATEGY, useClass: WeekSelectionStrategy };
2202
+ }
2203
+ /** Provides an arbitrary custom selection strategy. */
2204
+ function provideCalendarSelectionStrategy(strategy) {
2205
+ return { provide: CALENDAR_SELECTION_STRATEGY, useClass: strategy };
2206
+ }
2207
+
2208
+ const calendarVariants = tv({
2209
+ slots: {
2210
+ root: 'inline-block p-2 bg-surface text-fg',
2211
+ months: '',
2212
+ },
2213
+ variants: {
2214
+ bordered: {
2215
+ true: { root: 'rounded-lg shadow-sm border border-border' },
2216
+ false: {},
2217
+ },
2218
+ columns: {
2219
+ 1: { months: '' },
2220
+ 2: { months: 'flex gap-3' },
2221
+ },
2222
+ },
2223
+ defaultVariants: { bordered: true, columns: 1 },
2224
+ }, { twMerge: true });
2225
+ /**
2226
+ * Main calendar orchestrator. Composes the header and view components
2227
+ * (day, month, year) with view switching, navigation, focus management,
2228
+ * and the §8 internal state model. Every date operation is delegated to
2229
+ * an injected `DateAdapter` — call `provideNativeDateAdapter()` in your
2230
+ * app to bootstrap the default one.
2231
+ *
2232
+ * Generic parameters (§7.3):
2233
+ * - `M` — selection mode. Narrows `value` to the mode-specific shape.
2234
+ * - `D` — adapter date type.
2235
+ * - `TOut` — post-transformer form value type (Phase 14 wires it; until then
2236
+ * it defaults to `CalendarValue<M, D>` so forms see raw adapter values).
2237
+ */
2238
+ class CalendarComponent {
2239
+ dateAdapter = inject(DATE_ADAPTER);
2240
+ elementRef = inject((ElementRef));
2241
+ injector = inject(Injector);
2242
+ destroyRef = inject(DestroyRef);
2243
+ liveAnnouncer = inject(LiveAnnouncer);
2244
+ /**
2245
+ * Resolved lazily in `ngOnInit` — eager construction-time injection creates a
2246
+ * circular dependency (NG0200): the component registers itself as both
2247
+ * `NG_VALUE_ACCESSOR` and `NG_VALIDATORS`, and `FormControlName` resolves
2248
+ * those eagerly while constructing on the same element. Looking up
2249
+ * `NgControl` after construction breaks the cycle.
2250
+ */
2251
+ ngControl = null;
2252
+ /** Angular's `LOCALE_ID` — used as the fallback when no `locale` input is supplied (§19.1). */
2253
+ platformLocale = inject(LOCALE_ID);
2254
+ /**
2255
+ * DI-provided default `CalendarIntl`. New'd up locally when the consumer hasn't
2256
+ * supplied one — DI defaults are merged with the per-instance `intl` input
2257
+ * via `effectiveIntl()` (§19.4 per-field merge).
2258
+ */
2259
+ injectedIntl = inject(CalendarIntl, { optional: true }) ?? new CalendarIntl();
2260
+ injectedStrategy = inject(CALENDAR_SELECTION_STRATEGY, {
2261
+ optional: true,
2262
+ });
2263
+ monthViews = viewChildren(MonthViewComponent, ...(ngDevMode ? [{ debugName: "monthViews" }] : /* istanbul ignore next */ []));
2264
+ /**
2265
+ * Built-in strategies instantiated lazily in the host's injection context so
2266
+ * `inject(DATE_ADAPTER)` calls inside each strategy resolve. Range state will
2267
+ * migrate into the orchestrator in Phase 6; single / multiple stay here as
2268
+ * internals (not exported) via the spec's mode union.
2269
+ */
2270
+ localStrategies = runInInjectionContext(this.injector, () => ({
2271
+ single: new SingleSelectionStrategy(),
2272
+ range: new RangeSelectionStrategy(),
2273
+ multiple: new MultiSelectionStrategy(),
2274
+ }));
2275
+ // ---------------------------------------------------------------------------
2276
+ // Public inputs (§33.1)
2277
+ // ---------------------------------------------------------------------------
2278
+ /** Selection mode (§5). Changing this at runtime clears the value and emits `modeChange`. */
2279
+ mode = model('single', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2280
+ /**
2281
+ * Consumer-bound value. Shape narrows by `M`: `D | null` for single,
2282
+ * `D[]` for multiple, `{ start; end }` for range.
2283
+ */
2284
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2285
+ /** Anchor date for the displayed view. Defaults to today on mount. */
2286
+ startAt = input(null, ...(ngDevMode ? [{ debugName: "startAt" }] : /* istanbul ignore next */ []));
2287
+ /** Minimum selectable date. */
2288
+ minDate = input(null, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
2289
+ /** Maximum selectable date. */
2290
+ maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
2291
+ /** Per-date predicate — return `false` to disable. */
2292
+ dateFilter = input(null, ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
2293
+ /**
2294
+ * Explicitly disabled dates (§10.1). Either an array (each entry compared via
2295
+ * `adapter.sameDate`) or a predicate returning `true` for disabled dates.
2296
+ * OR-combined with `dateFilter`, `disabledDaysOfWeek`, and `[minDate, maxDate]`.
2297
+ */
2298
+ disabledDates = input(null, ...(ngDevMode ? [{ debugName: "disabledDates" }] : /* istanbul ignore next */ []));
2299
+ /** Days of the week to disable (0=Sun … 6=Sat). Empty array = no day-of-week disabling. */
2300
+ disabledDaysOfWeek = input([], ...(ngDevMode ? [{ debugName: "disabledDaysOfWeek" }] : /* istanbul ignore next */ []));
2301
+ /**
2302
+ * Bundle of date constraints (§10.1). Lets a consumer pass `minDate`, `maxDate`,
2303
+ * `disabledDates`, `disabledDaysOfWeek`, and `dateFilter` as a single object —
2304
+ * useful for sharing a constraint preset across multiple calendars. Each field is
2305
+ * optional. Individual inputs (`minDate`, `maxDate`, `disabledDates`,
2306
+ * `disabledDaysOfWeek`, `dateFilter`) take precedence over fields here when both
2307
+ * are set non-null.
2308
+ */
2309
+ constraints = input(null, ...(ngDevMode ? [{ debugName: "constraints" }] : /* istanbul ignore next */ []));
2310
+ /** Minimum range length in days, inclusive (`mode: 'range'` only). `null` = no minimum. */
2311
+ minRangeLength = input(null, ...(ngDevMode ? [{ debugName: "minRangeLength" }] : /* istanbul ignore next */ []));
2312
+ /** Maximum range length in days, inclusive (`mode: 'range'` only). `null` = no maximum. */
2313
+ maxRangeLength = input(null, ...(ngDevMode ? [{ debugName: "maxRangeLength" }] : /* istanbul ignore next */ []));
2314
+ /** Maximum number of selections in `mode: 'multiple'`. `null` = unlimited. */
2315
+ maxSelections = input(null, ...(ngDevMode ? [{ debugName: "maxSelections" }] : /* istanbul ignore next */ []));
2316
+ /**
2317
+ * What happens when the user tries to select past `maxSelections` (§10.1).
2318
+ * `'emit-limit-reached'` (default) emits `selectionLimitReached` and ignores
2319
+ * the click; `'replace-oldest'` drops the first entry; `'ignore'` is silent.
2320
+ */
2321
+ maxSelectionBehavior = input('emit-limit-reached', ...(ngDevMode ? [{ debugName: "maxSelectionBehavior" }] : /* istanbul ignore next */ []));
2322
+ /**
2323
+ * IDREF list for the form-error live region (§28.3). When set, the calendar
2324
+ * root carries `aria-describedby="<value>"` so screen readers read consumer-
2325
+ * rendered error messages alongside the focused cell announcement.
2326
+ */
2327
+ errorAriaDescribedBy = input(null, ...(ngDevMode ? [{ debugName: "errorAriaDescribedBy" }] : /* istanbul ignore next */ []));
2328
+ /**
2329
+ * How `mode: 'range'` reacts to a click after a complete range (§21.2):
2330
+ * - `'restart'` (default): start a new range with the clicked cell, emit `selectionRestart`.
2331
+ * - `'nearest-edge'`: move the closer endpoint (start vs end) to the clicked date and re-commit (§21.3); emits `selectionComplete({reason: 'nearest-edge'})`.
2332
+ * - `'require-clear'`: ignore the click + flash the cell as invalid (`data-state-invalid-flash`); user must call `clear()` first.
2333
+ */
2334
+ rangeClickBehavior = input('restart', ...(ngDevMode ? [{ debugName: "rangeClickBehavior" }] : /* istanbul ignore next */ []));
2335
+ /**
2336
+ * Range-mode behavior knobs. Accepts a partial object — unset fields use the
2337
+ * defaults documented on each property of `RangeBehaviorConfig`. Defaults:
2338
+ * `{ allowSingleDayRange: true, persistPartialRange: true, allowBackwardRange: false, disableRangesCrossingDisabledDates: false }`.
2339
+ */
2340
+ rangeBehavior = input({}, ...(ngDevMode ? [{ debugName: "rangeBehavior" }] : /* istanbul ignore next */ []));
2341
+ /** Function producing per-cell CSS classes. */
2342
+ dateClass = input(null, ...(ngDevMode ? [{ debugName: "dateClass" }] : /* istanbul ignore next */ []));
2343
+ /** When `true`, the calendar renders with a border and a soft shadow. */
2344
+ bordered = input(true, ...(ngDevMode ? [{ debugName: "bordered" }] : /* istanbul ignore next */ []));
2345
+ /** Which view opens first. Defaults to `'day'`. Phase 8 will derive the default from `rangeGranularity`. */
2346
+ startView = input('day', ...(ngDevMode ? [{ debugName: "startView" }] : /* istanbul ignore next */ []));
2347
+ /** Override first day of week (0=Sun, 1=Mon). Falls back to the adapter's default. */
2348
+ firstDayOfWeek = input(null, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
2349
+ /**
2350
+ * Number of months to display side-by-side (1 or 2). Phase 9 replaces this with a
2351
+ * full `numberOfMonths: 1..12+` surface (§23).
2352
+ */
2353
+ monthColumns = input(1, ...(ngDevMode ? [{ debugName: "monthColumns" }] : /* istanbul ignore next */ []));
2354
+ /** Disabled state. Merged with any `setDisabledState` call from a bound form control via `effectiveDisabled`. */
2355
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
2356
+ /** Read-only state (§33.1). When `true`, focus and keyboard navigation still work but selections cannot be committed. */
2357
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
2358
+ /** How a form reset restores internal state (§6.5). `'full'` (default) also restores view + active-date; `'value-only'` clears the draft but leaves navigation where the user left it. */
2359
+ resetBehavior = input('full', ...(ngDevMode ? [{ debugName: "resetBehavior" }] : /* istanbul ignore next */ []));
2360
+ /** Optional cell-content template. */
2361
+ cellTemplate = input(null, ...(ngDevMode ? [{ debugName: "cellTemplate" }] : /* istanbul ignore next */ []));
2362
+ /** Per-instance locale override. Falls back to Angular `LOCALE_ID` when `null` (§19.1). */
2363
+ locale = input(null, ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
2364
+ /**
2365
+ * Per-instance `CalendarIntl` override (§19.4). Per-field merge — unspecified
2366
+ * fields inherit the DI-provided default. Pass a partial bag (e.g. one of the
2367
+ * shipped locale packs `de`, `fr`, `es`, `pt`, `ja`) or a full custom instance.
2368
+ */
2369
+ intl = input(null, ...(ngDevMode ? [{ debugName: "intl" }] : /* istanbul ignore next */ []));
2370
+ // ---------------------------------------------------------------------------
2371
+ // Public outputs (§33.2). Outputs reachable only in overlay mode (opened/closed)
2372
+ // or through constraint-aware preview (rangePreview `invalidPreview`) are declared
2373
+ // here but wired in later phases per the plan.
2374
+ // ---------------------------------------------------------------------------
2375
+ /** Fires whenever the committed value changes (cell click, keyboard select, preset, mode change, programmatic clear). Payload is the untransformed mode-shaped `CalendarValue<M, D>` — `D | null` for single, `D[]` for multiple, `{ start, end }` for range (§7.6). */
2376
+ valueChange = output();
2377
+ /** Fires on the first click of a range selection (transitions selection state to SELECTING). Payload carries the chosen `start` date. */
2378
+ selectionStart = output();
2379
+ /** Fires as the hover / keyboard cursor moves during range SELECTING. Payload carries the `tentativeRange` and an `invalidPreview` flag set when the range crosses a disabled date or violates `min`/`maxRangeLength`. */
2380
+ rangePreview = output();
2381
+ /** Fires after `valueChange` once the selection commits. Payload carries the committed `value` and a `reason` flagging the commit path (`'commit' | 'auto-swap' | 'nearest-edge' | 'preset'`). */
2382
+ selectionComplete = output();
2383
+ /** Fires when a range selection restarts after a complete range (e.g. third click with `rangeClickBehavior='restart'`). Payload carries the new draft `start` date. */
2384
+ selectionRestart = output();
2385
+ /** Fires whenever the value clears. Payload carries a `reason` distinguishing user clear, programmatic, mode change, form reset, or a disabled flip. */
2386
+ selectionCleared = output();
2387
+ /** Fires in `mode: 'multiple'` when the user attempts to select past `maxSelections` (only with `maxSelectionBehavior: 'emit-limit-reached'`). Payload carries the configured `limit` and the rejected `attempted` date. */
2388
+ selectionLimitReached = output();
2389
+ /** Fires when the selected preset changes (user click on a preset chip, or programmatic clear). Payload is the new preset id, or `null` when no preset is active. Phase 12 wires it. */
2390
+ presetChange = output();
2391
+ /** Fires on every view transition (day ↔ month ↔ year). Payload carries `from`, `to`, and a `reason` distinguishing drill-down, drill-up, user header click, and programmatic changes. */
2392
+ viewChange = output();
2393
+ /** Fires when keyboard navigation, mouse hover, or programmatic action moves the focused (active) cell. Payload is the new active date. */
2394
+ activeDateChange = output();
2395
+ /** Fires when the displayed primary month changes (page-nav, drill-up, or programmatic). Payload carries the new `year` and zero-based `month`. */
2396
+ monthChange = output();
2397
+ /** Fires when the displayed year changes (month/year-view nav or year-page scroll). Payload carries the new `year`. */
2398
+ yearChange = output();
2399
+ /** Fires on every pointer click of any cell, including disabled ones — analytics only. Payload carries the clicked `date` and the underlying `PointerEvent`. Does NOT indicate a selection; subscribe to `valueChange` for that. */
2400
+ cellClick = output();
2401
+ /** Fires on every pointer hover of any cell — analytics only. Payload carries the hovered `date`. Does NOT indicate a preview; subscribe to `rangePreview` for range-mode hover state. */
2402
+ cellHover = output();
2403
+ /** Fires when `mode` changes at runtime, in canonical order `selectionCleared` → `modeChange` → `valueChange` (§11.2). Payload carries `{ from, to }` modes. */
2404
+ modeChange = output();
2405
+ // ---------------------------------------------------------------------------
2406
+ // Internal writable state (§8.1)
2407
+ // ---------------------------------------------------------------------------
2408
+ /** In-flight draft during range SELECTING. `null` in other states (§8.1). */
2409
+ internalDraftValue = signal(null, ...(ngDevMode ? [{ debugName: "internalDraftValue" }] : /* istanbul ignore next */ []));
2410
+ /** Current selection lifecycle state. */
2411
+ _selectionState = signal('EMPTY', ...(ngDevMode ? [{ debugName: "_selectionState" }] : /* istanbul ignore next */ []));
2412
+ /** Current view (writable for drill). Tracks `startView` until the user drills. */
2413
+ _viewState = linkedSignal(() => this.startView(), ...(ngDevMode ? [{ debugName: "_viewState" }] : /* istanbul ignore next */ []));
2414
+ /**
2415
+ * Active (focused) date. `linkedSignal` so changes to `startAt` at runtime update
2416
+ * the anchor, but internal navigation can still write it independently.
2417
+ */
2418
+ _activeDate = linkedSignal(() => this.startAt() ?? this.dateAdapter.today(), ...(ngDevMode ? [{ debugName: "_activeDate" }] : /* istanbul ignore next */ []));
2419
+ /** Pointer-hover date (desktop only). */
2420
+ _hoveredDate = signal(null, ...(ngDevMode ? [{ debugName: "_hoveredDate" }] : /* istanbul ignore next */ []));
2421
+ /** Overlay state. Always `'closed'` in inline mode until Phase 10 wires the lifecycle. */
2422
+ _overlayState = signal('closed', ...(ngDevMode ? [{ debugName: "_overlayState" }] : /* istanbul ignore next */ []));
2423
+ /** Preset selection id. Phase 12 writes this. */
2424
+ _selectedPresetId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedPresetId" }] : /* istanbul ignore next */ []));
2425
+ /** Raw `TOut` retained when `fromForm` throws (§7.6, Phase 14). `null` otherwise. */
2426
+ _lastInvalidFormValue = signal(null, ...(ngDevMode ? [{ debugName: "_lastInvalidFormValue" }] : /* istanbul ignore next */ []));
2427
+ // ---------------------------------------------------------------------------
2428
+ // Public readonly signals (§33.3)
2429
+ // ---------------------------------------------------------------------------
2430
+ /** Overlay lifecycle. `null` until `interaction: 'overlay'` lands in Phase 10; until then the implementation signal is exposed as `'closed'`. */
2431
+ overlayState = computed(() => this._overlayState(), ...(ngDevMode ? [{ debugName: "overlayState" }] : /* istanbul ignore next */ []));
2432
+ /** Selection lifecycle per §8. */
2433
+ selectionState = computed(() => this._selectionState(), ...(ngDevMode ? [{ debugName: "selectionState" }] : /* istanbul ignore next */ []));
2434
+ /** Active (focused) cell. `null` when the overlay is closed and no inline focus is established; in inline mode this is always the current anchor. */
2435
+ activeDate = computed(() => this._activeDate(), ...(ngDevMode ? [{ debugName: "activeDate" }] : /* istanbul ignore next */ []));
2436
+ /** Currently selected preset id. */
2437
+ selectedPresetId = computed(() => this._selectedPresetId(), ...(ngDevMode ? [{ debugName: "selectedPresetId" }] : /* istanbul ignore next */ []));
2438
+ /** Current view state (day / month / year). */
2439
+ viewState = computed(() => this._viewState(), ...(ngDevMode ? [{ debugName: "viewState" }] : /* istanbul ignore next */ []));
2440
+ /** Months currently rendered. Phase 9 extends this to the full responsive count. */
2441
+ displayedMonths = computed(() => {
2442
+ const active = this._activeDate();
2443
+ const primary = {
2444
+ year: this.dateAdapter.getYear(active),
2445
+ month: this.dateAdapter.getMonth(active),
2446
+ };
2447
+ if (this.effectiveMonthColumns() < 2)
2448
+ return [primary];
2449
+ const secondary = this.dateAdapter.addMonths(active, 1);
2450
+ return [
2451
+ primary,
2452
+ {
2453
+ year: this.dateAdapter.getYear(secondary),
2454
+ month: this.dateAdapter.getMonth(secondary),
2455
+ },
2456
+ ];
2457
+ }, ...(ngDevMode ? [{ debugName: "displayedMonths" }] : /* istanbul ignore next */ []));
2458
+ /** Raw form value rejected by the transformer's `fromForm` (§7.6, §33.3). */
2459
+ lastInvalidFormValue = computed(() => this._lastInvalidFormValue(), ...(ngDevMode ? [{ debugName: "lastInvalidFormValue" }] : /* istanbul ignore next */ []));
2460
+ // ---------------------------------------------------------------------------
2461
+ // ControlValueAccessor + Validator (Phase 3)
2462
+ // ---------------------------------------------------------------------------
2463
+ cvaOnChange = () => { };
2464
+ cvaOnTouched = () => { };
2465
+ validatorOnChange = () => { };
2466
+ /**
2467
+ * @internal Disabled state set by `ControlValueAccessor.setDisabledState` and
2468
+ * by the Signal Forms mode directives. OR-merged with the public `disabled`
2469
+ * input into `effectiveDisabled`.
2470
+ */
2471
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
2472
+ /**
2473
+ * @internal Read-only state set by the Signal Forms mode directives. OR-merged
2474
+ * with the public `readonly` input into `effectiveReadonly`.
2475
+ */
2476
+ cvaReadonly = signal(false, ...(ngDevMode ? [{ debugName: "cvaReadonly" }] : /* istanbul ignore next */ []));
2477
+ /** Effective disabled state — public `disabled` input OR any form-bound disabled flip. Drives `aria-disabled`. */
2478
+ effectiveDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
2479
+ /** Effective read-only state — public `readonly` input OR any form-bound readonly flip. Drives `aria-readonly`. */
2480
+ effectiveReadonly = computed(() => this.readonly() || this.cvaReadonly(), ...(ngDevMode ? [{ debugName: "effectiveReadonly" }] : /* istanbul ignore next */ []));
2481
+ /**
2482
+ * @internal Resolved range-behavior config. Merges the consumer-supplied
2483
+ * `Partial<RangeBehaviorConfig>` over the documented defaults so internal
2484
+ * call sites read each field directly without re-applying fallbacks.
2485
+ */
2486
+ _resolvedRangeBehavior = computed(() => ({
2487
+ allowBackwardRange: false,
2488
+ allowSingleDayRange: true,
2489
+ persistPartialRange: true,
2490
+ disableRangesCrossingDisabledDates: false,
2491
+ ...this.rangeBehavior(),
2492
+ }), ...(ngDevMode ? [{ debugName: "_resolvedRangeBehavior" }] : /* istanbul ignore next */ []));
2493
+ /** @internal Dev-mode warning guard so each mismatched write warns only once per instance. */
2494
+ _warnedShapeMismatch = false;
2495
+ /** @internal Set by `writeValue`; read by the `FormResetEvent` handler so it can emit `selectionCleared({reason: 'reset'})` only when a non-empty value was on the control before the reset cleared it. */
2496
+ _hadValueBeforeLastWrite = false;
2497
+ constructor() {
2498
+ this.destroyRef.onDestroy(() => {
2499
+ if (this._invalidFlashTimer !== null) {
2500
+ clearTimeout(this._invalidFlashTimer);
2501
+ this._invalidFlashTimer = null;
2502
+ }
2503
+ });
2504
+ // Runtime `mode` changes clear state and emit the canonical event order
2505
+ // per §11.2: selectionCleared({reason: 'mode-change'}) → modeChange → valueChange.
2506
+ // We read `mode()` via `linkedSignal` in a tracked effect so the prior value
2507
+ // can be observed via the captured closure.
2508
+ let previousMode = untracked(() => this.mode());
2509
+ effect(() => {
2510
+ const current = this.mode();
2511
+ if (current === previousMode)
2512
+ return;
2513
+ const from = previousMode;
2514
+ previousMode = current;
2515
+ untracked(() => this.onModeChanged(from, current));
2516
+ });
2517
+ // Re-run validation whenever mode, constraints, or the rejected-write
2518
+ // marker changes so the §10.2 codes stay in sync. Constraint change
2519
+ // revalidation per §11.4.
2520
+ effect(() => {
2521
+ this.mode();
2522
+ this._lastInvalidFormValue();
2523
+ // `resolvedConstraints` transitively tracks `minDate`, `maxDate`, `dateFilter`,
2524
+ // `disabledDates`, `disabledDaysOfWeek`, and the shorthand `constraints` input.
2525
+ this.resolvedConstraints();
2526
+ this.minRangeLength();
2527
+ this.maxRangeLength();
2528
+ this.maxSelections();
2529
+ untracked(() => this.validatorOnChange());
2530
+ });
2531
+ // Push the resolved locale into the date adapter so `Intl.DateTimeFormat`
2532
+ // calls (month/weekday names, format()) align with `LOCALE_ID` or the
2533
+ // per-instance `locale` override (§19.1).
2534
+ effect(() => {
2535
+ const locale = this.effectiveLocale();
2536
+ untracked(() => this.dateAdapter.setLocale(locale));
2537
+ });
2538
+ // Phase 6 — keyboard-driven `rangePreview` emission. While SELECTING, any
2539
+ // change to `previewRange` (which now reacts to `activeDate` via
2540
+ // `previewCursor`) emits the same `rangePreview` payload as a hover (§21.1).
2541
+ // The hover path keeps emitting from `onPreviewChange` directly; this
2542
+ // effect covers arrow-key + Home/End/PageUp/PageDown navigation.
2543
+ let lastEmittedPreviewKey = null;
2544
+ effect(() => {
2545
+ // Skip when no draft is in-flight (only meaningful during SELECTING).
2546
+ if (this._selectionState() !== 'SELECTING') {
2547
+ lastEmittedPreviewKey = null;
2548
+ return;
2549
+ }
2550
+ const pr = this.previewRange();
2551
+ if (!pr?.start || !pr?.end)
2552
+ return;
2553
+ // Pointer-hover already emits via `onPreviewChange`; only fire here for
2554
+ // keyboard navigation (no hover anchor present).
2555
+ if (this._hoveredDate())
2556
+ return;
2557
+ const key = this.dateAdapter.toIso(pr.start) + '|' + this.dateAdapter.toIso(pr.end);
2558
+ if (key === lastEmittedPreviewKey)
2559
+ return;
2560
+ lastEmittedPreviewKey = key;
2561
+ const start = pr.start;
2562
+ const end = pr.end;
2563
+ untracked(() => this.rangePreview.emit({
2564
+ tentativeRange: { start, end },
2565
+ invalidPreview: this.isPreviewInvalid(start, end),
2566
+ }));
2567
+ });
2568
+ }
2569
+ ngOnInit() {
2570
+ // Lazy NgControl lookup avoids the construction-time cycle (see field
2571
+ // declaration). By `ngOnInit` the host's `FormControlName`/`NgModel` is
2572
+ // fully constructed, so resolving it here is safe.
2573
+ this.ngControl = this.injector.get(NgControl, null, {
2574
+ self: true,
2575
+ optional: true,
2576
+ });
2577
+ // Reset detection per §6.5 — `FormControl.reset()` emits a dedicated
2578
+ // `FormResetEvent` on its events stream (and parent `FormGroup.reset()`
2579
+ // cascades via each child's `reset()`). Only Reactive / Template-driven
2580
+ // forms expose an `AbstractControl.events` Observable; Signal Forms'
2581
+ // `FormField` provides an `InteropNgControl` shim whose `.control`
2582
+ // re-points to itself and has no `events` stream, so we feature-detect
2583
+ // before subscribing. Signal Forms reset is handled via the field's
2584
+ // value-signal flow, not this event.
2585
+ const ctrl = this.ngControl?.control;
2586
+ if (ctrl?.events) {
2587
+ ctrl.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
2588
+ if (event instanceof FormResetEvent) {
2589
+ this.handleFormReset();
2590
+ }
2591
+ });
2592
+ }
2593
+ }
2594
+ /** @internal `ControlValueAccessor` — writes a value from a reactive / template-driven form. Wrong-shape writes preserve the prior value per §7.2. Programmatic writes during SELECTING discard the draft per §11.1. */
2595
+ writeValue(incoming) {
2596
+ this._hadValueBeforeLastWrite = !isEmptyCalendarValue(this.value());
2597
+ const mode = this.mode();
2598
+ // Phase 6 — programmatic write during SELECTING discards the in-flight draft.
2599
+ // Detect BEFORE we touch state so we can fire `selectionCleared` exactly once
2600
+ // when the draft existed, regardless of which branch below handles the write.
2601
+ const wasSelecting = this._selectionState() === 'SELECTING' && this.internalDraftValue() !== null;
2602
+ if (incoming === null || incoming === undefined) {
2603
+ const empty = emptyCalendarValue(mode);
2604
+ this.value.set(empty);
2605
+ this._selectionState.set('EMPTY');
2606
+ this.internalDraftValue.set(null);
2607
+ if (wasSelecting)
2608
+ this.selectionCleared.emit({ reason: 'programmatic' });
2609
+ if (this._lastInvalidFormValue() !== null) {
2610
+ this._lastInvalidFormValue.set(null);
2611
+ this.validatorOnChange();
2612
+ }
2613
+ return;
2614
+ }
2615
+ if (!matchesModeShape(mode, incoming)) {
2616
+ if (isDevMode() && !this._warnedShapeMismatch) {
2617
+ this._warnedShapeMismatch = true;
2618
+ console.warn(`[tw-calendar] writeValue received a value whose shape does not match mode="${mode}". ` +
2619
+ `Preserving the prior value; the form control will report calendarInvalidValue. ` +
2620
+ `Received:`, incoming);
2621
+ }
2622
+ this._lastInvalidFormValue.set(incoming);
2623
+ this.validatorOnChange();
2624
+ return;
2625
+ }
2626
+ // Phase 6 — reject SELECTING-shaped programmatic writes (`{ start: D, end: null }`)
2627
+ // for `mode: 'range'`; only EMPTY or COMPLETE shapes are valid form values.
2628
+ if (mode === 'range' && isPartialRangeValue(incoming)) {
2629
+ if (isDevMode() && !this._warnedShapeMismatch) {
2630
+ this._warnedShapeMismatch = true;
2631
+ console.warn('[tw-calendar] writeValue received a partial range `{start, end: null}` — only fully committed ranges are valid form values. Preserving the prior value; the form control will report calendarInvalidValue.', incoming);
2632
+ }
2633
+ this._lastInvalidFormValue.set(incoming);
2634
+ this.validatorOnChange();
2635
+ return;
2636
+ }
2637
+ const normalized = incoming;
2638
+ this.value.set(normalized);
2639
+ this._selectionState.set(deriveSelectionStateFromValue(normalized));
2640
+ this.internalDraftValue.set(null);
2641
+ if (wasSelecting)
2642
+ this.selectionCleared.emit({ reason: 'programmatic' });
2643
+ if (this._lastInvalidFormValue() !== null) {
2644
+ this._lastInvalidFormValue.set(null);
2645
+ this.validatorOnChange();
2646
+ }
2647
+ }
2648
+ /** @internal `ControlValueAccessor` — registers the on-change callback. */
2649
+ registerOnChange(fn) {
2650
+ this.cvaOnChange = fn;
2651
+ }
2652
+ /** @internal `ControlValueAccessor` — registers the touched callback. */
2653
+ registerOnTouched(fn) {
2654
+ this.cvaOnTouched = fn;
2655
+ }
2656
+ /** @internal `ControlValueAccessor` — reflects the form control's disabled state through `effectiveDisabled` / `aria-disabled`. */
2657
+ setDisabledState(isDisabled) {
2658
+ this.cvaDisabled.set(isDisabled);
2659
+ }
2660
+ /** @internal `Validator` — runs the §10.2 validator. Phase 3 codes (`calendarRequired`, `calendarInvalidValue`) plus Phase 4 constraint codes (`calendarMinDate`, `calendarMaxDate`, `calendarDisabledDate`, `calendarRangeTooShort`, `calendarRangeTooLong`, `calendarMaxSelections`, `calendarInvalidRange`). */
2661
+ validate(control) {
2662
+ const ctx = {
2663
+ mode: this.mode(),
2664
+ lastInvalidFormValue: this._lastInvalidFormValue(),
2665
+ constraints: this.resolvedConstraints(),
2666
+ adapter: this.dateAdapter,
2667
+ minRangeLength: this.minRangeLength(),
2668
+ maxRangeLength: this.maxRangeLength(),
2669
+ maxSelections: this.maxSelections(),
2670
+ };
2671
+ return calendarValidator(ctx)(control);
2672
+ }
2673
+ /** @internal `Validator` — registers the change callback so the form re-runs validation when internal state changes. */
2674
+ registerOnValidatorChange(fn) {
2675
+ this.validatorOnChange = fn;
2676
+ }
2677
+ /** @internal Host `(focusout)` handler — fires `onTouched` when focus leaves the component (§13.6 inline path). Overlay-mode wiring is Phase 10. */
2678
+ onBlur(event) {
2679
+ const host = this.elementRef.nativeElement;
2680
+ const related = event.relatedTarget;
2681
+ // Only fire when focus actually leaves the component tree. Focus moves
2682
+ // between cells (roving tabindex) stay inside the host and must not mark
2683
+ // the control touched.
2684
+ if (related && host.contains(related))
2685
+ return;
2686
+ this.cvaOnTouched();
2687
+ }
2688
+ // ---------------------------------------------------------------------------
2689
+ // Derived state (kept from the prior implementation; some refined per Phase 1)
2690
+ // ---------------------------------------------------------------------------
2691
+ /**
2692
+ * Effective `CalendarIntl` resolved per-field over the DI default (§19.4).
2693
+ * The injected default is the base; the `intl` input overrides individual
2694
+ * fields. Unspecified fields fall through to English defaults.
2695
+ */
2696
+ effectiveIntl = computed(() => {
2697
+ const override = this.intl();
2698
+ if (!override)
2699
+ return this.injectedIntl;
2700
+ // Prototype copy preserves bound methods; shallow per-field overlay matches
2701
+ // the spec's per-field merge semantics.
2702
+ const merged = Object.create(Object.getPrototypeOf(this.injectedIntl));
2703
+ Object.assign(merged, this.injectedIntl, override);
2704
+ return merged;
2705
+ }, ...(ngDevMode ? [{ debugName: "effectiveIntl" }] : /* istanbul ignore next */ []));
2706
+ /** Effective locale — `locale` input wins, otherwise Angular `LOCALE_ID`. */
2707
+ effectiveLocale = computed(() => this.locale() ?? this.platformLocale, ...(ngDevMode ? [{ debugName: "effectiveLocale" }] : /* istanbul ignore next */ []));
2708
+ /**
2709
+ * Active strategy for single/multiple/range. Injected strategies (custom) win.
2710
+ * Range state will migrate into this component in Phase 6; for now we reuse
2711
+ * the strategy for its `select` / `createPreview` / `isSelected` helpers.
2712
+ */
2713
+ selectionStrategy = computed(() => {
2714
+ if (this.injectedStrategy)
2715
+ return this.injectedStrategy;
2716
+ const m = this.mode();
2717
+ if (m === 'range')
2718
+ return this.localStrategies.range;
2719
+ if (m === 'multiple')
2720
+ return this.localStrategies.multiple;
2721
+ return this.localStrategies.single;
2722
+ }, ...(ngDevMode ? [{ debugName: "selectionStrategy" }] : /* istanbul ignore next */ []));
2723
+ /** Computed first-day-of-week (input override wins over adapter default). */
2724
+ computedFirstDayOfWeek = computed(() => {
2725
+ const override = this.firstDayOfWeek();
2726
+ if (override !== null && override >= 0 && override <= 6)
2727
+ return override;
2728
+ return this.dateAdapter.getFirstDayOfWeek();
2729
+ }, ...(ngDevMode ? [{ debugName: "computedFirstDayOfWeek" }] : /* istanbul ignore next */ []));
2730
+ /** Resolves the configured `monthColumns` value. Range-mode consumers pass `2` explicitly. */
2731
+ effectiveMonthColumns = computed(() => this.monthColumns(), ...(ngDevMode ? [{ debugName: "effectiveMonthColumns" }] : /* istanbul ignore next */ []));
2732
+ /**
2733
+ * @internal Drives `aria-multiselectable` on every view grid. `true` when the
2734
+ * current `mode` allows more than one cell to be selected at a time
2735
+ * (`'multiple'` or `'range'`); `false` for `'single'`.
2736
+ */
2737
+ multiSelectable = computed(() => this.mode() !== 'single', ...(ngDevMode ? [{ debugName: "multiSelectable" }] : /* istanbul ignore next */ []));
2738
+ /** Secondary anchor for the right-hand grid in multi-column mode. */
2739
+ secondaryActiveDate = computed(() => {
2740
+ if (this.effectiveMonthColumns() < 2)
2741
+ return null;
2742
+ return this.dateAdapter.addMonths(this._activeDate(), 1);
2743
+ }, ...(ngDevMode ? [{ debugName: "secondaryActiveDate" }] : /* istanbul ignore next */ []));
2744
+ /**
2745
+ * Phase 6 — preview cursor. Pointer hover wins; falls back to `activeDate`
2746
+ * when SELECTING so keyboard arrow-key moves drive the same preview path
2747
+ * (§21.1, §16.1). `null` when no SELECTING draft is in-flight and pointer
2748
+ * is off-grid.
2749
+ */
2750
+ previewCursor = computed(() => {
2751
+ const hovered = this._hoveredDate();
2752
+ if (hovered)
2753
+ return hovered;
2754
+ if (this._selectionState() === 'SELECTING')
2755
+ return this._activeDate();
2756
+ return null;
2757
+ }, ...(ngDevMode ? [{ debugName: "previewCursor" }] : /* istanbul ignore next */ []));
2758
+ /**
2759
+ * Preview range. Driven by the strategy and the `previewCursor` (hover or
2760
+ * keyboard active-date during SELECTING).
2761
+ */
2762
+ previewRange = computed(() => {
2763
+ const strategy = this.selectionStrategy();
2764
+ const cursor = this.previewCursor();
2765
+ const source = this.strategySelection();
2766
+ return strategy.createPreview(cursor, source);
2767
+ }, ...(ngDevMode ? [{ debugName: "previewRange" }] : /* istanbul ignore next */ []));
2768
+ /**
2769
+ * Adapter-compatible view of the current selection.
2770
+ * For range: a `{ start, end }` view of either the committed value or the in-flight draft.
2771
+ * For single: the raw `D | null`.
2772
+ * For multiple: the raw `D[]`.
2773
+ *
2774
+ * Used internally to feed the legacy strategy surface while Phase 6 migrates range state.
2775
+ */
2776
+ strategySelection = computed(() => {
2777
+ const m = this.mode();
2778
+ const v = this.value();
2779
+ if (m === 'range') {
2780
+ const draft = this.internalDraftValue();
2781
+ if (draft)
2782
+ return { start: draft.start, end: null };
2783
+ return v ?? null;
2784
+ }
2785
+ return v;
2786
+ }, ...(ngDevMode ? [{ debugName: "strategySelection" }] : /* istanbul ignore next */ []));
2787
+ /** Adapter-compatible view exposed to the view components. */
2788
+ legacySelected = computed(() => this.strategySelection(), ...(ngDevMode ? [{ debugName: "legacySelected" }] : /* istanbul ignore next */ []));
2789
+ variantResult = computed(() => calendarVariants({
2790
+ bordered: this.bordered(),
2791
+ columns: (this.effectiveMonthColumns() >= 2 ? 2 : 1),
2792
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
2793
+ /** @internal */
2794
+ rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
2795
+ /** @internal */
2796
+ monthsClasses = computed(() => this.variantResult().months(), ...(ngDevMode ? [{ debugName: "monthsClasses" }] : /* istanbul ignore next */ []));
2797
+ monthNames = computed(() => this.dateAdapter.getMonthNames('long'), ...(ngDevMode ? [{ debugName: "monthNames" }] : /* istanbul ignore next */ []));
2798
+ /** @internal */
2799
+ periodLabel = computed(() => {
2800
+ const date = this._activeDate();
2801
+ const view = this._viewState();
2802
+ const year = this.dateAdapter.getYear(date);
2803
+ const month = this.dateAdapter.getMonth(date);
2804
+ switch (view) {
2805
+ case 'day': {
2806
+ const name = this.monthNames()[month];
2807
+ if (this.effectiveMonthColumns() >= 2) {
2808
+ const secondDate = this.secondaryActiveDate();
2809
+ const secondMonth = this.dateAdapter.getMonth(secondDate);
2810
+ const secondYear = this.dateAdapter.getYear(secondDate);
2811
+ const secondName = this.monthNames()[secondMonth];
2812
+ if (year === secondYear)
2813
+ return `${name} – ${secondName} ${year}`;
2814
+ return `${name} ${year} – ${secondName} ${secondYear}`;
2815
+ }
2816
+ return `${name} ${year}`;
2817
+ }
2818
+ case 'month':
2819
+ return `${year}`;
2820
+ case 'year': {
2821
+ const startYear = getMultiYearStartingYear(year, YEARS_PER_PAGE);
2822
+ return `${startYear} – ${startYear + YEARS_PER_PAGE - 1}`;
2823
+ }
2824
+ }
2825
+ }, ...(ngDevMode ? [{ debugName: "periodLabel" }] : /* istanbul ignore next */ []));
2826
+ /** @internal */
2827
+ periodAriaLabel = computed(() => {
2828
+ const view = this._viewState();
2829
+ const label = this.periodLabel();
2830
+ const intl = this.effectiveIntl();
2831
+ switch (view) {
2832
+ case 'day':
2833
+ return intl.switchToMonthViewLabel(label);
2834
+ case 'month':
2835
+ return intl.switchToYearViewLabel(label);
2836
+ case 'year':
2837
+ return label;
2838
+ }
2839
+ }, ...(ngDevMode ? [{ debugName: "periodAriaLabel" }] : /* istanbul ignore next */ []));
2840
+ /** @internal */
2841
+ prevAriaLabel = computed(() => {
2842
+ const intl = this.effectiveIntl();
2843
+ switch (this._viewState()) {
2844
+ case 'day':
2845
+ return intl.previousMonthLabel;
2846
+ case 'month':
2847
+ return intl.previousYearLabel;
2848
+ case 'year':
2849
+ return intl.previousYearsLabel(YEARS_PER_PAGE);
2850
+ }
2851
+ }, ...(ngDevMode ? [{ debugName: "prevAriaLabel" }] : /* istanbul ignore next */ []));
2852
+ /** @internal */
2853
+ nextAriaLabel = computed(() => {
2854
+ const intl = this.effectiveIntl();
2855
+ switch (this._viewState()) {
2856
+ case 'day':
2857
+ return intl.nextMonthLabel;
2858
+ case 'month':
2859
+ return intl.nextYearLabel;
2860
+ case 'year':
2861
+ return intl.nextYearsLabel(YEARS_PER_PAGE);
2862
+ }
2863
+ }, ...(ngDevMode ? [{ debugName: "nextAriaLabel" }] : /* istanbul ignore next */ []));
2864
+ /** @internal */
2865
+ prevDisabled = computed(() => {
2866
+ const date = this._activeDate();
2867
+ const minDate = this.resolvedMinDate();
2868
+ if (!minDate)
2869
+ return false;
2870
+ const view = this._viewState();
2871
+ const year = this.dateAdapter.getYear(date);
2872
+ const month = this.dateAdapter.getMonth(date);
2873
+ switch (view) {
2874
+ case 'day': {
2875
+ const prevMonth = month === 0 ? 11 : month - 1;
2876
+ const prevYear = month === 0 ? year - 1 : year;
2877
+ return isMonthDisabled(prevYear, prevMonth, minDate, null, this.dateAdapter);
2878
+ }
2879
+ case 'month':
2880
+ return isYearDisabled(year - 1, minDate, null, this.dateAdapter);
2881
+ case 'year': {
2882
+ const startYear = getMultiYearStartingYear(year, YEARS_PER_PAGE);
2883
+ return isYearDisabled(startYear - 1, minDate, null, this.dateAdapter);
2884
+ }
2885
+ }
2886
+ }, ...(ngDevMode ? [{ debugName: "prevDisabled" }] : /* istanbul ignore next */ []));
2887
+ /** @internal */
2888
+ nextDisabled = computed(() => {
2889
+ const date = this._activeDate();
2890
+ const maxDate = this.resolvedMaxDate();
2891
+ if (!maxDate)
2892
+ return false;
2893
+ const view = this._viewState();
2894
+ const year = this.dateAdapter.getYear(date);
2895
+ const month = this.dateAdapter.getMonth(date);
2896
+ switch (view) {
2897
+ case 'day': {
2898
+ const nextMonth = month === 11 ? 0 : month + 1;
2899
+ const nextYear = month === 11 ? year + 1 : year;
2900
+ return isMonthDisabled(nextYear, nextMonth, null, maxDate, this.dateAdapter);
2901
+ }
2902
+ case 'month':
2903
+ return isYearDisabled(year + 1, null, maxDate, this.dateAdapter);
2904
+ case 'year': {
2905
+ const startYear = getMultiYearStartingYear(year, YEARS_PER_PAGE);
2906
+ return isYearDisabled(startYear + YEARS_PER_PAGE, null, maxDate, this.dateAdapter);
2907
+ }
2908
+ }
2909
+ }, ...(ngDevMode ? [{ debugName: "nextDisabled" }] : /* istanbul ignore next */ []));
2910
+ // ---------------------------------------------------------------------------
2911
+ // Public methods (§33.4)
2912
+ // ---------------------------------------------------------------------------
2913
+ /** Focuses `date` and optionally navigates the view to render it. */
2914
+ focusDate(date, opts) {
2915
+ const clamped = this.dateAdapter.clampDate(date, this.resolvedMinDate(), this.resolvedMaxDate());
2916
+ if (opts?.navigate)
2917
+ this._activeDate.set(clamped);
2918
+ this.activeDateChange.emit(clamped);
2919
+ this.focusActiveCell();
2920
+ }
2921
+ /** Sets the current view state and emits a `viewChange` event. */
2922
+ setView(view) {
2923
+ const from = this._viewState();
2924
+ if (from === view)
2925
+ return;
2926
+ this._viewState.set(view);
2927
+ this.viewChange.emit({ from, to: view, reason: 'programmatic' });
2928
+ }
2929
+ /** Navigates to a specific date (anchor). Does not emit a selection event. */
2930
+ goToDate(date) {
2931
+ const clamped = this.dateAdapter.clampDate(date, this.resolvedMinDate(), this.resolvedMaxDate());
2932
+ this._activeDate.set(clamped);
2933
+ this.activeDateChange.emit(clamped);
2934
+ }
2935
+ /** Navigates to today. */
2936
+ goToToday() {
2937
+ this.goToDate(this.dateAdapter.today());
2938
+ }
2939
+ /** Clears the current selection. Alias: `clearSelection`. */
2940
+ clear() {
2941
+ if (this.effectiveReadonly())
2942
+ return;
2943
+ const had = !isEmptyCalendarValue(this.value());
2944
+ const empty = emptyCalendarValue(this.mode());
2945
+ const hadPreset = this._selectedPresetId() !== null;
2946
+ this.value.set(empty);
2947
+ this.internalDraftValue.set(null);
2948
+ this._selectionState.set('EMPTY');
2949
+ if (had)
2950
+ this.selectionCleared.emit({ reason: 'user' });
2951
+ this.cvaOnChange(empty);
2952
+ this.valueChange.emit(empty);
2953
+ if (hadPreset) {
2954
+ this._selectedPresetId.set(null);
2955
+ this.presetChange.emit(null);
2956
+ }
2957
+ }
2958
+ /** Alias of `clear()` exported per §33.4 for discoverability. */
2959
+ clearSelection() {
2960
+ this.clear();
2961
+ }
2962
+ /** Resets to initial state — value + view + active date per `resetBehavior`. Phase 3 extends this with form-reset integration. */
2963
+ reset() {
2964
+ this.clear();
2965
+ this._viewState.set(this.startView());
2966
+ this._activeDate.set(this.startAt() ?? this.dateAdapter.today());
2967
+ }
2968
+ /** Revalidates constraints + cell filters (Phase 4 wires it). */
2969
+ revalidate() {
2970
+ // No-op until Phase 4 introduces the validator.
2971
+ }
2972
+ /** Opens the overlay. Phase 10 wires the CDK overlay; inline mode is a no-op. */
2973
+ open() {
2974
+ // Phase 10.
2975
+ }
2976
+ /** Closes the overlay. */
2977
+ close() {
2978
+ // Phase 10.
2979
+ }
2980
+ /** Toggles the overlay. */
2981
+ toggle() {
2982
+ // Phase 10.
2983
+ }
2984
+ /** Imperatively focuses the currently active cell. */
2985
+ focusActiveCell() {
2986
+ const views = this.monthViews();
2987
+ if (views.length > 0) {
2988
+ views[0].focusActiveCell();
2989
+ return;
2990
+ }
2991
+ const host = this.elementRef.nativeElement;
2992
+ host.querySelector('button[tabindex="0"]')?.focus();
2993
+ }
2994
+ // ---------------------------------------------------------------------------
2995
+ // Event handlers (day grid)
2996
+ // ---------------------------------------------------------------------------
2997
+ /** @internal */
2998
+ onPrevClicked() {
2999
+ const view = this._viewState();
3000
+ const date = this._activeDate();
3001
+ const previousYear = this.dateAdapter.getYear(date);
3002
+ const previousMonth = this.dateAdapter.getMonth(date);
3003
+ let next;
3004
+ switch (view) {
3005
+ case 'day':
3006
+ next = this.dateAdapter.addMonths(date, -1);
3007
+ break;
3008
+ case 'month':
3009
+ next = this.dateAdapter.addYears(date, -1);
3010
+ break;
3011
+ case 'year':
3012
+ next = this.dateAdapter.addYears(date, -YEARS_PER_PAGE);
3013
+ break;
3014
+ }
3015
+ this._activeDate.set(next);
3016
+ this.activeDateChange.emit(next);
3017
+ this.emitPeriodChange(previousYear, previousMonth, view);
3018
+ this.announceNavigation('previous');
3019
+ }
3020
+ /** @internal */
3021
+ onNextClicked() {
3022
+ const view = this._viewState();
3023
+ const date = this._activeDate();
3024
+ const previousYear = this.dateAdapter.getYear(date);
3025
+ const previousMonth = this.dateAdapter.getMonth(date);
3026
+ let next;
3027
+ switch (view) {
3028
+ case 'day':
3029
+ next = this.dateAdapter.addMonths(date, 1);
3030
+ break;
3031
+ case 'month':
3032
+ next = this.dateAdapter.addYears(date, 1);
3033
+ break;
3034
+ case 'year':
3035
+ next = this.dateAdapter.addYears(date, YEARS_PER_PAGE);
3036
+ break;
3037
+ }
3038
+ this._activeDate.set(next);
3039
+ this.activeDateChange.emit(next);
3040
+ this.emitPeriodChange(previousYear, previousMonth, view);
3041
+ this.announceNavigation('next');
3042
+ }
3043
+ /** @internal — period-button click: drill up (day → month → year). */
3044
+ onPeriodClicked() {
3045
+ const current = this._viewState();
3046
+ if (current === 'day') {
3047
+ this._viewState.set('month');
3048
+ this.viewChange.emit({ from: current, to: 'month', reason: 'drill-up' });
3049
+ }
3050
+ else if (current === 'month') {
3051
+ this._viewState.set('year');
3052
+ this.viewChange.emit({ from: current, to: 'year', reason: 'drill-up' });
3053
+ }
3054
+ this.announceViewChange();
3055
+ }
3056
+ /** @internal — day-grid cell activation. */
3057
+ onDateSelected(date) {
3058
+ if (this.effectiveDisabled())
3059
+ return;
3060
+ // Read-only: keep focus + navigation but skip every commit path.
3061
+ if (this.effectiveReadonly()) {
3062
+ this._activeDate.set(date);
3063
+ this.activeDateChange.emit(date);
3064
+ return;
3065
+ }
3066
+ this._activeDate.set(date);
3067
+ this.activeDateChange.emit(date);
3068
+ const mode = this.mode();
3069
+ if (mode === 'range') {
3070
+ this.commitRangeClick(date);
3071
+ return;
3072
+ }
3073
+ if (mode === 'multiple') {
3074
+ // Phase 4 — enforce maxSelections per maxSelectionBehavior (§10.1, §33.1).
3075
+ const limit = this.maxSelections();
3076
+ const currentArr = this.value() ?? [];
3077
+ const isAlreadySelected = currentArr.some((d) => this.dateAdapter.sameDate(d, date));
3078
+ if (limit !== null && !isAlreadySelected && currentArr.length >= limit) {
3079
+ const behavior = this.maxSelectionBehavior();
3080
+ if (behavior === 'emit-limit-reached') {
3081
+ this.selectionLimitReached.emit({ limit, attempted: date });
3082
+ return;
3083
+ }
3084
+ if (behavior === 'replace-oldest') {
3085
+ const next = [...currentArr.slice(1), date];
3086
+ this.commitValue(next, 'commit');
3087
+ return;
3088
+ }
3089
+ // 'ignore' — silently drop the click.
3090
+ return;
3091
+ }
3092
+ }
3093
+ const strategy = this.selectionStrategy();
3094
+ const current = mode === 'multiple' ? (this.value() ?? []) : this.value();
3095
+ const result = strategy.select(date, current);
3096
+ if (result.isComplete) {
3097
+ this.commitValue(result.selection, 'commit');
3098
+ }
3099
+ }
3100
+ /** @internal — preview sync from pointer hover. */
3101
+ onPreviewChange(date) {
3102
+ this._hoveredDate.set(date);
3103
+ if (date)
3104
+ this.cellHover.emit({ date });
3105
+ const draft = this.internalDraftValue();
3106
+ const pr = this.previewRange();
3107
+ if (draft && pr?.start && pr.end) {
3108
+ this.rangePreview.emit({
3109
+ tentativeRange: { start: pr.start, end: pr.end },
3110
+ invalidPreview: this.isPreviewInvalid(pr.start, pr.end),
3111
+ });
3112
+ }
3113
+ }
3114
+ /**
3115
+ * Phase 4 — `true` when a tentative range hover would commit an invalid value.
3116
+ * Reasons: crosses a disabled date, or length violates `min`/`maxRangeLength`.
3117
+ * The flag is informational — does NOT block the commit on its own (§4 default
3118
+ * is permissive). Subscribe to `rangePreview.invalidPreview` together with the
3119
+ * `calendarRangeTooShort` / `calendarRangeTooLong` validator codes to react.
3120
+ */
3121
+ isPreviewInvalid(start, end) {
3122
+ const constraints = this.resolvedConstraints();
3123
+ // Endpoints already pass `enabled` check (you can't hover a disabled cell
3124
+ // and produce a preview), so we only need to look at the interior.
3125
+ if (rangeCrossesDisabled(start, end, constraints, this.dateAdapter, false))
3126
+ return true;
3127
+ const length = rangeLengthDays(start, end, this.dateAdapter);
3128
+ const min = this.minRangeLength();
3129
+ if (min !== null && length < min)
3130
+ return true;
3131
+ const max = this.maxRangeLength();
3132
+ if (max !== null && length > max)
3133
+ return true;
3134
+ return false;
3135
+ }
3136
+ /**
3137
+ * Resolved `minDate` — individual `minDate` input wins, then `constraints.minDate`,
3138
+ * else `null`. Used by every internal min-date reader so the shorthand input is
3139
+ * transparent to the rest of the component.
3140
+ */
3141
+ resolvedMinDate = computed(() => this.minDate() ?? this.constraints()?.minDate ?? null, ...(ngDevMode ? [{ debugName: "resolvedMinDate" }] : /* istanbul ignore next */ []));
3142
+ /** Resolved `maxDate` — individual input wins, then `constraints.maxDate`, else `null`. */
3143
+ resolvedMaxDate = computed(() => this.maxDate() ?? this.constraints()?.maxDate ?? null, ...(ngDevMode ? [{ debugName: "resolvedMaxDate" }] : /* istanbul ignore next */ []));
3144
+ /** Resolved `disabledDates` — individual input wins, then `constraints.disabledDates`, else `null`. */
3145
+ resolvedDisabledDates = computed(() => this.disabledDates() ?? this.constraints()?.disabledDates ?? null, ...(ngDevMode ? [{ debugName: "resolvedDisabledDates" }] : /* istanbul ignore next */ []));
3146
+ /** Resolved `disabledDaysOfWeek` — individual input wins (when non-empty), then `constraints.disabledDaysOfWeek`, else `[]`. */
3147
+ resolvedDisabledDaysOfWeek = computed(() => {
3148
+ const own = this.disabledDaysOfWeek();
3149
+ if (own.length > 0)
3150
+ return own;
3151
+ return this.constraints()?.disabledDaysOfWeek ?? [];
3152
+ }, ...(ngDevMode ? [{ debugName: "resolvedDisabledDaysOfWeek" }] : /* istanbul ignore next */ []));
3153
+ /** Resolved `dateFilter` — individual input wins, then `constraints.dateFilter`, else `null`. */
3154
+ resolvedDateFilter = computed(() => this.dateFilter() ?? this.constraints()?.dateFilter ?? null, ...(ngDevMode ? [{ debugName: "resolvedDateFilter" }] : /* istanbul ignore next */ []));
3155
+ /** Aggregated, resolved constraint inputs — passed to the resolver and validator. */
3156
+ resolvedConstraints = computed(() => ({
3157
+ minDate: this.resolvedMinDate(),
3158
+ maxDate: this.resolvedMaxDate(),
3159
+ disabledDates: this.resolvedDisabledDates(),
3160
+ disabledDaysOfWeek: this.resolvedDisabledDaysOfWeek(),
3161
+ dateFilter: this.resolvedDateFilter(),
3162
+ }), ...(ngDevMode ? [{ debugName: "resolvedConstraints" }] : /* istanbul ignore next */ []));
3163
+ /** @internal — month-view cell activation (drill-down to day). */
3164
+ onMonthSelected(date) {
3165
+ this._activeDate.set(date);
3166
+ const from = this._viewState();
3167
+ this._viewState.set('day');
3168
+ this.activeDateChange.emit(date);
3169
+ this.viewChange.emit({ from, to: 'day', reason: 'drill-down' });
3170
+ this.announceViewChange();
3171
+ }
3172
+ /** @internal — years-view cell activation (drill-down to month). */
3173
+ onYearSelected(date) {
3174
+ this._activeDate.set(date);
3175
+ const from = this._viewState();
3176
+ this._viewState.set('month');
3177
+ this.activeDateChange.emit(date);
3178
+ this.viewChange.emit({ from, to: 'month', reason: 'drill-down' });
3179
+ this.announceViewChange();
3180
+ }
3181
+ /** @internal — keyboard / programmatic active-date change from a view. */
3182
+ onActiveDateChange(date, gridIndex) {
3183
+ const views = this.monthViews();
3184
+ if (this.effectiveMonthColumns() >= 2 && views.length === 2 && gridIndex !== undefined) {
3185
+ const primaryMonth = this.dateAdapter.getMonth(this._activeDate());
3186
+ const secondaryMonth = this.dateAdapter.getMonth(this.secondaryActiveDate());
3187
+ const targetMonth = this.dateAdapter.getMonth(date);
3188
+ if (gridIndex === 0 && targetMonth === secondaryMonth) {
3189
+ const rightView = views[1];
3190
+ if (rightView) {
3191
+ rightView.focusCell(this.computeCompareValue(date));
3192
+ return;
3193
+ }
3194
+ }
3195
+ if (gridIndex === 1 && targetMonth === primaryMonth) {
3196
+ const leftView = views[0];
3197
+ if (leftView) {
3198
+ leftView.focusCell(this.computeCompareValue(date));
3199
+ return;
3200
+ }
3201
+ }
3202
+ }
3203
+ this._activeDate.set(date);
3204
+ this.activeDateChange.emit(date);
3205
+ }
3206
+ // ---------------------------------------------------------------------------
3207
+ // Range state machine (§21) — orchestrator-owned. Click matrix:
3208
+ // EMPTY + click → SELECTING (selectionStart)
3209
+ // SELECTING+ click → COMPLETE (selectionComplete; auto-swap unless allowBackwardRange)
3210
+ // COMPLETE + click → branches on rangeClickBehavior:
3211
+ // 'restart' → SELECTING (selectionRestart)
3212
+ // 'nearest-edge' → COMPLETE (selectionComplete reason='nearest-edge')
3213
+ // 'require-clear' → no-op + invalid-flash on the clicked cell
3214
+ // ---------------------------------------------------------------------------
3215
+ /** @internal Last invalidly-clicked date, cleared after ~200ms — drives `data-state-invalid-flash`. */
3216
+ _invalidFlashDate = signal(null, ...(ngDevMode ? [{ debugName: "_invalidFlashDate" }] : /* istanbul ignore next */ []));
3217
+ /** Public read of the transient invalid-flash anchor. Views consume this and tag the matching cell. */
3218
+ invalidFlashDate = computed(() => this._invalidFlashDate(), ...(ngDevMode ? [{ debugName: "invalidFlashDate" }] : /* istanbul ignore next */ []));
3219
+ commitRangeClick(date) {
3220
+ const draft = this.internalDraftValue();
3221
+ const state = this._selectionState();
3222
+ // ─── COMPLETE state branch — third click after a committed range. ────────
3223
+ if (state === 'COMPLETE') {
3224
+ const behavior = this.rangeClickBehavior();
3225
+ if (behavior === 'require-clear') {
3226
+ this.flashInvalid(date);
3227
+ return;
3228
+ }
3229
+ if (behavior === 'nearest-edge') {
3230
+ this.commitNearestEdge(date);
3231
+ return;
3232
+ }
3233
+ // 'restart' (default) → fall through to SELECTING entry.
3234
+ this.enterSelecting(date, /* fromComplete */ true);
3235
+ return;
3236
+ }
3237
+ // ─── EMPTY → SELECTING ──────────────────────────────────────────────────
3238
+ if (state !== 'SELECTING' || !draft) {
3239
+ this.enterSelecting(date, /* fromComplete */ false);
3240
+ return;
3241
+ }
3242
+ // ─── SELECTING → COMPLETE: commit the range. ────────────────────────────
3243
+ const start = draft.start;
3244
+ const cmp = this.dateAdapter.compare(date, start);
3245
+ const behavior = this._resolvedRangeBehavior();
3246
+ // `allowSingleDayRange = false` rejects clicking the same cell twice.
3247
+ if (cmp === 0 && !behavior.allowSingleDayRange) {
3248
+ this.flashInvalid(date);
3249
+ return;
3250
+ }
3251
+ let rangeStart;
3252
+ let rangeEnd;
3253
+ let reason = 'commit';
3254
+ if (cmp < 0) {
3255
+ if (behavior.allowBackwardRange) {
3256
+ // Preserve user-clicked order; do not normalize.
3257
+ rangeStart = start;
3258
+ rangeEnd = date;
3259
+ }
3260
+ else {
3261
+ // §21.5 silent auto-swap — DO NOT emit selectionRestart.
3262
+ rangeStart = date;
3263
+ rangeEnd = start;
3264
+ reason = 'auto-swap';
3265
+ }
3266
+ }
3267
+ else {
3268
+ rangeStart = start;
3269
+ rangeEnd = date;
3270
+ }
3271
+ // §21.4 disable-crossing guard: if any interior date is disabled, reject.
3272
+ if (behavior.disableRangesCrossingDisabledDates &&
3273
+ rangeCrossesDisabled(rangeStart, rangeEnd, this.resolvedConstraints(), this.dateAdapter,
3274
+ /* includeEndpoints */ false)) {
3275
+ this.flashInvalid(date);
3276
+ return;
3277
+ }
3278
+ const committed = { start: rangeStart, end: rangeEnd };
3279
+ this.internalDraftValue.set(null);
3280
+ this.commitValue(committed, reason);
3281
+ }
3282
+ /** Enters SELECTING with `date` as draft.start. Emits selectionStart or selectionRestart. */
3283
+ enterSelecting(date, fromComplete) {
3284
+ this.internalDraftValue.set({ start: date });
3285
+ this._selectionState.set('SELECTING');
3286
+ if (fromComplete) {
3287
+ this.selectionRestart.emit({ start: date });
3288
+ }
3289
+ else {
3290
+ this.selectionStart.emit({ start: date });
3291
+ }
3292
+ this.announceRangeStart(date);
3293
+ }
3294
+ /**
3295
+ * `'nearest-edge'` move (§21.3) — drag the closer endpoint of the committed
3296
+ * range to `date`. Emits `selectionComplete({reason: 'nearest-edge'})`.
3297
+ */
3298
+ commitNearestEdge(date) {
3299
+ const current = this.value();
3300
+ if (!current?.start || !current?.end) {
3301
+ // Defensive — shouldn't reach here in COMPLETE without both endpoints.
3302
+ this.enterSelecting(date, true);
3303
+ return;
3304
+ }
3305
+ const distStart = Math.abs(this.dateAdapter.compare(date, current.start));
3306
+ const distEnd = Math.abs(this.dateAdapter.compare(date, current.end));
3307
+ let nextStart;
3308
+ let nextEnd;
3309
+ if (distStart <= distEnd) {
3310
+ // Move start; if it crosses the existing end, swap.
3311
+ if (this.dateAdapter.compare(date, current.end) > 0) {
3312
+ nextStart = current.end;
3313
+ nextEnd = date;
3314
+ }
3315
+ else {
3316
+ nextStart = date;
3317
+ nextEnd = current.end;
3318
+ }
3319
+ }
3320
+ else {
3321
+ // Move end; if it crosses the existing start, swap.
3322
+ if (this.dateAdapter.compare(date, current.start) < 0) {
3323
+ nextStart = date;
3324
+ nextEnd = current.start;
3325
+ }
3326
+ else {
3327
+ nextStart = current.start;
3328
+ nextEnd = date;
3329
+ }
3330
+ }
3331
+ if (this._resolvedRangeBehavior().disableRangesCrossingDisabledDates &&
3332
+ rangeCrossesDisabled(nextStart, nextEnd, this.resolvedConstraints(), this.dateAdapter,
3333
+ /* includeEndpoints */ false)) {
3334
+ this.flashInvalid(date);
3335
+ return;
3336
+ }
3337
+ const committed = { start: nextStart, end: nextEnd };
3338
+ this.commitValue(committed, 'nearest-edge');
3339
+ }
3340
+ /** Pending invalid-flash clear timer; tracked so we can cancel on destroy. */
3341
+ _invalidFlashTimer = null;
3342
+ /** Sets the invalid-flash anchor and schedules a clear after 200ms. */
3343
+ flashInvalid(date) {
3344
+ this._invalidFlashDate.set(date);
3345
+ this.announceRejection();
3346
+ if (this._invalidFlashTimer !== null)
3347
+ clearTimeout(this._invalidFlashTimer);
3348
+ this._invalidFlashTimer = setTimeout(() => {
3349
+ this._invalidFlashTimer = null;
3350
+ // Only clear if we're still flashing this exact date (a new flash may have superseded).
3351
+ if (this._invalidFlashDate() === date) {
3352
+ this._invalidFlashDate.set(null);
3353
+ }
3354
+ }, 200);
3355
+ }
3356
+ /** Commits a value into the form + outputs with spec-ordered event emission. */
3357
+ commitValue(newValue, reason) {
3358
+ this.value.set(newValue);
3359
+ this._selectionState.set('COMPLETE');
3360
+ // §8.5: at most one `valueChange` + one `selectionComplete` per user action.
3361
+ this.cvaOnChange(newValue);
3362
+ this.cvaOnTouched();
3363
+ this.valueChange.emit(newValue);
3364
+ this.selectionComplete.emit({ value: newValue, reason });
3365
+ this.announceSelectionComplete(newValue);
3366
+ }
3367
+ // ---------------------------------------------------------------------------
3368
+ // Form reset handling (§6.5)
3369
+ // ---------------------------------------------------------------------------
3370
+ /**
3371
+ * Handles a form-owned reset. Triggered when the bound `NgControl` flips
3372
+ * `pristine` back to `true` (via `FormControl.reset()` / `FormGroup.reset()`).
3373
+ * Per §6.5 the form owns the value write, so this path does NOT emit
3374
+ * `valueChange` and does NOT call `onTouched`. It emits `selectionCleared`
3375
+ * when a value was previously committed and restores UI state per
3376
+ * `resetBehavior`.
3377
+ */
3378
+ handleFormReset() {
3379
+ // `writeValue` fires before `FormResetEvent`, so by the time we reach this
3380
+ // handler `this.value()` is already the reset value (typically empty).
3381
+ // `_hadValueBeforeLastWrite` is the snapshot captured at the start of
3382
+ // the preceding `writeValue` and is the correct signal for "was there
3383
+ // anything to clear?" per §6.5.
3384
+ const hadValue = this._hadValueBeforeLastWrite;
3385
+ this._hadValueBeforeLastWrite = false;
3386
+ const hadPreset = this._selectedPresetId() !== null;
3387
+ // Always clear draft / hovered / preset regardless of resetBehavior.
3388
+ this.internalDraftValue.set(null);
3389
+ this._hoveredDate.set(null);
3390
+ if (isEmptyCalendarValue(this.value())) {
3391
+ this._selectionState.set('EMPTY');
3392
+ }
3393
+ if (this.resetBehavior() === 'full') {
3394
+ this._viewState.set(this.startView());
3395
+ this._activeDate.set(this.startAt() ?? this.dateAdapter.today());
3396
+ }
3397
+ if (hadPreset) {
3398
+ this._selectedPresetId.set(null);
3399
+ this.presetChange.emit(null);
3400
+ }
3401
+ if (hadValue) {
3402
+ this.selectionCleared.emit({ reason: 'reset' });
3403
+ }
3404
+ }
3405
+ // ---------------------------------------------------------------------------
3406
+ // Mode change handling (§11.2)
3407
+ // ---------------------------------------------------------------------------
3408
+ onModeChanged(from, to) {
3409
+ const hadValue = !isEmptyCalendarValue(this.value());
3410
+ const hadPreset = this._selectedPresetId() !== null;
3411
+ // Canonical emit order per §11.2:
3412
+ // 1. selectionCleared({reason: 'mode-change'}) — only if a value was previously held
3413
+ // 2. presetChange(null) — only if a preset id was set
3414
+ // 3. modeChange
3415
+ // 4. valueChange (empty for new mode)
3416
+ this.internalDraftValue.set(null);
3417
+ this._selectionState.set('EMPTY');
3418
+ const empty = emptyCalendarValue(to);
3419
+ this.value.set(empty);
3420
+ if (hadValue) {
3421
+ this.selectionCleared.emit({ reason: 'mode-change' });
3422
+ }
3423
+ if (hadPreset) {
3424
+ this._selectedPresetId.set(null);
3425
+ this.presetChange.emit(null);
3426
+ }
3427
+ this.modeChange.emit({ from, to });
3428
+ this.cvaOnChange(empty);
3429
+ this.valueChange.emit(empty);
3430
+ }
3431
+ // ---------------------------------------------------------------------------
3432
+ // Utility helpers
3433
+ // ---------------------------------------------------------------------------
3434
+ computeCompareValue(date) {
3435
+ return (this.dateAdapter.getYear(date) * 10000 +
3436
+ this.dateAdapter.getMonth(date) * 100 +
3437
+ this.dateAdapter.getDate(date));
3438
+ }
3439
+ emitPeriodChange(previousYear, previousMonth, view) {
3440
+ const now = this._activeDate();
3441
+ const year = this.dateAdapter.getYear(now);
3442
+ const month = this.dateAdapter.getMonth(now);
3443
+ if (view === 'day' && (year !== previousYear || month !== previousMonth)) {
3444
+ this.monthChange.emit({ year, month });
3445
+ }
3446
+ if (year !== previousYear)
3447
+ this.yearChange.emit({ year });
3448
+ }
3449
+ announceViewChange() {
3450
+ const intl = this.effectiveIntl();
3451
+ if (intl.skipAnnouncement)
3452
+ return;
3453
+ this.liveAnnouncer.announce(intl.viewSwitched(this._viewState(), this.periodLabel()), 'polite');
3454
+ }
3455
+ announceNavigation(direction) {
3456
+ const intl = this.effectiveIntl();
3457
+ if (intl.skipAnnouncement)
3458
+ return;
3459
+ this.liveAnnouncer.announce(intl.navigatedTo(direction, this.periodLabel()), 'polite');
3460
+ }
3461
+ /** Announces a committed selection for AT users. Branches on mode so the message matches the §19.4 intl strings. */
3462
+ announceSelectionComplete(value) {
3463
+ const intl = this.effectiveIntl();
3464
+ if (intl.skipAnnouncement)
3465
+ return;
3466
+ const mode = this.mode();
3467
+ if (mode === 'single') {
3468
+ const single = value;
3469
+ if (!single)
3470
+ return;
3471
+ this.liveAnnouncer.announce(intl.selectedAnnouncement(this.formatDate(single)), 'polite');
3472
+ return;
3473
+ }
3474
+ if (mode === 'multiple') {
3475
+ const arr = value ?? [];
3476
+ this.liveAnnouncer.announce(intl.multipleSelectionAnnouncement(arr.length), 'polite');
3477
+ return;
3478
+ }
3479
+ const range = value;
3480
+ if (!range?.start || !range?.end)
3481
+ return;
3482
+ const length = rangeLengthDays(range.start, range.end, this.dateAdapter);
3483
+ this.liveAnnouncer.announce(intl.rangeUpdateAnnouncement(this.formatDate(range.start), this.formatDate(range.end), length), 'polite');
3484
+ }
3485
+ /** Announces the start of a range pick for AT users. */
3486
+ announceRangeStart(date) {
3487
+ const intl = this.effectiveIntl();
3488
+ if (intl.skipAnnouncement)
3489
+ return;
3490
+ this.liveAnnouncer.announce(intl.rangeStartAnnouncement(this.formatDate(date)), 'polite');
3491
+ }
3492
+ /** Announces that a click was rejected (require-clear, disable-cross commit, single-day disallowed). */
3493
+ announceRejection() {
3494
+ const intl = this.effectiveIntl();
3495
+ if (intl.skipAnnouncement)
3496
+ return;
3497
+ this.liveAnnouncer.announce(intl.selectionRejectedAnnouncement, 'polite');
3498
+ }
3499
+ /** Locale-aware long-form date format used in announcements (e.g. "Wednesday, April 15, 2026"). */
3500
+ formatDate(date) {
3501
+ return this.dateAdapter.format(date, { dateTimeFormat: { dateStyle: 'full' } });
3502
+ }
3503
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3504
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CalendarComponent, isStandalone: true, selector: "tw-calendar", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, disabledDaysOfWeek: { classPropertyName: "disabledDaysOfWeek", publicName: "disabledDaysOfWeek", isSignal: true, isRequired: false, transformFunction: null }, constraints: { classPropertyName: "constraints", publicName: "constraints", isSignal: true, isRequired: false, transformFunction: null }, minRangeLength: { classPropertyName: "minRangeLength", publicName: "minRangeLength", isSignal: true, isRequired: false, transformFunction: null }, maxRangeLength: { classPropertyName: "maxRangeLength", publicName: "maxRangeLength", isSignal: true, isRequired: false, transformFunction: null }, maxSelections: { classPropertyName: "maxSelections", publicName: "maxSelections", isSignal: true, isRequired: false, transformFunction: null }, maxSelectionBehavior: { classPropertyName: "maxSelectionBehavior", publicName: "maxSelectionBehavior", isSignal: true, isRequired: false, transformFunction: null }, errorAriaDescribedBy: { classPropertyName: "errorAriaDescribedBy", publicName: "errorAriaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, rangeClickBehavior: { classPropertyName: "rangeClickBehavior", publicName: "rangeClickBehavior", isSignal: true, isRequired: false, transformFunction: null }, rangeBehavior: { classPropertyName: "rangeBehavior", publicName: "rangeBehavior", isSignal: true, isRequired: false, transformFunction: null }, dateClass: { classPropertyName: "dateClass", publicName: "dateClass", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, monthColumns: { classPropertyName: "monthColumns", publicName: "monthColumns", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, resetBehavior: { classPropertyName: "resetBehavior", publicName: "resetBehavior", isSignal: true, isRequired: false, transformFunction: null }, cellTemplate: { classPropertyName: "cellTemplate", publicName: "cellTemplate", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, intl: { classPropertyName: "intl", publicName: "intl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mode: "modeChange", value: "valueChange", valueChange: "valueChange", selectionStart: "selectionStart", rangePreview: "rangePreview", selectionComplete: "selectionComplete", selectionRestart: "selectionRestart", selectionCleared: "selectionCleared", selectionLimitReached: "selectionLimitReached", presetChange: "presetChange", viewChange: "viewChange", activeDateChange: "activeDateChange", monthChange: "monthChange", yearChange: "yearChange", cellClick: "cellClick", cellHover: "cellHover", modeChange: "modeChange" }, host: { listeners: { "focusout": "onBlur($event)" }, properties: { "attr.aria-disabled": "effectiveDisabled() || null", "attr.aria-readonly": "effectiveReadonly() || null", "attr.aria-describedby": "errorAriaDescribedBy() || null" }, classAttribute: "block" }, providers: [
3505
+ {
3506
+ provide: NG_VALUE_ACCESSOR,
3507
+ useExisting: forwardRef(() => CalendarComponent),
3508
+ multi: true,
3509
+ },
3510
+ {
3511
+ provide: NG_VALIDATORS,
3512
+ useExisting: forwardRef(() => CalendarComponent),
3513
+ multi: true,
3514
+ },
3515
+ ], viewQueries: [{ propertyName: "monthViews", predicate: MonthViewComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
3516
+ <div
3517
+ [class]="rootClasses()"
3518
+ role="group"
3519
+ [attr.aria-label]="effectiveIntl().calendarLabel"
3520
+ >
3521
+ <tw-calendar-header
3522
+ [periodLabel]="periodLabel()"
3523
+ [periodAriaLabel]="periodAriaLabel()"
3524
+ [prevAriaLabel]="prevAriaLabel()"
3525
+ [nextAriaLabel]="nextAriaLabel()"
3526
+ [prevDisabled]="prevDisabled()"
3527
+ [nextDisabled]="nextDisabled()"
3528
+ [canSwitchView]="viewState() !== 'year'"
3529
+ (prevClicked)="onPrevClicked()"
3530
+ (nextClicked)="onNextClicked()"
3531
+ (periodClicked)="onPeriodClicked()"
3532
+ />
3533
+
3534
+ <ng-content select="[twCalendarPresets]" />
3535
+
3536
+ @switch (viewState()) {
3537
+ @case ('day') {
3538
+ <div [class]="monthsClasses()">
3539
+ <tw-calendar-month-view
3540
+ [activeDate]="activeDate()!"
3541
+ [selected]="legacySelected()"
3542
+ [minDate]="resolvedMinDate()"
3543
+ [maxDate]="resolvedMaxDate()"
3544
+ [dateFilter]="resolvedDateFilter()"
3545
+ [disabledDates]="resolvedDisabledDates()"
3546
+ [disabledDaysOfWeek]="resolvedDisabledDaysOfWeek()"
3547
+ [dateClass]="dateClass()"
3548
+ [cellTemplate]="cellTemplate()"
3549
+ [firstDayOfWeek]="computedFirstDayOfWeek()"
3550
+ [previewStart]="previewRange()?.start ?? null"
3551
+ [previewEnd]="previewRange()?.end ?? null"
3552
+ [invalidFlashDate]="invalidFlashDate()"
3553
+ [gridIndex]="0"
3554
+ [multiSelectable]="multiSelectable()"
3555
+ [readonlyGrid]="effectiveReadonly()"
3556
+ (selectedChange)="onDateSelected($event)"
3557
+ (activeDateChange)="onActiveDateChange($event, 0)"
3558
+ (previewChange)="onPreviewChange($event)"
3559
+ />
3560
+ @if (effectiveMonthColumns() >= 2 && secondaryActiveDate()) {
3561
+ <tw-calendar-month-view
3562
+ [activeDate]="secondaryActiveDate()!"
3563
+ [selected]="legacySelected()"
3564
+ [minDate]="resolvedMinDate()"
3565
+ [maxDate]="resolvedMaxDate()"
3566
+ [dateFilter]="resolvedDateFilter()"
3567
+ [disabledDates]="resolvedDisabledDates()"
3568
+ [disabledDaysOfWeek]="resolvedDisabledDaysOfWeek()"
3569
+ [dateClass]="dateClass()"
3570
+ [cellTemplate]="cellTemplate()"
3571
+ [firstDayOfWeek]="computedFirstDayOfWeek()"
3572
+ [previewStart]="previewRange()?.start ?? null"
3573
+ [previewEnd]="previewRange()?.end ?? null"
3574
+ [invalidFlashDate]="invalidFlashDate()"
3575
+ [gridIndex]="1"
3576
+ [multiSelectable]="multiSelectable()"
3577
+ [readonlyGrid]="effectiveReadonly()"
3578
+ (selectedChange)="onDateSelected($event)"
3579
+ (activeDateChange)="onActiveDateChange($event, 1)"
3580
+ (previewChange)="onPreviewChange($event)"
3581
+ />
3582
+ }
3583
+ </div>
3584
+ }
3585
+ @case ('month') {
3586
+ <tw-calendar-year-view
3587
+ [activeDate]="activeDate()!"
3588
+ [selected]="legacySelected()"
3589
+ [minDate]="resolvedMinDate()"
3590
+ [maxDate]="resolvedMaxDate()"
3591
+ [dateClass]="dateClass()"
3592
+ [cellTemplate]="cellTemplate()"
3593
+ [previewStart]="previewRange()?.start ?? null"
3594
+ [previewEnd]="previewRange()?.end ?? null"
3595
+ [multiSelectable]="multiSelectable()"
3596
+ [readonlyGrid]="effectiveReadonly()"
3597
+ (selectedChange)="onMonthSelected($event)"
3598
+ (activeDateChange)="onActiveDateChange($event)"
3599
+ (previewChange)="onPreviewChange($event)"
3600
+ />
3601
+ }
3602
+ @case ('year') {
3603
+ <tw-calendar-years-view
3604
+ [activeDate]="activeDate()!"
3605
+ [selected]="legacySelected()"
3606
+ [minDate]="resolvedMinDate()"
3607
+ [maxDate]="resolvedMaxDate()"
3608
+ [dateClass]="dateClass()"
3609
+ [cellTemplate]="cellTemplate()"
3610
+ [previewStart]="previewRange()?.start ?? null"
3611
+ [previewEnd]="previewRange()?.end ?? null"
3612
+ [multiSelectable]="multiSelectable()"
3613
+ [readonlyGrid]="effectiveReadonly()"
3614
+ (selectedChange)="onYearSelected($event)"
3615
+ (activeDateChange)="onActiveDateChange($event)"
3616
+ (previewChange)="onPreviewChange($event)"
3617
+ />
3618
+ }
3619
+ }
3620
+ </div>
3621
+ `, isInline: true, dependencies: [{ kind: "component", type: CalendarHeaderComponent, selector: "tw-calendar-header", inputs: ["periodLabel", "periodAriaLabel", "prevAriaLabel", "nextAriaLabel", "prevDisabled", "nextDisabled", "canSwitchView"], outputs: ["prevClicked", "nextClicked", "periodClicked"] }, { kind: "component", type: MonthViewComponent, selector: "tw-calendar-month-view", inputs: ["firstDayOfWeek", "gridIndex"] }, { kind: "component", type: YearViewComponent, selector: "tw-calendar-year-view" }, { kind: "component", type: YearsViewComponent, selector: "tw-calendar-years-view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3622
+ }
3623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarComponent, decorators: [{
3624
+ type: Component,
3625
+ args: [{
3626
+ selector: 'tw-calendar',
3627
+ imports: [
3628
+ CalendarHeaderComponent,
3629
+ MonthViewComponent,
3630
+ YearViewComponent,
3631
+ YearsViewComponent,
3632
+ ],
3633
+ providers: [
3634
+ {
3635
+ provide: NG_VALUE_ACCESSOR,
3636
+ useExisting: forwardRef(() => CalendarComponent),
3637
+ multi: true,
3638
+ },
3639
+ {
3640
+ provide: NG_VALIDATORS,
3641
+ useExisting: forwardRef(() => CalendarComponent),
3642
+ multi: true,
3643
+ },
3644
+ ],
3645
+ changeDetection: ChangeDetectionStrategy.OnPush,
3646
+ host: {
3647
+ class: 'block',
3648
+ '[attr.aria-disabled]': 'effectiveDisabled() || null',
3649
+ '[attr.aria-readonly]': 'effectiveReadonly() || null',
3650
+ '[attr.aria-describedby]': 'errorAriaDescribedBy() || null',
3651
+ '(focusout)': 'onBlur($event)',
3652
+ },
3653
+ template: `
3654
+ <div
3655
+ [class]="rootClasses()"
3656
+ role="group"
3657
+ [attr.aria-label]="effectiveIntl().calendarLabel"
3658
+ >
3659
+ <tw-calendar-header
3660
+ [periodLabel]="periodLabel()"
3661
+ [periodAriaLabel]="periodAriaLabel()"
3662
+ [prevAriaLabel]="prevAriaLabel()"
3663
+ [nextAriaLabel]="nextAriaLabel()"
3664
+ [prevDisabled]="prevDisabled()"
3665
+ [nextDisabled]="nextDisabled()"
3666
+ [canSwitchView]="viewState() !== 'year'"
3667
+ (prevClicked)="onPrevClicked()"
3668
+ (nextClicked)="onNextClicked()"
3669
+ (periodClicked)="onPeriodClicked()"
3670
+ />
3671
+
3672
+ <ng-content select="[twCalendarPresets]" />
3673
+
3674
+ @switch (viewState()) {
3675
+ @case ('day') {
3676
+ <div [class]="monthsClasses()">
3677
+ <tw-calendar-month-view
3678
+ [activeDate]="activeDate()!"
3679
+ [selected]="legacySelected()"
3680
+ [minDate]="resolvedMinDate()"
3681
+ [maxDate]="resolvedMaxDate()"
3682
+ [dateFilter]="resolvedDateFilter()"
3683
+ [disabledDates]="resolvedDisabledDates()"
3684
+ [disabledDaysOfWeek]="resolvedDisabledDaysOfWeek()"
3685
+ [dateClass]="dateClass()"
3686
+ [cellTemplate]="cellTemplate()"
3687
+ [firstDayOfWeek]="computedFirstDayOfWeek()"
3688
+ [previewStart]="previewRange()?.start ?? null"
3689
+ [previewEnd]="previewRange()?.end ?? null"
3690
+ [invalidFlashDate]="invalidFlashDate()"
3691
+ [gridIndex]="0"
3692
+ [multiSelectable]="multiSelectable()"
3693
+ [readonlyGrid]="effectiveReadonly()"
3694
+ (selectedChange)="onDateSelected($event)"
3695
+ (activeDateChange)="onActiveDateChange($event, 0)"
3696
+ (previewChange)="onPreviewChange($event)"
3697
+ />
3698
+ @if (effectiveMonthColumns() >= 2 && secondaryActiveDate()) {
3699
+ <tw-calendar-month-view
3700
+ [activeDate]="secondaryActiveDate()!"
3701
+ [selected]="legacySelected()"
3702
+ [minDate]="resolvedMinDate()"
3703
+ [maxDate]="resolvedMaxDate()"
3704
+ [dateFilter]="resolvedDateFilter()"
3705
+ [disabledDates]="resolvedDisabledDates()"
3706
+ [disabledDaysOfWeek]="resolvedDisabledDaysOfWeek()"
3707
+ [dateClass]="dateClass()"
3708
+ [cellTemplate]="cellTemplate()"
3709
+ [firstDayOfWeek]="computedFirstDayOfWeek()"
3710
+ [previewStart]="previewRange()?.start ?? null"
3711
+ [previewEnd]="previewRange()?.end ?? null"
3712
+ [invalidFlashDate]="invalidFlashDate()"
3713
+ [gridIndex]="1"
3714
+ [multiSelectable]="multiSelectable()"
3715
+ [readonlyGrid]="effectiveReadonly()"
3716
+ (selectedChange)="onDateSelected($event)"
3717
+ (activeDateChange)="onActiveDateChange($event, 1)"
3718
+ (previewChange)="onPreviewChange($event)"
3719
+ />
3720
+ }
3721
+ </div>
3722
+ }
3723
+ @case ('month') {
3724
+ <tw-calendar-year-view
3725
+ [activeDate]="activeDate()!"
3726
+ [selected]="legacySelected()"
3727
+ [minDate]="resolvedMinDate()"
3728
+ [maxDate]="resolvedMaxDate()"
3729
+ [dateClass]="dateClass()"
3730
+ [cellTemplate]="cellTemplate()"
3731
+ [previewStart]="previewRange()?.start ?? null"
3732
+ [previewEnd]="previewRange()?.end ?? null"
3733
+ [multiSelectable]="multiSelectable()"
3734
+ [readonlyGrid]="effectiveReadonly()"
3735
+ (selectedChange)="onMonthSelected($event)"
3736
+ (activeDateChange)="onActiveDateChange($event)"
3737
+ (previewChange)="onPreviewChange($event)"
3738
+ />
3739
+ }
3740
+ @case ('year') {
3741
+ <tw-calendar-years-view
3742
+ [activeDate]="activeDate()!"
3743
+ [selected]="legacySelected()"
3744
+ [minDate]="resolvedMinDate()"
3745
+ [maxDate]="resolvedMaxDate()"
3746
+ [dateClass]="dateClass()"
3747
+ [cellTemplate]="cellTemplate()"
3748
+ [previewStart]="previewRange()?.start ?? null"
3749
+ [previewEnd]="previewRange()?.end ?? null"
3750
+ [multiSelectable]="multiSelectable()"
3751
+ [readonlyGrid]="effectiveReadonly()"
3752
+ (selectedChange)="onYearSelected($event)"
3753
+ (activeDateChange)="onActiveDateChange($event)"
3754
+ (previewChange)="onPreviewChange($event)"
3755
+ />
3756
+ }
3757
+ }
3758
+ </div>
3759
+ `,
3760
+ }]
3761
+ }], ctorParameters: () => [], propDecorators: { monthViews: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => MonthViewComponent), { isSignal: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }, { type: i0.Output, args: ["modeChange"] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], startAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAt", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], disabledDaysOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDaysOfWeek", required: false }] }], constraints: [{ type: i0.Input, args: [{ isSignal: true, alias: "constraints", required: false }] }], minRangeLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRangeLength", required: false }] }], maxRangeLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRangeLength", required: false }] }], maxSelections: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelections", required: false }] }], maxSelectionBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectionBehavior", required: false }] }], errorAriaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorAriaDescribedBy", required: false }] }], rangeClickBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeClickBehavior", required: false }] }], rangeBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeBehavior", required: false }] }], dateClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateClass", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], monthColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "monthColumns", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], resetBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "resetBehavior", required: false }] }], cellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplate", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], intl: [{ type: i0.Input, args: [{ isSignal: true, alias: "intl", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], selectionStart: [{ type: i0.Output, args: ["selectionStart"] }], rangePreview: [{ type: i0.Output, args: ["rangePreview"] }], selectionComplete: [{ type: i0.Output, args: ["selectionComplete"] }], selectionRestart: [{ type: i0.Output, args: ["selectionRestart"] }], selectionCleared: [{ type: i0.Output, args: ["selectionCleared"] }], selectionLimitReached: [{ type: i0.Output, args: ["selectionLimitReached"] }], presetChange: [{ type: i0.Output, args: ["presetChange"] }], viewChange: [{ type: i0.Output, args: ["viewChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], monthChange: [{ type: i0.Output, args: ["monthChange"] }], yearChange: [{ type: i0.Output, args: ["yearChange"] }], cellClick: [{ type: i0.Output, args: ["cellClick"] }], cellHover: [{ type: i0.Output, args: ["cellHover"] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }] } });
3762
+
3763
+ function wireSignalFormsInputs(host, api) {
3764
+ // Mirror Signal Forms flags onto the component. Effects run in the
3765
+ // directive's injection context (constructor-created), so cleanup is
3766
+ // automatic on destroy.
3767
+ effect(() => {
3768
+ host.cvaDisabled.set(api.disabled());
3769
+ });
3770
+ effect(() => {
3771
+ host.cvaReadonly.set(api.readonly());
3772
+ });
3773
+ }
3774
+ /**
3775
+ * Signal Forms strict binding for `CalendarComponent` in `single` mode.
3776
+ * Implements `FormValueControl<D | null>` so `[field]="form.someDate"`
3777
+ * infers `FieldTree<D | null>` with no casts.
3778
+ *
3779
+ * Applies automatically when `<tw-calendar mode="single">` appears in the
3780
+ * template. When the mode attribute is omitted, consumers bind the component
3781
+ * directly — the CVA path handles reactive / template-driven forms.
3782
+ */
3783
+ class CalendarSingleDirective {
3784
+ host = inject(CalendarComponent, { self: true });
3785
+ /** Two-way value, forwarded to the hosting `CalendarComponent`. Signal Forms `Field` binds against this. */
3786
+ value = this.host.value;
3787
+ /** Mirrors the bound field's disabled flag. */
3788
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3789
+ /** Mirrors the bound field's readonly flag. */
3790
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
3791
+ /** Mirrors the bound field's required flag. */
3792
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
3793
+ /** Mirrors the bound field's invalid flag. */
3794
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
3795
+ /** Mirrors the bound field's hidden flag. */
3796
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
3797
+ /** Mirrors the bound field's errors. */
3798
+ errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
3799
+ /** Mirrors the bound field's disablement reasons. */
3800
+ disabledReasons = input([], ...(ngDevMode ? [{ debugName: "disabledReasons" }] : /* istanbul ignore next */ []));
3801
+ /** Two-way `touched` flag — drives and reflects the field's touched state. */
3802
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
3803
+ constructor() {
3804
+ wireSignalFormsInputs(this.host, this);
3805
+ }
3806
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarSingleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3807
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: CalendarSingleDirective, isStandalone: true, selector: "tw-calendar[mode=\"single\"]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, disabledReasons: { classPropertyName: "disabledReasons", publicName: "disabledReasons", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange" }, exportAs: ["twCalendarSingle"], ngImport: i0 });
3808
+ }
3809
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarSingleDirective, decorators: [{
3810
+ type: Directive,
3811
+ args: [{
3812
+ selector: 'tw-calendar[mode="single"]',
3813
+ exportAs: 'twCalendarSingle',
3814
+ }]
3815
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], disabledReasons: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledReasons", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
3816
+ /**
3817
+ * Signal Forms strict binding for `CalendarComponent` in `multiple` mode.
3818
+ * Implements `FormValueControl<D[]>`.
3819
+ */
3820
+ class CalendarMultipleDirective {
3821
+ host = inject(CalendarComponent, { self: true });
3822
+ /** Two-way value, forwarded to the hosting `CalendarComponent`. Signal Forms `Field` binds against this. */
3823
+ value = this.host.value;
3824
+ /** Mirrors the bound field's disabled flag. */
3825
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3826
+ /** Mirrors the bound field's readonly flag. */
3827
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
3828
+ /** Mirrors the bound field's required flag. */
3829
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
3830
+ /** Mirrors the bound field's invalid flag. */
3831
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
3832
+ /** Mirrors the bound field's hidden flag. */
3833
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
3834
+ /** Mirrors the bound field's errors. */
3835
+ errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
3836
+ /** Mirrors the bound field's disablement reasons. */
3837
+ disabledReasons = input([], ...(ngDevMode ? [{ debugName: "disabledReasons" }] : /* istanbul ignore next */ []));
3838
+ /** Two-way `touched` flag — drives and reflects the field's touched state. */
3839
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
3840
+ constructor() {
3841
+ wireSignalFormsInputs(this.host, this);
3842
+ }
3843
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarMultipleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3844
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: CalendarMultipleDirective, isStandalone: true, selector: "tw-calendar[mode=\"multiple\"]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, disabledReasons: { classPropertyName: "disabledReasons", publicName: "disabledReasons", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange" }, exportAs: ["twCalendarMultiple"], ngImport: i0 });
3845
+ }
3846
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarMultipleDirective, decorators: [{
3847
+ type: Directive,
3848
+ args: [{
3849
+ selector: 'tw-calendar[mode="multiple"]',
3850
+ exportAs: 'twCalendarMultiple',
3851
+ }]
3852
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], disabledReasons: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledReasons", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
3853
+ /**
3854
+ * Signal Forms strict binding for `CalendarComponent` in `range` mode.
3855
+ * Implements `FormValueControl<{ start: D | null; end: D | null }>`.
3856
+ */
3857
+ class CalendarRangeDirective {
3858
+ host = inject(CalendarComponent, { self: true });
3859
+ /** Two-way value, forwarded to the hosting `CalendarComponent`. Signal Forms `Field` binds against this. */
3860
+ value = this.host.value;
3861
+ /** Mirrors the bound field's disabled flag. */
3862
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3863
+ /** Mirrors the bound field's readonly flag. */
3864
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
3865
+ /** Mirrors the bound field's required flag. */
3866
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
3867
+ /** Mirrors the bound field's invalid flag. */
3868
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
3869
+ /** Mirrors the bound field's hidden flag. */
3870
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
3871
+ /** Mirrors the bound field's errors. */
3872
+ errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
3873
+ /** Mirrors the bound field's disablement reasons. */
3874
+ disabledReasons = input([], ...(ngDevMode ? [{ debugName: "disabledReasons" }] : /* istanbul ignore next */ []));
3875
+ /** Two-way `touched` flag — drives and reflects the field's touched state. */
3876
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
3877
+ constructor() {
3878
+ wireSignalFormsInputs(this.host, this);
3879
+ }
3880
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarRangeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3881
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: CalendarRangeDirective, isStandalone: true, selector: "tw-calendar[mode=\"range\"]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, disabledReasons: { classPropertyName: "disabledReasons", publicName: "disabledReasons", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange" }, exportAs: ["twCalendarRange"], ngImport: i0 });
3882
+ }
3883
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarRangeDirective, decorators: [{
3884
+ type: Directive,
3885
+ args: [{
3886
+ selector: 'tw-calendar[mode="range"]',
3887
+ exportAs: 'twCalendarRange',
3888
+ }]
3889
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], disabledReasons: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledReasons", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
3890
+
3891
+ /**
3892
+ * Marker directive for a slot positioned above the calendar grid. Used to
3893
+ * style an external preset rail; the calendar renders projected content
3894
+ * with `<ng-content select="[twCalendarPresets]">`.
3895
+ */
3896
+ class CalendarPresetsDirective {
3897
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarPresetsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3898
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CalendarPresetsDirective, isStandalone: true, selector: "[twCalendarPresets]", host: { classAttribute: "flex flex-wrap items-center gap-1.5 px-2 pb-2" }, ngImport: i0 });
3899
+ }
3900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CalendarPresetsDirective, decorators: [{
3901
+ type: Directive,
3902
+ args: [{
3903
+ selector: '[twCalendarPresets]',
3904
+ host: {
3905
+ class: 'flex flex-wrap items-center gap-1.5 px-2 pb-2',
3906
+ },
3907
+ }]
3908
+ }] });
3909
+
3910
+ /**
3911
+ * German (de) locale pack for the calendar's user-visible strings.
3912
+ * Pass into the `intl` input or via {@link provideCalendarIntl}. Per-field
3913
+ * merge semantics (§19.4) — unspecified fields fall back to English defaults.
3914
+ */
3915
+ const de = {
3916
+ previousMonthLabel: 'Vorheriger Monat',
3917
+ nextMonthLabel: 'Naechster Monat',
3918
+ previousYearLabel: 'Vorheriges Jahr',
3919
+ nextYearLabel: 'Naechstes Jahr',
3920
+ previousYearsLabel(yearsPerPage) {
3921
+ return yearsPerPage === 1 ? 'Vorheriges Jahr' : `Vorherige ${yearsPerPage} Jahre`;
3922
+ },
3923
+ nextYearsLabel(yearsPerPage) {
3924
+ return yearsPerPage === 1 ? 'Naechstes Jahr' : `Naechste ${yearsPerPage} Jahre`;
3925
+ },
3926
+ todayLabel: 'Heute',
3927
+ clearLabel: 'Loeschen',
3928
+ applyLabel: 'Uebernehmen',
3929
+ openCalendarLabel: 'Kalender oeffnen',
3930
+ chooseDateLabel: 'Datum auswaehlen',
3931
+ calendarLabel: 'Kalender',
3932
+ monthViewLabel: 'Monatsansicht',
3933
+ yearViewLabel: 'Jahresansicht',
3934
+ decadeViewLabel: 'Jahrzehntansicht',
3935
+ switchToMonthViewLabel(period) {
3936
+ return `${period}, klicken um zur Monatsansicht zu wechseln`;
3937
+ },
3938
+ switchToYearViewLabel(period) {
3939
+ return `${period}, klicken um zur Jahresansicht zu wechseln`;
3940
+ },
3941
+ navigatedTo(direction, period) {
3942
+ const word = direction === 'previous' ? 'vorigen' : 'naechsten';
3943
+ return `Zu ${word} ${period} navigiert`;
3944
+ },
3945
+ selectedAnnouncement(value) {
3946
+ return `Ausgewaehlt: ${value}`;
3947
+ },
3948
+ rangeStartAnnouncement(start) {
3949
+ return `Startdatum ausgewaehlt: ${start}. Bitte Enddatum auswaehlen.`;
3950
+ },
3951
+ rangeUpdateAnnouncement(start, end, lengthDays) {
3952
+ const noun = lengthDays === 1 ? 'Tag' : 'Tage';
3953
+ return `Zeitraum ausgewaehlt: ${start} bis ${end}, ${lengthDays} ${noun}`;
3954
+ },
3955
+ requiredError: 'Bitte ein Datum auswaehlen',
3956
+ minDateError(date) {
3957
+ return `Datum muss am oder nach dem ${date} liegen`;
3958
+ },
3959
+ maxDateError(date) {
3960
+ return `Datum muss am oder vor dem ${date} liegen`;
3961
+ },
3962
+ invalidValueError: 'Ungueltiger Datumswert',
3963
+ invalidRangeError: 'Ungueltiger Zeitraum',
3964
+ dateFilterError: 'Dieses Datum ist nicht verfuegbar',
3965
+ parseErrorLabel: 'Ungueltiges Datumsformat',
3966
+ };
3967
+
3968
+ /**
3969
+ * French (fr) locale pack for the calendar's user-visible strings.
3970
+ * Pass into the `intl` input or via {@link provideCalendarIntl}. Per-field
3971
+ * merge semantics (§19.4) — unspecified fields fall back to English defaults.
3972
+ */
3973
+ const fr = {
3974
+ previousMonthLabel: 'Mois precedent',
3975
+ nextMonthLabel: 'Mois suivant',
3976
+ previousYearLabel: 'Annee precedente',
3977
+ nextYearLabel: 'Annee suivante',
3978
+ previousYearsLabel(yearsPerPage) {
3979
+ return yearsPerPage === 1 ? 'Annee precedente' : `${yearsPerPage} annees precedentes`;
3980
+ },
3981
+ nextYearsLabel(yearsPerPage) {
3982
+ return yearsPerPage === 1 ? 'Annee suivante' : `${yearsPerPage} annees suivantes`;
3983
+ },
3984
+ todayLabel: "Aujourd'hui",
3985
+ clearLabel: 'Effacer',
3986
+ applyLabel: 'Appliquer',
3987
+ openCalendarLabel: 'Ouvrir le calendrier',
3988
+ chooseDateLabel: 'Choisir une date',
3989
+ calendarLabel: 'Calendrier',
3990
+ monthViewLabel: 'vue mensuelle',
3991
+ yearViewLabel: 'vue annuelle',
3992
+ decadeViewLabel: 'vue par decennie',
3993
+ switchToMonthViewLabel(period) {
3994
+ return `${period}, cliquer pour passer a la vue mensuelle`;
3995
+ },
3996
+ switchToYearViewLabel(period) {
3997
+ return `${period}, cliquer pour passer a la vue annuelle`;
3998
+ },
3999
+ navigatedTo(direction, period) {
4000
+ const word = direction === 'previous' ? 'precedent' : 'suivant';
4001
+ return `Navigation vers ${period} ${word}`;
4002
+ },
4003
+ selectedAnnouncement(value) {
4004
+ return `Selectionne : ${value}`;
4005
+ },
4006
+ rangeStartAnnouncement(start) {
4007
+ return `Date de debut selectionnee : ${start}. Selectionnez la date de fin.`;
4008
+ },
4009
+ rangeUpdateAnnouncement(start, end, lengthDays) {
4010
+ const noun = lengthDays === 1 ? 'jour' : 'jours';
4011
+ return `Plage selectionnee : ${start} au ${end}, ${lengthDays} ${noun}`;
4012
+ },
4013
+ requiredError: 'Veuillez selectionner une date',
4014
+ minDateError(date) {
4015
+ return `La date doit etre le ${date} ou apres`;
4016
+ },
4017
+ maxDateError(date) {
4018
+ return `La date doit etre le ${date} ou avant`;
4019
+ },
4020
+ invalidValueError: 'Valeur de date invalide',
4021
+ invalidRangeError: 'Plage de dates invalide',
4022
+ dateFilterError: 'Cette date n\'est pas disponible',
4023
+ parseErrorLabel: 'Format de date invalide',
4024
+ };
4025
+
4026
+ /**
4027
+ * Spanish (es) locale pack for the calendar's user-visible strings.
4028
+ * Pass into the `intl` input or via {@link provideCalendarIntl}. Per-field
4029
+ * merge semantics (§19.4) — unspecified fields fall back to English defaults.
4030
+ */
4031
+ const es = {
4032
+ previousMonthLabel: 'Mes anterior',
4033
+ nextMonthLabel: 'Mes siguiente',
4034
+ previousYearLabel: 'Ano anterior',
4035
+ nextYearLabel: 'Ano siguiente',
4036
+ previousYearsLabel(yearsPerPage) {
4037
+ return yearsPerPage === 1 ? 'Ano anterior' : `${yearsPerPage} anos anteriores`;
4038
+ },
4039
+ nextYearsLabel(yearsPerPage) {
4040
+ return yearsPerPage === 1 ? 'Ano siguiente' : `${yearsPerPage} anos siguientes`;
4041
+ },
4042
+ todayLabel: 'Hoy',
4043
+ clearLabel: 'Borrar',
4044
+ applyLabel: 'Aplicar',
4045
+ openCalendarLabel: 'Abrir calendario',
4046
+ chooseDateLabel: 'Elegir fecha',
4047
+ calendarLabel: 'Calendario',
4048
+ monthViewLabel: 'vista de mes',
4049
+ yearViewLabel: 'vista de ano',
4050
+ decadeViewLabel: 'vista de decada',
4051
+ switchToMonthViewLabel(period) {
4052
+ return `${period}, haga clic para cambiar a la vista de mes`;
4053
+ },
4054
+ switchToYearViewLabel(period) {
4055
+ return `${period}, haga clic para cambiar a la vista de ano`;
4056
+ },
4057
+ navigatedTo(direction, period) {
4058
+ const word = direction === 'previous' ? 'anterior' : 'siguiente';
4059
+ return `Navegado al ${period} ${word}`;
4060
+ },
4061
+ selectedAnnouncement(value) {
4062
+ return `Seleccionado: ${value}`;
4063
+ },
4064
+ rangeStartAnnouncement(start) {
4065
+ return `Fecha de inicio seleccionada: ${start}. Ahora seleccione la fecha de fin.`;
4066
+ },
4067
+ rangeUpdateAnnouncement(start, end, lengthDays) {
4068
+ const noun = lengthDays === 1 ? 'dia' : 'dias';
4069
+ return `Rango seleccionado: ${start} al ${end}, ${lengthDays} ${noun}`;
4070
+ },
4071
+ requiredError: 'Por favor seleccione una fecha',
4072
+ minDateError(date) {
4073
+ return `La fecha debe ser el ${date} o posterior`;
4074
+ },
4075
+ maxDateError(date) {
4076
+ return `La fecha debe ser el ${date} o anterior`;
4077
+ },
4078
+ invalidValueError: 'Valor de fecha invalido',
4079
+ invalidRangeError: 'Rango de fechas invalido',
4080
+ dateFilterError: 'Esta fecha no esta disponible',
4081
+ parseErrorLabel: 'Formato de fecha invalido',
4082
+ };
4083
+
4084
+ /**
4085
+ * Portuguese (pt) locale pack for the calendar's user-visible strings.
4086
+ * Pass into the `intl` input or via {@link provideCalendarIntl}. Per-field
4087
+ * merge semantics (§19.4) — unspecified fields fall back to English defaults.
4088
+ */
4089
+ const pt = {
4090
+ previousMonthLabel: 'Mes anterior',
4091
+ nextMonthLabel: 'Proximo mes',
4092
+ previousYearLabel: 'Ano anterior',
4093
+ nextYearLabel: 'Proximo ano',
4094
+ previousYearsLabel(yearsPerPage) {
4095
+ return yearsPerPage === 1 ? 'Ano anterior' : `${yearsPerPage} anos anteriores`;
4096
+ },
4097
+ nextYearsLabel(yearsPerPage) {
4098
+ return yearsPerPage === 1 ? 'Proximo ano' : `Proximos ${yearsPerPage} anos`;
4099
+ },
4100
+ todayLabel: 'Hoje',
4101
+ clearLabel: 'Limpar',
4102
+ applyLabel: 'Aplicar',
4103
+ openCalendarLabel: 'Abrir calendario',
4104
+ chooseDateLabel: 'Escolher data',
4105
+ calendarLabel: 'Calendario',
4106
+ monthViewLabel: 'visao de mes',
4107
+ yearViewLabel: 'visao de ano',
4108
+ decadeViewLabel: 'visao de decada',
4109
+ switchToMonthViewLabel(period) {
4110
+ return `${period}, clique para alternar para a visao de mes`;
4111
+ },
4112
+ switchToYearViewLabel(period) {
4113
+ return `${period}, clique para alternar para a visao de ano`;
4114
+ },
4115
+ navigatedTo(direction, period) {
4116
+ const word = direction === 'previous' ? 'anterior' : 'proximo';
4117
+ return `Navegou para ${period} ${word}`;
4118
+ },
4119
+ selectedAnnouncement(value) {
4120
+ return `Selecionado: ${value}`;
4121
+ },
4122
+ rangeStartAnnouncement(start) {
4123
+ return `Data inicial selecionada: ${start}. Selecione a data final.`;
4124
+ },
4125
+ rangeUpdateAnnouncement(start, end, lengthDays) {
4126
+ const noun = lengthDays === 1 ? 'dia' : 'dias';
4127
+ return `Intervalo selecionado: ${start} a ${end}, ${lengthDays} ${noun}`;
4128
+ },
4129
+ requiredError: 'Por favor selecione uma data',
4130
+ minDateError(date) {
4131
+ return `A data deve ser ${date} ou posterior`;
4132
+ },
4133
+ maxDateError(date) {
4134
+ return `A data deve ser ${date} ou anterior`;
4135
+ },
4136
+ invalidValueError: 'Valor de data invalido',
4137
+ invalidRangeError: 'Intervalo de datas invalido',
4138
+ dateFilterError: 'Esta data nao esta disponivel',
4139
+ parseErrorLabel: 'Formato de data invalido',
4140
+ };
4141
+
4142
+ /**
4143
+ * Japanese (ja) locale pack for the calendar's user-visible strings.
4144
+ * Pass into the `intl` input or via {@link provideCalendarIntl}. Per-field
4145
+ * merge semantics (§19.4) — unspecified fields fall back to English defaults.
4146
+ *
4147
+ * Note: Japanese has no grammatical plural form, so plural-marked methods
4148
+ * collapse to a single form.
4149
+ */
4150
+ const ja = {
4151
+ previousMonthLabel: '前月',
4152
+ nextMonthLabel: '翌月',
4153
+ previousYearLabel: '前年',
4154
+ nextYearLabel: '翌年',
4155
+ previousYearsLabel(yearsPerPage) {
4156
+ return `前の${yearsPerPage}年`;
4157
+ },
4158
+ nextYearsLabel(yearsPerPage) {
4159
+ return `次の${yearsPerPage}年`;
4160
+ },
4161
+ todayLabel: '今日',
4162
+ clearLabel: 'クリア',
4163
+ applyLabel: '適用',
4164
+ openCalendarLabel: 'カレンダーを開く',
4165
+ chooseDateLabel: '日付を選択',
4166
+ calendarLabel: 'カレンダー',
4167
+ monthViewLabel: '月表示',
4168
+ yearViewLabel: '年表示',
4169
+ decadeViewLabel: '10年表示',
4170
+ switchToMonthViewLabel(period) {
4171
+ return `${period}、クリックして月表示に切り替え`;
4172
+ },
4173
+ switchToYearViewLabel(period) {
4174
+ return `${period}、クリックして年表示に切り替え`;
4175
+ },
4176
+ navigatedTo(direction, period) {
4177
+ const word = direction === 'previous' ? '前の' : '次の';
4178
+ return `${word}${period}に移動しました`;
4179
+ },
4180
+ selectedAnnouncement(value) {
4181
+ return `選択済み: ${value}`;
4182
+ },
4183
+ rangeStartAnnouncement(start) {
4184
+ return `開始日が選択されました: ${start}。終了日を選択してください。`;
4185
+ },
4186
+ rangeUpdateAnnouncement(start, end, lengthDays) {
4187
+ return `期間が選択されました: ${start} から ${end}、${lengthDays}日間`;
4188
+ },
4189
+ requiredError: '日付を選択してください',
4190
+ minDateError(date) {
4191
+ return `日付は${date}以降である必要があります`;
4192
+ },
4193
+ maxDateError(date) {
4194
+ return `日付は${date}以前である必要があります`;
4195
+ },
4196
+ invalidValueError: '無効な日付値',
4197
+ invalidRangeError: '無効な日付範囲',
4198
+ dateFilterError: 'この日付は利用できません',
4199
+ parseErrorLabel: '無効な日付形式',
4200
+ };
4201
+
4202
+ function serializeCalendarValue(value, adapter) {
4203
+ if (value === null || value === undefined)
4204
+ return null;
4205
+ if (Array.isArray(value)) {
4206
+ return value.map((d) => adapter.toIso(adapter.startOfDay(d)));
4207
+ }
4208
+ if (typeof value === 'object' && value !== null && 'start' in value && 'end' in value) {
4209
+ const r = value;
4210
+ return {
4211
+ start: r.start ? adapter.toIso(adapter.startOfDay(r.start)) : null,
4212
+ end: r.end ? adapter.toIso(adapter.startOfDay(r.end)) : null,
4213
+ };
4214
+ }
4215
+ return adapter.toIso(adapter.startOfDay(value));
4216
+ }
4217
+
4218
+ const MS_PER_MINUTE = 60_000;
4219
+ const warnedLocales = new Set();
4220
+ function warnUnresolvableLocaleOnce(locale) {
4221
+ if (!isDevMode() || warnedLocales.has(locale))
4222
+ return;
4223
+ warnedLocales.add(locale);
4224
+ // eslint-disable-next-line no-console
4225
+ console.warn(`[ngx-tw/calendar] Locale "${locale}" is unknown to Intl.Locale.getWeekInfo(); falling back to Monday (1) as the first day of week (§19.2).`);
4226
+ }
4227
+ /** `Intl`-driven `Date` adapter. Zero runtime dependencies. */
4228
+ class NativeDateAdapter extends DateAdapter {
4229
+ /**
4230
+ * Locale is resolved lazily on first read so the constructor stays
4231
+ * SSR-safe (no `navigator` access at class-construction time — §31.1).
4232
+ */
4233
+ _locale = null;
4234
+ get locale() {
4235
+ if (this._locale !== null)
4236
+ return this._locale;
4237
+ this._locale =
4238
+ typeof navigator !== 'undefined' && typeof navigator.language === 'string'
4239
+ ? navigator.language
4240
+ : 'en-US';
4241
+ return this._locale;
4242
+ }
4243
+ today() {
4244
+ const now = new Date();
4245
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
4246
+ }
4247
+ /**
4248
+ * Constructs a `Date`. **`month` is 1-based** — `create(2026, 1, 1)` yields
4249
+ * January 1, 2026.
4250
+ */
4251
+ create(year, month, day) {
4252
+ const d = new Date(year, month - 1, day);
4253
+ // `new Date(year, ...)` on two-digit years maps to 19xx by convention;
4254
+ // re-anchor explicitly so consumers can construct years < 100.
4255
+ if (year >= 0 && year < 100) {
4256
+ d.setFullYear(year);
4257
+ }
4258
+ return d;
4259
+ }
4260
+ clone(date) {
4261
+ return new Date(date.getTime());
4262
+ }
4263
+ setLocale(locale) {
4264
+ this._locale = locale;
4265
+ }
4266
+ getLocale() {
4267
+ return this.locale;
4268
+ }
4269
+ getYear(date) {
4270
+ return date.getFullYear();
4271
+ }
4272
+ getMonth(date) {
4273
+ return date.getMonth();
4274
+ }
4275
+ getDate(date) {
4276
+ return date.getDate();
4277
+ }
4278
+ getDayOfWeek(date) {
4279
+ return date.getDay();
4280
+ }
4281
+ getNumDaysInMonth(date) {
4282
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
4283
+ }
4284
+ getHours(date) {
4285
+ return date.getHours();
4286
+ }
4287
+ getMinutes(date) {
4288
+ return date.getMinutes();
4289
+ }
4290
+ getSeconds(date) {
4291
+ return date.getSeconds();
4292
+ }
4293
+ withTime(date, hours, minutes, seconds) {
4294
+ const d = new Date(date.getTime());
4295
+ d.setHours(hours, minutes, seconds, 0);
4296
+ return d;
4297
+ }
4298
+ getFirstDayOfWeek() {
4299
+ // §19.2: derive from `Intl.Locale.getWeekInfo()` when available; fallback to
4300
+ // Monday (1) and emit a single dev-mode warning when the locale is unknown
4301
+ // to `Intl.Locale`. ICU/CLDR returns 1=Monday … 7=Sunday — we normalize to
4302
+ // the project's 0=Sunday … 6=Saturday convention.
4303
+ if (typeof Intl !== 'undefined' && typeof Intl.Locale === 'function') {
4304
+ try {
4305
+ const l = new Intl.Locale(this.locale);
4306
+ const info = (l.getWeekInfo?.() ?? l.weekInfo);
4307
+ if (info && typeof info.firstDay === 'number') {
4308
+ return info.firstDay === 7 ? 0 : info.firstDay;
4309
+ }
4310
+ }
4311
+ catch {
4312
+ warnUnresolvableLocaleOnce(this.locale);
4313
+ return 1;
4314
+ }
4315
+ }
4316
+ warnUnresolvableLocaleOnce(this.locale);
4317
+ return 1;
4318
+ }
4319
+ getMonthNames(style) {
4320
+ const fmt = new Intl.DateTimeFormat(this.locale, { month: style, timeZone: 'UTC' });
4321
+ return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(Date.UTC(2017, i, 1))));
4322
+ }
4323
+ getDayOfWeekNames(style) {
4324
+ const fmt = new Intl.DateTimeFormat(this.locale, { weekday: style, timeZone: 'UTC' });
4325
+ return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(Date.UTC(2017, 0, i + 1))));
4326
+ }
4327
+ getDateNames(style = 'short') {
4328
+ const fmt = new Intl.DateTimeFormat(this.locale, { day: style === 'narrow' ? 'numeric' : 'numeric', timeZone: 'UTC' });
4329
+ return Array.from({ length: 31 }, (_, i) => fmt.format(new Date(Date.UTC(2017, 0, i + 1))));
4330
+ }
4331
+ getYearName(date) {
4332
+ return new Intl.DateTimeFormat(this.locale, { year: 'numeric', timeZone: 'UTC' }).format(new Date(Date.UTC(date.getFullYear(), 0, 1)));
4333
+ }
4334
+ format(date, displayFormat) {
4335
+ if (!this.isValid(date))
4336
+ return '';
4337
+ const fmt = displayFormat?.dateTimeFormat ?? {
4338
+ year: 'numeric',
4339
+ month: 'short',
4340
+ day: 'numeric',
4341
+ };
4342
+ return new Intl.DateTimeFormat(this.locale, fmt).format(date);
4343
+ }
4344
+ addYears(date, years) {
4345
+ return this.addMonths(date, years * 12);
4346
+ }
4347
+ addMonths(date, months) {
4348
+ const d = new Date(date.getFullYear(), date.getMonth() + months, 1);
4349
+ const targetDay = Math.min(date.getDate(), new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate());
4350
+ return new Date(d.getFullYear(), d.getMonth(), targetDay);
4351
+ }
4352
+ addDays(date, days) {
4353
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
4354
+ }
4355
+ addMinutes(date, minutes) {
4356
+ return new Date(date.getTime() + minutes * MS_PER_MINUTE);
4357
+ }
4358
+ addHours(date, hours) {
4359
+ return new Date(date.getTime() + hours * 60 * MS_PER_MINUTE);
4360
+ }
4361
+ compare(first, second) {
4362
+ return (this.getYear(first) - this.getYear(second) ||
4363
+ this.getMonth(first) - this.getMonth(second) ||
4364
+ this.getDate(first) - this.getDate(second));
4365
+ }
4366
+ sameDate(first, second) {
4367
+ if (first && second) {
4368
+ const f = this.isValid(first);
4369
+ const s = this.isValid(second);
4370
+ if (f && s)
4371
+ return this.compare(first, second) === 0;
4372
+ return f === s;
4373
+ }
4374
+ return first === second;
4375
+ }
4376
+ /**
4377
+ * Start-of-day normalization. Rebuilds the date at `00:00:00.000` local time.
4378
+ * On DST spring-forward days where midnight is valid, this is a no-op beyond
4379
+ * zeroing h/m/s/ms. On DST fall-back days the repeated-01:00 hour does not
4380
+ * affect `00:00`, so this is also safe.
4381
+ *
4382
+ * The edge case of an entire DST-skipped midnight (rare — exists in some
4383
+ * historical zones) is resolved by `new Date(year, month, day)` selecting the
4384
+ * next valid instant forward.
4385
+ */
4386
+ startOfDay(date) {
4387
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
4388
+ }
4389
+ isValid(date) {
4390
+ return date instanceof Date && !isNaN(date.getTime());
4391
+ }
4392
+ invalid() {
4393
+ return new Date(NaN);
4394
+ }
4395
+ parse(value) {
4396
+ if (typeof value === 'number')
4397
+ return new Date(value);
4398
+ if (value instanceof Date)
4399
+ return this.clone(value);
4400
+ if (typeof value === 'string') {
4401
+ if (!value.trim())
4402
+ return null;
4403
+ const parsed = new Date(value);
4404
+ return this.isValid(parsed) ? parsed : null;
4405
+ }
4406
+ return null;
4407
+ }
4408
+ deserialize(value) {
4409
+ if (value == null || value === '')
4410
+ return null;
4411
+ if (value instanceof Date)
4412
+ return this.isValid(value) ? this.clone(value) : this.invalid();
4413
+ const parsed = this.parse(value);
4414
+ if (parsed && this.isValid(parsed))
4415
+ return parsed;
4416
+ return this.invalid();
4417
+ }
4418
+ toIso(date) {
4419
+ return [
4420
+ String(date.getFullYear()).padStart(4, '0'),
4421
+ String(date.getMonth() + 1).padStart(2, '0'),
4422
+ String(date.getDate()).padStart(2, '0'),
4423
+ ].join('-');
4424
+ }
4425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NativeDateAdapter, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
4426
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NativeDateAdapter });
4427
+ }
4428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NativeDateAdapter, decorators: [{
4429
+ type: Injectable
4430
+ }] });
4431
+ /** Provides the default `Date`-based adapter. */
4432
+ function provideNativeDateAdapter() {
4433
+ return makeEnvironmentProviders([
4434
+ NativeDateAdapter,
4435
+ { provide: DATE_ADAPTER, useExisting: NativeDateAdapter },
4436
+ ]);
4437
+ }
4438
+ /** Provides a custom `DateAdapter` implementation. */
4439
+ function provideTwCalendar(config) {
4440
+ return makeEnvironmentProviders([
4441
+ config.adapter,
4442
+ { provide: DATE_ADAPTER, useExisting: config.adapter },
4443
+ ...(config.extraProviders ?? []),
4444
+ ]);
4445
+ }
4446
+
4447
+ /**
4448
+ * Convenience class that satisfies the `DateRange<D>` interface and carries
4449
+ * `complete` / `empty` getters. Interchangeable with a plain
4450
+ * `{ start, end }` object anywhere a `DateRange<D>` is expected.
4451
+ */
4452
+ class TwDateRange {
4453
+ start;
4454
+ end;
4455
+ constructor(start, end) {
4456
+ this.start = start;
4457
+ this.end = end;
4458
+ }
4459
+ /** True when both endpoints are set. */
4460
+ get complete() {
4461
+ return this.start !== null && this.end !== null;
4462
+ }
4463
+ /** True when neither endpoint is set. */
4464
+ get empty() {
4465
+ return this.start === null && this.end === null;
4466
+ }
4467
+ }
4468
+ /** @internal Normalises any range-shaped value into a `TwDateRange` instance. */
4469
+ function toTwDateRange(value) {
4470
+ if (value === null || value === undefined)
4471
+ return null;
4472
+ if (value instanceof TwDateRange)
4473
+ return value;
4474
+ return new TwDateRange(value.start ?? null, value.end ?? null);
4475
+ }
4476
+
4477
+ /**
4478
+ * Generated bundle index. Do not edit.
4479
+ */
4480
+
4481
+ export { CALENDAR_SELECTION_STRATEGY, CalendarCellComponent, CalendarComponent, CalendarHeaderComponent, CalendarIntl, CalendarMultipleDirective, CalendarPresetsDirective, CalendarRangeDirective, CalendarSelectionStrategy, CalendarSingleDirective, CalendarViewBase, DATE_ADAPTER, DATE_FORMATS, DATE_SERIALIZATION, DAYS_PER_WEEK, DateAdapter, MONTHS_PER_ROW, MonthViewComponent, MultiSelectionStrategy, NativeDateAdapter, RangeSelectionStrategy, SingleSelectionStrategy, TZ_OVERRIDE, TwDateRange, WEEKS_PER_MONTH, WeekSelectionStrategy, YEARS_PER_PAGE, YEARS_PER_ROW, YearViewComponent, YearsViewComponent, calendarRequiredValidator, calendarValidator, createCalendarCell, createDateRange, createGrid, de, emptyCalendarValue, es, fr, getFirstDayOfMonth, getMultiYearStartingYear, getWeekdayHeaders, isCalendarValueEmpty, isDateDisabled, isDateInRange, isMonthDisabled, isYearDisabled, ja, navigateGrid, provideCalendarIntl, provideCalendarSelectionStrategy, provideMultiSelectionStrategy, provideNativeDateAdapter, provideRangeSelectionStrategy, provideSingleSelectionStrategy, provideTwCalendar, provideWeekSelectionStrategy, pt, serializeCalendarValue, toTwDateRange, yearsPerPage };
4482
+ //# sourceMappingURL=cdevhub-ngx-tw-calendar.mjs.map