@proyecto-viviana/solid-stately 0.2.7 → 0.4.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 (254) hide show
  1. package/README.md +30 -283
  2. package/dist/autocomplete/createAutocompleteState.d.ts +2 -2
  3. package/dist/autocomplete/index.d.ts +1 -1
  4. package/dist/calendar/IncompleteDate.d.ts +61 -0
  5. package/dist/calendar/createCalendarState.d.ts +41 -16
  6. package/dist/calendar/createDateFieldState.d.ts +39 -9
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +37 -10
  9. package/dist/calendar/createTimeFieldState.d.ts +34 -7
  10. package/dist/calendar/index.d.ts +7 -6
  11. package/dist/checkbox/createCheckboxGroupState.d.ts +6 -6
  12. package/dist/checkbox/index.d.ts +1 -1
  13. package/dist/collections/ListCollection.d.ts +1 -1
  14. package/dist/collections/createListState.d.ts +11 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +22 -10
  17. package/dist/collections/getChildNodes.d.ts +11 -0
  18. package/dist/collections/index.d.ts +6 -5
  19. package/dist/collections/types.d.ts +12 -12
  20. package/dist/color/Color.d.ts +1 -1
  21. package/dist/color/createColorAreaState.d.ts +4 -2
  22. package/dist/color/createColorFieldState.d.ts +38 -12
  23. package/dist/color/createColorSliderState.d.ts +9 -2
  24. package/dist/color/createColorWheelState.d.ts +13 -2
  25. package/dist/color/getColorChannels.d.ts +1 -1
  26. package/dist/color/index.d.ts +6 -6
  27. package/dist/color/types.d.ts +4 -4
  28. package/dist/combobox/createComboBoxState.d.ts +34 -13
  29. package/dist/combobox/index.d.ts +1 -1
  30. package/dist/data/createAsyncList.d.ts +4 -4
  31. package/dist/data/createListData.d.ts +2 -2
  32. package/dist/data/createTreeData.d.ts +6 -2
  33. package/dist/data/index.d.ts +3 -3
  34. package/dist/datepicker/index.d.ts +2 -4
  35. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  36. package/dist/disclosure/index.d.ts +1 -1
  37. package/dist/dnd/createDragState.d.ts +2 -2
  38. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  39. package/dist/dnd/createDropState.d.ts +2 -2
  40. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  41. package/dist/dnd/index.d.ts +5 -5
  42. package/dist/dnd/types.d.ts +19 -17
  43. package/dist/form/createFormValidationState.d.ts +3 -3
  44. package/dist/form/index.d.ts +1 -1
  45. package/dist/grid/createGridState.d.ts +2 -2
  46. package/dist/grid/index.d.ts +2 -2
  47. package/dist/grid/types.d.ts +27 -11
  48. package/dist/index.d.ts +31 -30
  49. package/dist/index.js +9077 -7246
  50. package/dist/index.js.map +1 -1
  51. package/dist/menu/index.d.ts +2 -2
  52. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  53. package/dist/numberfield/index.d.ts +1 -1
  54. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  55. package/dist/overlays/index.d.ts +1 -1
  56. package/dist/radio/createRadioGroupState.d.ts +7 -7
  57. package/dist/radio/index.d.ts +1 -1
  58. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  59. package/dist/searchfield/index.d.ts +2 -2
  60. package/dist/select/createSelectState.d.ts +14 -8
  61. package/dist/select/index.d.ts +1 -1
  62. package/dist/selection/Selection.d.ts +16 -0
  63. package/dist/selection/SelectionManager.d.ts +110 -0
  64. package/dist/selection/createMultipleSelectionState.d.ts +59 -0
  65. package/dist/selection/index.d.ts +6 -5
  66. package/dist/slider/createSliderState.d.ts +3 -3
  67. package/dist/slider/index.d.ts +2 -2
  68. package/dist/steplist/createStepListState.d.ts +48 -0
  69. package/dist/steplist/index.d.ts +1 -0
  70. package/dist/table/TableCollection.d.ts +33 -3
  71. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  72. package/dist/table/createTableState.d.ts +2 -2
  73. package/dist/table/createTreeGridState.d.ts +15 -0
  74. package/dist/table/index.d.ts +6 -3
  75. package/dist/table/types.d.ts +68 -10
  76. package/dist/tabs/createTabListState.d.ts +5 -5
  77. package/dist/tabs/index.d.ts +1 -1
  78. package/dist/textfield/createTextFieldState.d.ts +2 -2
  79. package/dist/textfield/index.d.ts +1 -1
  80. package/dist/toast/createToastState.d.ts +24 -2
  81. package/dist/toast/index.d.ts +1 -1
  82. package/dist/toggle/createToggleGroupState.d.ts +4 -4
  83. package/dist/toggle/createToggleState.d.ts +2 -2
  84. package/dist/toggle/index.d.ts +2 -2
  85. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  86. package/dist/tooltip/index.d.ts +1 -1
  87. package/dist/tree/TreeCollection.d.ts +2 -2
  88. package/dist/tree/createTreeState.d.ts +2 -2
  89. package/dist/tree/index.d.ts +3 -3
  90. package/dist/tree/types.d.ts +27 -13
  91. package/dist/utils/index.d.ts +1 -1
  92. package/dist/utils/reactivity.d.ts +1 -1
  93. package/package.json +29 -30
  94. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  95. package/src/autocomplete/createAutocompleteState.ts +24 -30
  96. package/src/autocomplete/index.d.ts +6 -2
  97. package/src/autocomplete/index.ts +1 -1
  98. package/src/calendar/IncompleteDate.ts +430 -0
  99. package/src/calendar/createCalendarState.d.ts +139 -117
  100. package/src/calendar/createCalendarState.ts +308 -94
  101. package/src/calendar/createDateFieldState.d.ts +130 -95
  102. package/src/calendar/createDateFieldState.ts +447 -334
  103. package/src/calendar/createDatePickerState.ts +347 -0
  104. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  105. package/src/calendar/createRangeCalendarState.ts +411 -85
  106. package/src/calendar/createTimeFieldState.d.ts +81 -79
  107. package/src/calendar/createTimeFieldState.ts +187 -108
  108. package/src/calendar/index.d.ts +80 -7
  109. package/src/calendar/index.ts +23 -12
  110. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  111. package/src/checkbox/createCheckboxGroupState.ts +20 -29
  112. package/src/checkbox/index.d.ts +6 -2
  113. package/src/checkbox/index.ts +1 -1
  114. package/src/collections/ListCollection.d.ts +25 -22
  115. package/src/collections/ListCollection.ts +6 -6
  116. package/src/collections/createListState.d.ts +73 -58
  117. package/src/collections/createListState.ts +67 -35
  118. package/src/collections/createMenuState.d.ts +36 -31
  119. package/src/collections/createMenuState.ts +21 -12
  120. package/src/collections/createSelectionState.d.ts +78 -65
  121. package/src/collections/createSelectionState.ts +66 -52
  122. package/src/collections/getChildNodes.ts +98 -0
  123. package/src/collections/index.d.ts +38 -6
  124. package/src/collections/index.ts +14 -10
  125. package/src/collections/types.d.ts +101 -101
  126. package/src/collections/types.ts +12 -12
  127. package/src/color/Color.d.ts +20 -5
  128. package/src/color/Color.ts +357 -254
  129. package/src/color/createColorAreaState.d.ts +67 -65
  130. package/src/color/createColorAreaState.ts +21 -13
  131. package/src/color/createColorFieldState.d.ts +46 -44
  132. package/src/color/createColorFieldState.ts +285 -149
  133. package/src/color/createColorSliderState.d.ts +58 -56
  134. package/src/color/createColorSliderState.ts +45 -14
  135. package/src/color/createColorWheelState.d.ts +42 -40
  136. package/src/color/createColorWheelState.ts +101 -64
  137. package/src/color/getColorChannels.ts +7 -5
  138. package/src/color/index.d.ts +36 -7
  139. package/src/color/index.ts +6 -9
  140. package/src/color/types.d.ts +68 -63
  141. package/src/color/types.ts +11 -19
  142. package/src/combobox/createComboBoxState.d.ts +107 -105
  143. package/src/combobox/createComboBoxState.ts +184 -89
  144. package/src/combobox/index.d.ts +10 -2
  145. package/src/combobox/index.ts +1 -1
  146. package/src/data/createAsyncList.ts +89 -63
  147. package/src/data/createListData.ts +67 -67
  148. package/src/data/createTreeData.ts +304 -91
  149. package/src/data/index.ts +3 -3
  150. package/src/datepicker/index.ts +6 -10
  151. package/src/disclosure/createDisclosureState.d.ts +47 -43
  152. package/src/disclosure/createDisclosureState.ts +7 -21
  153. package/src/disclosure/index.d.ts +10 -2
  154. package/src/disclosure/index.ts +1 -1
  155. package/src/dnd/createDragState.d.ts +51 -42
  156. package/src/dnd/createDragState.ts +12 -14
  157. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  158. package/src/dnd/createDraggableCollectionState.ts +14 -21
  159. package/src/dnd/createDropState.d.ts +64 -45
  160. package/src/dnd/createDropState.ts +22 -32
  161. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  162. package/src/dnd/createDroppableCollectionState.ts +40 -43
  163. package/src/dnd/index.d.ts +55 -6
  164. package/src/dnd/index.ts +6 -18
  165. package/src/dnd/types.d.ts +170 -157
  166. package/src/dnd/types.ts +24 -22
  167. package/src/form/createFormValidationState.d.ts +47 -45
  168. package/src/form/createFormValidationState.ts +30 -63
  169. package/src/form/index.d.ts +16 -2
  170. package/src/form/index.ts +1 -1
  171. package/src/grid/createGridState.d.ts +7 -4
  172. package/src/grid/createGridState.ts +78 -51
  173. package/src/grid/index.d.ts +3 -3
  174. package/src/grid/index.ts +2 -8
  175. package/src/grid/types.d.ts +136 -129
  176. package/src/grid/types.ts +34 -18
  177. package/src/index.d.ts +311 -26
  178. package/src/index.ts +59 -89
  179. package/src/menu/index.ts +2 -4
  180. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  181. package/src/numberfield/createNumberFieldState.ts +26 -31
  182. package/src/numberfield/index.d.ts +6 -2
  183. package/src/numberfield/index.ts +1 -1
  184. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  185. package/src/overlays/createOverlayTriggerState.ts +3 -4
  186. package/src/overlays/index.d.ts +6 -2
  187. package/src/overlays/index.ts +1 -1
  188. package/src/radio/createRadioGroupState.d.ts +54 -52
  189. package/src/radio/createRadioGroupState.ts +27 -29
  190. package/src/radio/index.d.ts +7 -2
  191. package/src/radio/index.ts +1 -1
  192. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  193. package/src/searchfield/createSearchFieldState.ts +5 -11
  194. package/src/searchfield/index.d.ts +3 -3
  195. package/src/searchfield/index.ts +2 -5
  196. package/src/select/createSelectState.d.ts +63 -61
  197. package/src/select/createSelectState.ts +42 -29
  198. package/src/select/index.d.ts +2 -2
  199. package/src/select/index.ts +1 -5
  200. package/src/selection/Selection.ts +28 -0
  201. package/src/selection/SelectionManager.ts +526 -0
  202. package/src/selection/createMultipleSelectionState.ts +247 -0
  203. package/src/selection/index.ts +12 -7
  204. package/src/slider/createSliderState.d.ts +60 -60
  205. package/src/slider/createSliderState.ts +55 -33
  206. package/src/slider/index.d.ts +3 -3
  207. package/src/slider/index.ts +2 -6
  208. package/src/ssr/index.d.ts +1 -1
  209. package/src/ssr/index.ts +2 -2
  210. package/src/steplist/createStepListState.ts +191 -0
  211. package/src/steplist/index.ts +5 -0
  212. package/src/table/TableCollection.d.ts +42 -40
  213. package/src/table/TableCollection.ts +266 -99
  214. package/src/table/createTableColumnResizeState.ts +242 -0
  215. package/src/table/createTableState.d.ts +7 -4
  216. package/src/table/createTableState.ts +30 -7
  217. package/src/table/createTreeGridState.ts +146 -0
  218. package/src/table/index.d.ts +14 -4
  219. package/src/table/index.ts +13 -3
  220. package/src/table/types.d.ts +98 -92
  221. package/src/table/types.ts +83 -13
  222. package/src/tabs/createTabListState.d.ts +59 -57
  223. package/src/tabs/createTabListState.ts +34 -39
  224. package/src/tabs/index.d.ts +8 -2
  225. package/src/tabs/index.ts +1 -1
  226. package/src/textfield/createTextFieldState.d.ts +16 -14
  227. package/src/textfield/createTextFieldState.ts +7 -18
  228. package/src/textfield/index.d.ts +6 -2
  229. package/src/textfield/index.ts +1 -1
  230. package/src/toast/createToastState.d.ts +92 -86
  231. package/src/toast/createToastState.ts +90 -52
  232. package/src/toast/index.d.ts +12 -2
  233. package/src/toast/index.ts +2 -1
  234. package/src/toggle/createToggleGroupState.ts +11 -21
  235. package/src/toggle/createToggleState.d.ts +19 -19
  236. package/src/toggle/createToggleState.ts +2 -16
  237. package/src/toggle/index.d.ts +2 -2
  238. package/src/toggle/index.ts +2 -6
  239. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  240. package/src/tooltip/createTooltipTriggerState.ts +12 -11
  241. package/src/tooltip/index.d.ts +7 -2
  242. package/src/tooltip/index.ts +1 -1
  243. package/src/tree/TreeCollection.d.ts +26 -23
  244. package/src/tree/TreeCollection.ts +29 -22
  245. package/src/tree/createTreeState.d.ts +7 -4
  246. package/src/tree/createTreeState.ts +70 -41
  247. package/src/tree/index.d.ts +10 -4
  248. package/src/tree/index.ts +3 -3
  249. package/src/tree/types.d.ts +131 -126
  250. package/src/tree/types.ts +30 -13
  251. package/src/utils/index.d.ts +2 -2
  252. package/src/utils/index.ts +1 -1
  253. package/src/utils/reactivity.d.ts +2 -2
  254. package/src/utils/reactivity.ts +3 -3
@@ -3,38 +3,51 @@
3
3
  *
4
4
  * Provides state management for date field components with segment-based editing.
5
5
  * Based on @react-stately/datepicker useDateFieldState
6
+ *
7
+ * Editing is modeled with an {@link IncompleteDate} display value so that invalid
8
+ * intermediate dates (e.g. February 31st, produced by editing the day before the
9
+ * month) are held as-typed while the field is focused and only constrained on blur
10
+ * via `confirmPlaceholder`, matching upstream behavior. Out-of-range values are
11
+ * never snapped to `minValue`/`maxValue`; they are kept and reported as invalid.
6
12
  */
7
13
 
8
- import { createSignal, createMemo, type Accessor } from 'solid-js';
14
+ import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
9
15
  import {
10
- type CalendarDate,
16
+ type AnyDateTime,
17
+ type Calendar,
11
18
  type CalendarDateTime,
12
- type ZonedDateTime,
13
19
  type DateValue,
20
+ GregorianCalendar,
14
21
  today,
15
22
  getLocalTimeZone,
16
23
  DateFormatter,
17
24
  toCalendarDate as intlToCalendarDate,
18
25
  toCalendarDateTime,
19
- } from '@internationalized/date';
20
- import { access, type MaybeAccessor } from '../utils';
21
- import type { ValidationState } from './createCalendarState';
22
-
23
- // ============================================
24
- // TYPES
25
- // ============================================
26
+ } from "@internationalized/date";
27
+ import { access, type MaybeAccessor } from "../utils";
28
+ import type { ValidationState } from "./createCalendarState";
29
+ import { IncompleteDate } from "./IncompleteDate";
30
+ import {
31
+ createFormValidationState,
32
+ DEFAULT_VALIDATION_RESULT,
33
+ VALID_VALIDITY_STATE,
34
+ type ValidationBehavior,
35
+ type ValidationFunction,
36
+ type ValidationResult,
37
+ type ValidityState,
38
+ } from "../form";
26
39
 
27
40
  export type DateSegmentType =
28
- | 'year'
29
- | 'month'
30
- | 'day'
31
- | 'hour'
32
- | 'minute'
33
- | 'second'
34
- | 'dayPeriod'
35
- | 'era'
36
- | 'timeZoneName'
37
- | 'literal';
41
+ | "year"
42
+ | "month"
43
+ | "day"
44
+ | "hour"
45
+ | "minute"
46
+ | "second"
47
+ | "dayPeriod"
48
+ | "era"
49
+ | "timeZoneName"
50
+ | "literal";
38
51
 
39
52
  export interface DateSegment {
40
53
  /** The type of segment. */
@@ -42,7 +55,7 @@ export interface DateSegment {
42
55
  /** The text content of the segment. */
43
56
  text: string;
44
57
  /** The numeric value of the segment (if applicable). */
45
- value?: number;
58
+ value?: number | null;
46
59
  /** The minimum value for the segment. */
47
60
  minValue?: number;
48
61
  /** The maximum value for the segment. */
@@ -72,18 +85,26 @@ export interface DateFieldStateProps<T extends DateValue = DateValue> {
72
85
  isReadOnly?: MaybeAccessor<boolean>;
73
86
  /** Whether the field is required. */
74
87
  isRequired?: MaybeAccessor<boolean>;
88
+ /** Whether the value is invalid (controlled). */
89
+ isInvalid?: boolean;
75
90
  /** The locale to use for formatting. */
76
91
  locale?: string;
77
92
  /** The granularity of the date/time (day, hour, minute, second). */
78
- granularity?: 'day' | 'hour' | 'minute' | 'second';
93
+ granularity?: "day" | "hour" | "minute" | "second";
79
94
  /** Whether to show the hour in 12 or 24 hour format. */
80
95
  hourCycle?: 12 | 24;
81
96
  /** Whether to hide the time zone. */
82
97
  hideTimeZone?: boolean;
83
98
  /** The placeholder date (determines segment structure). */
84
99
  placeholderValue?: DateValue;
100
+ /** Whether to force leading zeroes in month/day segments. */
101
+ shouldForceLeadingZeros?: boolean;
85
102
  /** Validation state. */
86
103
  validationState?: MaybeAccessor<ValidationState | undefined>;
104
+ /** Validation behavior mode. */
105
+ validationBehavior?: ValidationBehavior;
106
+ /** Custom validation function. */
107
+ validate?: ValidationFunction<T>;
87
108
  /** Description text. */
88
109
  description?: string;
89
110
  /** Error message. */
@@ -92,6 +113,8 @@ export interface DateFieldStateProps<T extends DateValue = DateValue> {
92
113
  allowsNonContiguousRanges?: boolean;
93
114
  /** Whether to create a date or datetime. */
94
115
  createCalendar?: (name: string) => unknown;
116
+ /** Whether a date is unavailable. */
117
+ isDateUnavailable?: (date: DateValue) => boolean;
95
118
  }
96
119
 
97
120
  export interface DateFieldState<T extends DateValue = DateValue> {
@@ -124,294 +147,339 @@ export interface DateFieldState<T extends DateValue = DateValue> {
124
147
  /** The validation state. */
125
148
  validationState: Accessor<ValidationState | undefined>;
126
149
  /** The granularity. */
127
- granularity: 'day' | 'hour' | 'minute' | 'second';
150
+ granularity: "day" | "hour" | "minute" | "second";
128
151
  /** Whether the value is invalid. */
129
152
  isInvalid: Accessor<boolean>;
153
+ /** Realtime validation results, including native and custom constraints. */
154
+ realtimeValidation: Accessor<ValidationResult>;
155
+ /** Currently displayed validation results. */
156
+ displayValidation: Accessor<ValidationResult>;
157
+ /** Updates the current validation result. */
158
+ updateValidation: (result: ValidationResult) => void;
159
+ /** Resets displayed validation to valid. */
160
+ resetValidation: () => void;
161
+ /** Commits realtime validation to displayed validation. */
162
+ commitValidation: () => void;
130
163
  /** The locale. */
131
164
  locale: string;
132
165
  /** The time zone. */
133
166
  timeZone: string;
167
+ /** The default value. */
168
+ defaultValue: T | null;
134
169
  }
135
170
 
136
- // ============================================
137
- // IMPLEMENTATION
138
- // ============================================
171
+ type HourCycle = "h11" | "h12" | "h23" | "h24";
139
172
 
140
173
  /**
141
174
  * Provides state management for a date field component.
142
175
  */
143
- export function createDateFieldState<T extends DateValue = CalendarDate>(
144
- props: DateFieldStateProps<T> = {}
176
+ export function createDateFieldState<T extends DateValue = DateValue>(
177
+ props: DateFieldStateProps<T> = {},
145
178
  ): DateFieldState<T> {
146
179
  const timeZone = getLocalTimeZone();
147
- const locale = props.locale ?? 'en-US';
148
- const granularity = props.granularity ?? 'day';
180
+ const locale = props.locale ?? "en-US";
181
+ const granularity = props.granularity ?? "day";
182
+ const calendar: Calendar = new GregorianCalendar();
183
+ const hourCycle = resolveHourCycle(locale, props.hourCycle);
184
+ const displaySegments = computeDisplaySegments(granularity, hourCycle);
185
+
186
+ const getPlaceholderValue = (): DateValue => {
187
+ const base = props.placeholderValue ?? today(timeZone);
188
+ return granularity === "day" ? intlToCalendarDate(base) : toCalendarDateTime(base);
189
+ };
149
190
 
150
- // State signals
151
- const [internalValue, setInternalValue] = createSignal<T | null>(
152
- props.defaultValue ?? null
153
- );
191
+ const toSource = (v: DateValue | null): Partial<Omit<AnyDateTime, "copy">> | null =>
192
+ (v as Partial<Omit<AnyDateTime, "copy">> | null) ?? null;
154
193
 
155
- // Track partial values during editing
156
- const [placeholderDate, setPlaceholderDate] = createSignal<Partial<DateParts>>({});
194
+ // State signals
195
+ const initialValue = props.defaultValue ?? null;
196
+ const [internalValue, setInternalValue] = createSignal<T | null>(initialValue);
157
197
 
158
- // Controlled vs uncontrolled value
198
+ // Controlled vs uncontrolled committed value
159
199
  const value = createMemo<T | null>(() => {
160
200
  const controlled = access(props.value);
161
201
  return controlled !== undefined ? controlled : internalValue();
162
202
  });
163
203
 
164
- // The effective date value
165
- const dateValue = createMemo<DateValue | null>(() => {
166
- const v = value();
167
- if (v) return v;
168
-
169
- // Build from placeholder parts
170
- const parts = placeholderDate();
171
- const placeholder = props.placeholderValue ?? today(timeZone);
172
-
173
- if (Object.keys(parts).length === 0) return null;
174
-
175
- // Create a date from the parts
176
- const year = parts.year ?? placeholder.year;
177
- const month = parts.month ?? placeholder.month;
178
- const day = parts.day ?? placeholder.day;
204
+ // The display override: holds the (possibly invalid/incomplete) value being edited.
205
+ const [displayValue, setDisplayValue] = createSignal<IncompleteDate>(
206
+ new IncompleteDate(calendar, hourCycle, toSource(value())),
207
+ );
179
208
 
180
- if (granularity === 'day') {
181
- return intlToCalendarDate(placeholder).set({ year, month, day });
209
+ // Reset the display override whenever the committed value changes from the outside
210
+ // (e.g. a controlled `value` prop update).
211
+ let lastSyncedValue = value();
212
+ createEffect(() => {
213
+ const v = value();
214
+ if (v !== lastSyncedValue) {
215
+ lastSyncedValue = v;
216
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(v)));
182
217
  }
183
-
184
- // For time granularities
185
- const hour = parts.hour ?? 0;
186
- const minute = parts.minute ?? 0;
187
- const second = parts.second ?? 0;
188
-
189
- return toCalendarDateTime(placeholder).set({
190
- year,
191
- month,
192
- day,
193
- hour,
194
- minute,
195
- second,
196
- }) as unknown as T;
197
218
  });
198
219
 
199
220
  // Derived states
200
221
  const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
201
222
  const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
202
223
  const isRequired = createMemo(() => access(props.isRequired) ?? false);
203
- const validationState = createMemo(() => access(props.validationState));
204
-
205
- // Check if value is invalid
206
- const isInvalid = createMemo(() => {
224
+ const explicitValidationState = createMemo(() => access(props.validationState));
225
+ const validationBehavior = () => props.validationBehavior ?? "native";
226
+ const builtinValidation = createMemo<ValidationResult>(() => {
207
227
  const v = value();
208
- if (!v) return false;
228
+ if (!v) return DEFAULT_VALIDATION_RESULT;
209
229
 
210
230
  const minValue = access(props.minValue);
211
231
  const maxValue = access(props.maxValue);
232
+ const validationDetails: ValidityState = { ...VALID_VALIDITY_STATE };
233
+ const validationErrors: string[] = [];
212
234
 
213
- if (minValue && v.compare(minValue) < 0) return true;
214
- if (maxValue && v.compare(maxValue) > 0) return true;
235
+ if (minValue && v.compare(minValue) < 0) {
236
+ validationDetails.rangeUnderflow = true;
237
+ validationErrors.push("Value is below the minimum date.");
238
+ }
215
239
 
216
- return validationState() === 'invalid';
217
- });
240
+ if (maxValue && v.compare(maxValue) > 0) {
241
+ validationDetails.rangeOverflow = true;
242
+ validationErrors.push("Value is above the maximum date.");
243
+ }
218
244
 
219
- // Generate segments based on granularity and locale
220
- const segments = createMemo<DateSegment[]>(() => {
221
- const v = value();
222
- const placeholder = props.placeholderValue ?? today(timeZone);
223
- const parts = placeholderDate();
245
+ if (props.isDateUnavailable?.(v)) {
246
+ validationDetails.customError = true;
247
+ validationErrors.push("Date is unavailable.");
248
+ }
224
249
 
225
- const segs: DateSegment[] = [];
250
+ if (validationErrors.length === 0) {
251
+ return DEFAULT_VALIDATION_RESULT;
252
+ }
226
253
 
227
- // Determine format options based on granularity
228
- const formatOptions: Intl.DateTimeFormatOptions = {
229
- year: 'numeric',
230
- month: '2-digit',
231
- day: '2-digit',
254
+ validationDetails.valid = false;
255
+ return {
256
+ isInvalid: true,
257
+ validationDetails,
258
+ validationErrors,
232
259
  };
260
+ });
261
+ const validation = createFormValidationState<T>({
262
+ get value() {
263
+ return value();
264
+ },
265
+ get isInvalid() {
266
+ return props.isInvalid;
267
+ },
268
+ get validationState() {
269
+ return explicitValidationState();
270
+ },
271
+ get validationBehavior() {
272
+ return validationBehavior();
273
+ },
274
+ get validate() {
275
+ return props.validate;
276
+ },
277
+ get builtinValidation() {
278
+ return builtinValidation();
279
+ },
280
+ });
281
+ const isInvalid = createMemo(() => validation.displayValidation().isInvalid);
282
+ const validationState = createMemo<ValidationState | undefined>(
283
+ () => explicitValidationState() ?? (isInvalid() ? "invalid" : undefined),
284
+ );
233
285
 
234
- if (granularity !== 'day') {
235
- formatOptions.hour = '2-digit';
236
- formatOptions.minute = '2-digit';
237
- if (granularity === 'second') {
238
- formatOptions.second = '2-digit';
239
- }
240
- if (props.hourCycle) {
241
- formatOptions.hourCycle = props.hourCycle === 12 ? 'h12' : 'h23';
242
- }
243
- }
286
+ // The effective date value, derived from the display override.
287
+ const dateValue = createMemo<DateValue | null>(() => {
288
+ const v = value();
289
+ if (v) return v;
290
+ const dv = displayValue();
291
+ if (dv.isCleared(displaySegments)) return null;
292
+ return dv.toValue(getPlaceholderValue());
293
+ });
294
+
295
+ // Generate segments from the display override (so invalid intermediate values render as typed).
296
+ const segments = createMemo<DateSegment[]>(() => {
297
+ const dv = displayValue();
298
+ const base = value() ?? getPlaceholderValue();
299
+ const resolved = dv.toValue(base);
300
+ const jsDate = resolved.toDate(timeZone);
244
301
 
302
+ const shouldForceLeadingZeros = props.shouldForceLeadingZeros === true;
303
+ const formatOptions = buildFormatOptions(granularity, shouldForceLeadingZeros, props.hourCycle);
245
304
  const formatter = new DateFormatter(locale, formatOptions);
246
- const dateToFormat = v ?? placeholder;
247
- const formattedParts = formatter.formatToParts(dateToFormat.toDate(timeZone));
305
+ const resolvedOptions = formatter.resolvedOptions() as unknown as Record<string, string>;
306
+ const parts = formatter.formatToParts(jsDate);
248
307
 
249
- for (const part of formattedParts) {
308
+ const numberFormatter = new Intl.NumberFormat(locale, { useGrouping: false });
309
+ const twoDigitFormatter = new Intl.NumberFormat(locale, {
310
+ useGrouping: false,
311
+ minimumIntegerDigits: 2,
312
+ });
313
+
314
+ const result: DateSegment[] = [];
315
+ for (const part of parts) {
250
316
  const type = mapPartType(part.type);
251
317
 
252
- if (type === 'literal') {
253
- segs.push({
254
- type: 'literal',
318
+ if (type === "literal") {
319
+ result.push({
320
+ type: "literal",
255
321
  text: part.value,
256
322
  isEditable: false,
257
323
  isPlaceholder: false,
258
324
  placeholder: part.value,
259
325
  });
260
- } else if (type) {
261
- const segValue = getSegmentValue(v, type);
262
- const placeholderValue = getSegmentValue(placeholder, type);
263
- const partValue = parts[type as keyof DateParts];
264
- const hasValue = v !== null || partValue !== undefined;
265
-
266
- segs.push({
267
- type,
268
- text: hasValue ? part.value : getPlaceholderText(type),
269
- value: segValue ?? partValue,
270
- minValue: getMinValue(type),
271
- maxValue: getMaxValue(type, v ?? placeholder),
272
- isEditable: !isDisabled() && !isReadOnly(),
273
- isPlaceholder: !hasValue,
274
- placeholder: getPlaceholderText(type),
275
- });
326
+ continue;
327
+ }
328
+ if (!type) continue;
329
+
330
+ const editable = isEditableSegment(type);
331
+ const rawValue = rawSegmentValue(dv, type);
332
+ const isPlaceholder = editable && rawValue == null;
333
+ const limits = dv.getSegmentLimits(type);
334
+ const placeholderText = getPlaceholderText(
335
+ type,
336
+ locale,
337
+ granularity,
338
+ shouldForceLeadingZeros,
339
+ );
340
+
341
+ // For numeric date/time segments, render the raw edited value (e.g. "31" even
342
+ // when the resolved date is Feb 28) using a NumberFormatter, mirroring upstream.
343
+ let text = part.value;
344
+ if (
345
+ (type === "year" || type === "month" || type === "day" || type === "hour") &&
346
+ typeof rawValue === "number"
347
+ ) {
348
+ const numeric = rawValue ?? 0;
349
+ text =
350
+ resolvedOptions[type] === "2-digit"
351
+ ? twoDigitFormatter.format(numeric)
352
+ : numberFormatter.format(numeric);
276
353
  }
354
+
355
+ result.push({
356
+ type,
357
+ text: isPlaceholder ? placeholderText : text,
358
+ value: limits?.value,
359
+ minValue: limits?.minValue,
360
+ maxValue: limits?.maxValue,
361
+ isEditable: editable && !isDisabled() && !isReadOnly(),
362
+ isPlaceholder,
363
+ placeholder: placeholderText,
364
+ });
277
365
  }
278
366
 
279
- return segs;
367
+ return result;
280
368
  });
281
369
 
282
- // Set value with onChange callback
283
- const setValue = (newValue: T | null) => {
284
- if (isDisabled() || isReadOnly()) return;
285
-
370
+ // Commit a concrete value (updates the controlled/uncontrolled value and fires onChange).
371
+ const commitDate = (newValue: T | null) => {
286
372
  const controlled = access(props.value);
287
373
  if (controlled === undefined) {
374
+ lastSyncedValue = newValue;
288
375
  setInternalValue(() => newValue);
289
376
  }
290
-
291
- if (props.onChange) {
292
- props.onChange(newValue);
293
- }
294
-
295
- // Clear placeholder parts
296
- setPlaceholderDate({});
377
+ props.onChange?.(newValue);
297
378
  };
298
379
 
299
- // Set a specific segment value
300
- const setSegment = (type: DateSegmentType, newValue: number) => {
380
+ // Set value with the upstream split: complete + valid commits eagerly; an incomplete
381
+ // or invalid (e.g. Feb 31) value is held as a display override until blur.
382
+ const setValue = (newValue: T | IncompleteDate | null) => {
301
383
  if (isDisabled() || isReadOnly()) return;
302
384
 
303
- const v = value();
385
+ if (
386
+ newValue == null ||
387
+ (newValue instanceof IncompleteDate && newValue.isCleared(displaySegments))
388
+ ) {
389
+ commitDate(null);
390
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, null));
391
+ return;
392
+ }
304
393
 
305
- if (v) {
306
- // Update existing value
307
- const updated = updateDatePart(v, type, newValue);
308
- setValue(updated as T);
309
- } else {
310
- // Update placeholder parts
311
- setPlaceholderDate((prev) => ({
312
- ...prev,
313
- [type]: newValue,
314
- }));
394
+ if (!(newValue instanceof IncompleteDate)) {
395
+ commitDate(newValue);
396
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(newValue)));
397
+ return;
315
398
  }
316
- };
317
399
 
318
- // Increment a segment
319
- const incrementSegment = (type: DateSegmentType) => {
320
- if (isDisabled() || isReadOnly()) return;
400
+ // If the new value is complete and valid, trigger onChange eagerly. If it represents
401
+ // an incomplete or invalid value (e.g. February 30th), wait until blur to trigger onChange.
402
+ if (newValue.isComplete(displaySegments)) {
403
+ const resolved = newValue.toValue(value() ?? getPlaceholderValue());
404
+ if (newValue.validate(resolved, displaySegments)) {
405
+ const committed = value();
406
+ if (!committed || resolved.compare(committed) !== 0) {
407
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(resolved)));
408
+ commitDate(resolved as T);
409
+ return;
410
+ }
411
+ }
412
+ }
321
413
 
322
- const v = value();
323
- const current = v
324
- ? getSegmentValue(v, type)
325
- : placeholderDate()[type as keyof DateParts];
326
- const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
327
- const min = getMinValue(type);
328
-
329
- const newValue = current !== undefined ? current + 1 : min;
330
- setSegment(type, newValue > max ? min : newValue);
414
+ // Incomplete/invalid value. Set temporary display override.
415
+ setDisplayValue(newValue);
331
416
  };
332
417
 
333
- // Decrement a segment
334
- const decrementSegment = (type: DateSegmentType) => {
418
+ const adjustSegment = (type: DateSegmentType, amount: number) => {
335
419
  if (isDisabled() || isReadOnly()) return;
420
+ setValue(displayValue().cycle(type, amount, getPlaceholderValue(), displaySegments));
421
+ };
336
422
 
337
- const v = value();
338
- const current = v
339
- ? getSegmentValue(v, type)
340
- : placeholderDate()[type as keyof DateParts];
341
- const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
342
- const min = getMinValue(type);
343
-
344
- const newValue = current !== undefined ? current - 1 : max;
345
- setSegment(type, newValue < min ? max : newValue);
423
+ // Set a specific segment value
424
+ const setSegment = (type: DateSegmentType, newValue: number) => {
425
+ if (isDisabled() || isReadOnly()) return;
426
+ setValue(displayValue().set(type, newValue, getPlaceholderValue()));
346
427
  };
347
428
 
348
- // Clear a segment
429
+ const incrementSegment = (type: DateSegmentType) => adjustSegment(type, 1);
430
+ const decrementSegment = (type: DateSegmentType) => adjustSegment(type, -1);
431
+
432
+ // Clear a segment, reverting it to its placeholder.
349
433
  const clearSegment = (type: DateSegmentType) => {
350
434
  if (isDisabled() || isReadOnly()) return;
351
-
352
- const v = value();
353
- if (v) {
354
- // Clear entire value if any segment is cleared
355
- setValue(null);
356
- } else {
357
- setPlaceholderDate((prev) => {
358
- const next = { ...prev };
359
- delete next[type as keyof DateParts];
360
- return next;
361
- });
435
+ let dv = displayValue();
436
+ if (type !== "timeZoneName" && type !== "literal") {
437
+ dv = dv.clear(type);
362
438
  }
439
+ setValue(dv);
363
440
  };
364
441
 
365
- // Confirm placeholder value
442
+ // When the field is blurred, constrain a complete-but-invalid display value
443
+ // (e.g. February 31st -> February 28th) and emit onChange. Out-of-range values
444
+ // are left as-is and reported via validation rather than being snapped.
366
445
  const confirmPlaceholder = () => {
367
446
  if (isDisabled() || isReadOnly()) return;
368
-
369
- const minValue = access(props.minValue);
370
- const maxValue = access(props.maxValue);
371
- const constrain = (candidate: DateValue): DateValue => {
372
- if (minValue && candidate.compare(minValue) < 0) {
373
- return minValue;
374
- }
375
- if (maxValue && candidate.compare(maxValue) > 0) {
376
- return maxValue;
377
- }
378
- return candidate;
379
- };
380
-
381
- const parts = placeholderDate();
382
- if (Object.keys(parts).length > 0) {
383
- const dv = dateValue();
384
- if (dv) {
385
- setValue(constrain(dv) as T);
386
- }
387
- return;
447
+ const dv = displayValue();
448
+ if (!dv.isComplete(displaySegments)) return;
449
+
450
+ const resolved = dv.toValue(value() ?? getPlaceholderValue());
451
+ const committed = value();
452
+ const needsCommit = !committed || resolved.compare(committed) !== 0;
453
+ if (needsCommit) {
454
+ commitDate(resolved as T);
388
455
  }
389
456
 
390
- const current = value();
391
- if (current) {
392
- const constrained = constrain(current);
393
- if (constrained.compare(current) !== 0) {
394
- setValue(constrained as T);
395
- }
457
+ // Reset the display override so a constrained value (e.g. February 31st ->
458
+ // February 28th) renders. Only touch the signal when the displayed raw value
459
+ // diverged from the resolved value, or when we committed — otherwise leave it
460
+ // untouched so blur (which fires on every focus move) does not churn the
461
+ // segments and drop focus.
462
+ if (needsCommit || !dv.validate(resolved, displaySegments)) {
463
+ setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(value() ?? resolved)));
396
464
  }
397
465
  };
398
466
 
399
467
  // Format the current value
400
468
  const formatValue = (fieldOptions?: Intl.DateTimeFormatOptions): string => {
401
469
  const v = value();
402
- if (!v) return '';
470
+ if (!v) return "";
403
471
 
404
472
  const options: Intl.DateTimeFormatOptions = fieldOptions ?? {
405
- year: 'numeric',
406
- month: '2-digit',
407
- day: '2-digit',
473
+ year: "numeric",
474
+ month: props.shouldForceLeadingZeros === true ? "2-digit" : "numeric",
475
+ day: props.shouldForceLeadingZeros === true ? "2-digit" : "numeric",
408
476
  };
409
477
 
410
- if (granularity !== 'day') {
411
- options.hour = '2-digit';
412
- options.minute = '2-digit';
413
- if (granularity === 'second') {
414
- options.second = '2-digit';
478
+ if (granularity !== "day") {
479
+ options.hour = "2-digit";
480
+ options.minute = "2-digit";
481
+ if (granularity === "second") {
482
+ options.second = "2-digit";
415
483
  }
416
484
  }
417
485
 
@@ -421,6 +489,7 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
421
489
 
422
490
  return {
423
491
  value,
492
+ defaultValue: initialValue,
424
493
  dateValue,
425
494
  setValue,
426
495
  segments,
@@ -436,150 +505,194 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
436
505
  validationState,
437
506
  granularity,
438
507
  isInvalid,
508
+ realtimeValidation: validation.realtimeValidation,
509
+ displayValidation: validation.displayValidation,
510
+ updateValidation: validation.updateValidation,
511
+ resetValidation: validation.resetValidation,
512
+ commitValidation: validation.commitValidation,
439
513
  locale,
440
514
  timeZone,
441
515
  };
442
516
  }
443
517
 
444
- // ============================================
445
- // HELPER TYPES & FUNCTIONS
446
- // ============================================
447
-
448
- interface DateParts {
449
- year?: number;
450
- month?: number;
451
- day?: number;
452
- hour?: number;
453
- minute?: number;
454
- second?: number;
518
+ const EDITABLE_SEGMENTS: Record<string, boolean> = {
519
+ year: true,
520
+ month: true,
521
+ day: true,
522
+ hour: true,
523
+ minute: true,
524
+ second: true,
525
+ dayPeriod: true,
526
+ era: true,
527
+ };
528
+
529
+ function isEditableSegment(type: DateSegmentType): boolean {
530
+ return EDITABLE_SEGMENTS[type] === true;
455
531
  }
456
532
 
457
- function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
533
+ function rawSegmentValue(dv: IncompleteDate, type: DateSegmentType): number | string | null {
458
534
  switch (type) {
459
- case 'year':
460
- return 'year';
461
- case 'month':
462
- return 'month';
463
- case 'day':
464
- return 'day';
465
- case 'hour':
466
- return 'hour';
467
- case 'minute':
468
- return 'minute';
469
- case 'second':
470
- return 'second';
471
- case 'dayPeriod':
472
- return 'dayPeriod';
473
- case 'era':
474
- return 'era';
475
- case 'timeZoneName':
476
- return 'timeZoneName';
477
- case 'literal':
478
- return 'literal';
535
+ case "era":
536
+ return dv.era;
537
+ case "year":
538
+ return dv.year;
539
+ case "month":
540
+ return dv.month;
541
+ case "day":
542
+ return dv.day;
543
+ case "hour":
544
+ return dv.hour;
545
+ case "minute":
546
+ return dv.minute;
547
+ case "second":
548
+ return dv.second;
549
+ case "dayPeriod":
550
+ return dv.dayPeriod;
479
551
  default:
480
552
  return null;
481
553
  }
482
554
  }
483
555
 
484
- function getSegmentValue(
485
- date: DateValue | null,
486
- type: DateSegmentType
487
- ): number | undefined {
488
- if (!date) return undefined;
556
+ function resolveHourCycle(locale: string, hourCycle?: 12 | 24): HourCycle {
557
+ const formatter = new DateFormatter(locale, {
558
+ hour: "numeric",
559
+ hour12: hourCycle != null ? hourCycle === 12 : undefined,
560
+ });
561
+ return (formatter.resolvedOptions().hourCycle as HourCycle) ?? "h23";
562
+ }
489
563
 
490
- switch (type) {
491
- case 'year':
492
- return date.year;
493
- case 'month':
494
- return date.month;
495
- case 'day':
496
- return date.day;
497
- case 'hour':
498
- return 'hour' in date ? (date as CalendarDateTime).hour : undefined;
499
- case 'minute':
500
- return 'minute' in date ? (date as CalendarDateTime).minute : undefined;
501
- case 'second':
502
- return 'second' in date ? (date as CalendarDateTime).second : undefined;
503
- default:
504
- return undefined;
505
- }
564
+ function computeDisplaySegments(
565
+ granularity: "day" | "hour" | "minute" | "second",
566
+ hourCycle: HourCycle,
567
+ ): DateSegmentType[] {
568
+ const is12HourClock = hourCycle === "h11" || hourCycle === "h12";
569
+ const segments: DateSegmentType[] = [
570
+ "era",
571
+ "year",
572
+ "month",
573
+ "day",
574
+ "hour",
575
+ ...(is12HourClock ? (["dayPeriod"] as const) : []),
576
+ "minute",
577
+ "second",
578
+ ];
579
+ // We never expose era as an editable segment, so the maximum displayed unit is the year.
580
+ const minIndex = segments.indexOf("era");
581
+ const maxIndex = segments.indexOf(
582
+ granularity === "hour" && is12HourClock ? "dayPeriod" : granularity,
583
+ );
584
+ return segments.slice(minIndex, maxIndex + 1);
506
585
  }
507
586
 
508
- function getMinValue(type: DateSegmentType): number {
509
- switch (type) {
510
- case 'year':
511
- return 1;
512
- case 'month':
513
- return 1;
514
- case 'day':
515
- return 1;
516
- case 'hour':
517
- return 0;
518
- case 'minute':
519
- return 0;
520
- case 'second':
521
- return 0;
522
- default:
523
- return 0;
587
+ function buildFormatOptions(
588
+ granularity: "day" | "hour" | "minute" | "second",
589
+ shouldForceLeadingZeros: boolean,
590
+ hourCycle?: 12 | 24,
591
+ ): Intl.DateTimeFormatOptions {
592
+ const formatOptions: Intl.DateTimeFormatOptions = {
593
+ year: "numeric",
594
+ month: shouldForceLeadingZeros ? "2-digit" : "numeric",
595
+ day: shouldForceLeadingZeros ? "2-digit" : "numeric",
596
+ };
597
+
598
+ if (granularity !== "day") {
599
+ formatOptions.hour = "2-digit";
600
+ formatOptions.minute = "2-digit";
601
+ if (granularity === "second") {
602
+ formatOptions.second = "2-digit";
603
+ }
604
+ if (hourCycle) {
605
+ formatOptions.hourCycle = hourCycle === 12 ? "h12" : "h23";
606
+ }
524
607
  }
608
+
609
+ return formatOptions;
525
610
  }
526
611
 
527
- function getMaxValue(type: DateSegmentType, date: DateValue): number {
612
+ function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
528
613
  switch (type) {
529
- case 'year':
530
- return 9999;
531
- case 'month':
532
- return 12;
533
- case 'day':
534
- // Get days in month
535
- return date.calendar.getDaysInMonth?.(date) ?? 31;
536
- case 'hour':
537
- return 23;
538
- case 'minute':
539
- return 59;
540
- case 'second':
541
- return 59;
614
+ case "year":
615
+ return "year";
616
+ case "month":
617
+ return "month";
618
+ case "day":
619
+ return "day";
620
+ case "hour":
621
+ return "hour";
622
+ case "minute":
623
+ return "minute";
624
+ case "second":
625
+ return "second";
626
+ case "dayPeriod":
627
+ return "dayPeriod";
628
+ case "era":
629
+ return "era";
630
+ case "timeZoneName":
631
+ return "timeZoneName";
632
+ case "literal":
633
+ return "literal";
542
634
  default:
543
- return 0;
635
+ return null;
544
636
  }
545
637
  }
546
638
 
547
- function getPlaceholderText(type: DateSegmentType): string {
548
- switch (type) {
549
- case 'year':
550
- return 'yyyy';
551
- case 'month':
552
- return 'mm';
553
- case 'day':
554
- return 'dd';
555
- case 'hour':
556
- return '––';
557
- case 'minute':
558
- return '––';
559
- case 'second':
560
- return '––';
561
- case 'dayPeriod':
562
- return 'AM';
563
- default:
564
- return '';
639
+ function getPlaceholderText(
640
+ type: DateSegmentType,
641
+ locale: string,
642
+ granularity: "day" | "hour" | "minute" | "second",
643
+ shouldForceLeadingZeros = false,
644
+ ): string {
645
+ if (type === "literal") return "";
646
+ if (type === "dayPeriod") return "AM";
647
+
648
+ const formatOptions: Intl.DateTimeFormatOptions = {
649
+ year: "numeric",
650
+ month: shouldForceLeadingZeros ? "2-digit" : "numeric",
651
+ day: shouldForceLeadingZeros ? "2-digit" : "numeric",
652
+ };
653
+
654
+ if (granularity !== "day") {
655
+ formatOptions.hour = "2-digit";
656
+ formatOptions.minute = "2-digit";
657
+ if (granularity === "second") {
658
+ formatOptions.second = "2-digit";
659
+ }
660
+ }
661
+
662
+ const formatter = new DateFormatter(locale, formatOptions);
663
+ // Use a reference date with recognizable digits
664
+ const refDate = new Date(2222, 10, 22, 22, 22, 22);
665
+ const parts = formatter.formatToParts(refDate);
666
+
667
+ for (const part of parts) {
668
+ const mappedType = mapPartType(part.type);
669
+ if (mappedType === type) {
670
+ return part.value.replace(/\d/g, (_d) => {
671
+ if (type === "year") return "y";
672
+ if (type === "month") return "m";
673
+ if (type === "day") return "d";
674
+ return "–";
675
+ });
676
+ }
565
677
  }
566
- }
567
678
 
568
- function updateDatePart(date: DateValue, type: DateSegmentType, value: number): DateValue {
569
679
  switch (type) {
570
- case 'year':
571
- return (date as CalendarDate).set({ year: value });
572
- case 'month':
573
- return (date as CalendarDate).set({ month: value });
574
- case 'day':
575
- return (date as CalendarDate).set({ day: value });
576
- case 'hour':
577
- return (date as CalendarDateTime).set({ hour: value });
578
- case 'minute':
579
- return (date as CalendarDateTime).set({ minute: value });
580
- case 'second':
581
- return (date as CalendarDateTime).set({ second: value });
680
+ case "year":
681
+ return "yyyy";
682
+ case "month":
683
+ return "mm";
684
+ case "day":
685
+ return "dd";
686
+ case "hour":
687
+ return "––";
688
+ case "minute":
689
+ return "––";
690
+ case "second":
691
+ return "––";
582
692
  default:
583
- return date;
693
+ return "";
584
694
  }
585
695
  }
696
+
697
+ // Re-export so existing imports of CalendarDateTime keep resolving through this module.
698
+ export type { CalendarDateTime };