@proyecto-viviana/solid-stately 0.2.4 → 0.3.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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -283
  3. package/dist/autocomplete/createAutocompleteState.d.ts +3 -2
  4. package/dist/autocomplete/index.d.ts +1 -1
  5. package/dist/calendar/createCalendarState.d.ts +22 -7
  6. package/dist/calendar/createDateFieldState.d.ts +30 -7
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +27 -8
  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 +12 -3
  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 +8 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +5 -5
  17. package/dist/collections/index.d.ts +5 -5
  18. package/dist/collections/types.d.ts +23 -12
  19. package/dist/color/Color.d.ts +1 -1
  20. package/dist/color/createColorAreaState.d.ts +4 -2
  21. package/dist/color/createColorFieldState.d.ts +38 -12
  22. package/dist/color/createColorSliderState.d.ts +9 -2
  23. package/dist/color/createColorWheelState.d.ts +13 -2
  24. package/dist/color/getColorChannels.d.ts +20 -0
  25. package/dist/color/index.d.ts +6 -6
  26. package/dist/color/types.d.ts +4 -4
  27. package/dist/combobox/createComboBoxState.d.ts +27 -13
  28. package/dist/combobox/index.d.ts +1 -1
  29. package/dist/data/createAsyncList.d.ts +111 -0
  30. package/dist/data/createListData.d.ts +65 -0
  31. package/dist/data/createTreeData.d.ts +65 -0
  32. package/dist/data/index.d.ts +3 -0
  33. package/dist/datepicker/index.d.ts +8 -0
  34. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  35. package/dist/disclosure/index.d.ts +1 -1
  36. package/dist/dnd/createDragState.d.ts +2 -2
  37. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  38. package/dist/dnd/createDropState.d.ts +2 -2
  39. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  40. package/dist/dnd/index.d.ts +5 -5
  41. package/dist/dnd/types.d.ts +16 -16
  42. package/dist/form/createFormValidationState.d.ts +3 -3
  43. package/dist/form/index.d.ts +1 -1
  44. package/dist/grid/createGridState.d.ts +2 -2
  45. package/dist/grid/index.d.ts +2 -2
  46. package/dist/grid/types.d.ts +17 -11
  47. package/dist/index.d.ts +31 -25
  48. package/dist/index.js +7990 -6264
  49. package/dist/index.js.map +1 -7
  50. package/dist/menu/index.d.ts +8 -0
  51. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  52. package/dist/numberfield/index.d.ts +1 -1
  53. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  54. package/dist/overlays/index.d.ts +1 -1
  55. package/dist/radio/createRadioGroupState.d.ts +13 -4
  56. package/dist/radio/index.d.ts +1 -1
  57. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  58. package/dist/searchfield/index.d.ts +2 -2
  59. package/dist/select/createSelectState.d.ts +20 -3
  60. package/dist/select/index.d.ts +1 -1
  61. package/dist/selection/index.d.ts +11 -0
  62. package/dist/slider/createSliderState.d.ts +3 -3
  63. package/dist/slider/index.d.ts +2 -2
  64. package/dist/steplist/createStepListState.d.ts +48 -0
  65. package/dist/steplist/index.d.ts +1 -0
  66. package/dist/table/TableCollection.d.ts +3 -3
  67. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  68. package/dist/table/createTableState.d.ts +2 -2
  69. package/dist/table/index.d.ts +5 -3
  70. package/dist/table/types.d.ts +10 -10
  71. package/dist/tabs/createTabListState.d.ts +5 -5
  72. package/dist/tabs/index.d.ts +1 -1
  73. package/dist/textfield/createTextFieldState.d.ts +2 -2
  74. package/dist/textfield/index.d.ts +1 -1
  75. package/dist/toast/createToastState.d.ts +15 -3
  76. package/dist/toast/index.d.ts +1 -1
  77. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  78. package/dist/toggle/createToggleState.d.ts +2 -2
  79. package/dist/toggle/index.d.ts +2 -1
  80. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  81. package/dist/tooltip/index.d.ts +1 -1
  82. package/dist/tree/TreeCollection.d.ts +5 -4
  83. package/dist/tree/createTreeState.d.ts +2 -2
  84. package/dist/tree/index.d.ts +3 -3
  85. package/dist/tree/types.d.ts +19 -13
  86. package/dist/utils/index.d.ts +1 -1
  87. package/dist/utils/reactivity.d.ts +1 -1
  88. package/package.json +30 -30
  89. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  90. package/src/autocomplete/createAutocompleteState.ts +25 -32
  91. package/src/autocomplete/index.d.ts +6 -2
  92. package/src/autocomplete/index.ts +1 -1
  93. package/src/calendar/createCalendarState.d.ts +139 -117
  94. package/src/calendar/createCalendarState.ts +221 -74
  95. package/src/calendar/createDateFieldState.d.ts +130 -95
  96. package/src/calendar/createDateFieldState.ts +286 -151
  97. package/src/calendar/createDatePickerState.ts +347 -0
  98. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  99. package/src/calendar/createRangeCalendarState.ts +293 -79
  100. package/src/calendar/createTimeFieldState.d.ts +81 -79
  101. package/src/calendar/createTimeFieldState.ts +187 -108
  102. package/src/calendar/index.d.ts +80 -7
  103. package/src/calendar/index.ts +21 -12
  104. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  105. package/src/checkbox/createCheckboxGroupState.ts +57 -30
  106. package/src/checkbox/index.d.ts +6 -2
  107. package/src/checkbox/index.ts +1 -1
  108. package/src/collections/ListCollection.d.ts +25 -22
  109. package/src/collections/ListCollection.ts +152 -146
  110. package/src/collections/createListState.d.ts +73 -58
  111. package/src/collections/createListState.ts +267 -264
  112. package/src/collections/createMenuState.d.ts +36 -31
  113. package/src/collections/createMenuState.ts +112 -106
  114. package/src/collections/createSelectionState.d.ts +78 -65
  115. package/src/collections/createSelectionState.ts +340 -336
  116. package/src/collections/index.d.ts +38 -6
  117. package/src/collections/index.ts +41 -46
  118. package/src/collections/types.d.ts +101 -101
  119. package/src/collections/types.ts +181 -169
  120. package/src/color/Color.d.ts +20 -5
  121. package/src/color/Color.ts +1054 -951
  122. package/src/color/createColorAreaState.d.ts +67 -65
  123. package/src/color/createColorAreaState.ts +301 -293
  124. package/src/color/createColorFieldState.d.ts +46 -44
  125. package/src/color/createColorFieldState.ts +428 -292
  126. package/src/color/createColorSliderState.d.ts +58 -56
  127. package/src/color/createColorSliderState.ts +272 -241
  128. package/src/color/createColorWheelState.d.ts +42 -40
  129. package/src/color/createColorWheelState.ts +248 -211
  130. package/src/color/getColorChannels.ts +36 -0
  131. package/src/color/index.d.ts +36 -7
  132. package/src/color/index.ts +44 -47
  133. package/src/color/types.d.ts +68 -63
  134. package/src/color/types.ts +119 -127
  135. package/src/combobox/createComboBoxState.d.ts +107 -105
  136. package/src/combobox/createComboBoxState.ts +790 -703
  137. package/src/combobox/index.d.ts +10 -2
  138. package/src/combobox/index.ts +13 -13
  139. package/src/data/createAsyncList.ts +403 -0
  140. package/src/data/createListData.ts +298 -0
  141. package/src/data/createTreeData.ts +646 -0
  142. package/src/data/index.ts +25 -0
  143. package/src/datepicker/index.ts +32 -0
  144. package/src/disclosure/createDisclosureState.d.ts +47 -43
  145. package/src/disclosure/createDisclosureState.ts +9 -23
  146. package/src/disclosure/index.d.ts +10 -2
  147. package/src/disclosure/index.ts +1 -1
  148. package/src/dnd/createDragState.d.ts +51 -42
  149. package/src/dnd/createDragState.ts +151 -153
  150. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  151. package/src/dnd/createDraggableCollectionState.ts +158 -165
  152. package/src/dnd/createDropState.d.ts +64 -45
  153. package/src/dnd/createDropState.ts +202 -212
  154. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  155. package/src/dnd/createDroppableCollectionState.ts +354 -357
  156. package/src/dnd/index.d.ts +54 -6
  157. package/src/dnd/index.ts +63 -76
  158. package/src/dnd/types.d.ts +167 -156
  159. package/src/dnd/types.ts +316 -317
  160. package/src/form/createFormValidationState.d.ts +47 -45
  161. package/src/form/createFormValidationState.ts +356 -389
  162. package/src/form/index.d.ts +16 -2
  163. package/src/form/index.ts +15 -15
  164. package/src/grid/createGridState.d.ts +7 -4
  165. package/src/grid/createGridState.ts +44 -51
  166. package/src/grid/index.d.ts +3 -3
  167. package/src/grid/index.ts +2 -8
  168. package/src/grid/types.d.ts +136 -129
  169. package/src/grid/types.ts +24 -17
  170. package/src/index.d.ts +310 -26
  171. package/src/index.ts +87 -79
  172. package/src/menu/index.ts +17 -0
  173. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  174. package/src/numberfield/createNumberFieldState.ts +422 -383
  175. package/src/numberfield/index.d.ts +6 -2
  176. package/src/numberfield/index.ts +5 -5
  177. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  178. package/src/overlays/createOverlayTriggerState.ts +66 -67
  179. package/src/overlays/index.d.ts +6 -2
  180. package/src/overlays/index.ts +5 -5
  181. package/src/radio/createRadioGroupState.d.ts +54 -52
  182. package/src/radio/createRadioGroupState.ts +66 -30
  183. package/src/radio/index.d.ts +7 -2
  184. package/src/radio/index.ts +1 -1
  185. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  186. package/src/searchfield/createSearchFieldState.ts +56 -62
  187. package/src/searchfield/index.d.ts +3 -3
  188. package/src/searchfield/index.ts +2 -5
  189. package/src/select/createSelectState.d.ts +63 -61
  190. package/src/select/createSelectState.ts +290 -181
  191. package/src/select/index.d.ts +2 -2
  192. package/src/select/index.ts +1 -5
  193. package/src/selection/index.ts +27 -0
  194. package/src/slider/createSliderState.d.ts +60 -60
  195. package/src/slider/createSliderState.ts +233 -211
  196. package/src/slider/index.d.ts +3 -3
  197. package/src/slider/index.ts +2 -6
  198. package/src/ssr/index.d.ts +1 -1
  199. package/src/ssr/index.ts +2 -2
  200. package/src/steplist/createStepListState.ts +191 -0
  201. package/src/steplist/index.ts +5 -0
  202. package/src/table/TableCollection.d.ts +42 -40
  203. package/src/table/TableCollection.ts +61 -64
  204. package/src/table/createTableColumnResizeState.ts +242 -0
  205. package/src/table/createTableState.d.ts +7 -4
  206. package/src/table/createTableState.ts +10 -7
  207. package/src/table/index.d.ts +14 -4
  208. package/src/table/index.ts +10 -3
  209. package/src/table/types.d.ts +98 -92
  210. package/src/table/types.ts +17 -13
  211. package/src/tabs/createTabListState.d.ts +59 -57
  212. package/src/tabs/createTabListState.ts +58 -39
  213. package/src/tabs/index.d.ts +8 -2
  214. package/src/tabs/index.ts +1 -1
  215. package/src/textfield/createTextFieldState.d.ts +16 -14
  216. package/src/textfield/createTextFieldState.ts +7 -18
  217. package/src/textfield/index.d.ts +6 -2
  218. package/src/textfield/index.ts +1 -1
  219. package/src/toast/createToastState.d.ts +92 -81
  220. package/src/toast/createToastState.ts +56 -44
  221. package/src/toast/index.d.ts +12 -2
  222. package/src/toast/index.ts +1 -1
  223. package/src/toggle/createToggleGroupState.ts +117 -0
  224. package/src/toggle/createToggleState.d.ts +19 -19
  225. package/src/toggle/createToggleState.ts +2 -16
  226. package/src/toggle/index.d.ts +2 -2
  227. package/src/toggle/index.ts +6 -4
  228. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  229. package/src/tooltip/createTooltipTriggerState.ts +184 -183
  230. package/src/tooltip/index.d.ts +7 -2
  231. package/src/tooltip/index.ts +6 -6
  232. package/src/tree/TreeCollection.d.ts +26 -23
  233. package/src/tree/TreeCollection.ts +215 -175
  234. package/src/tree/createTreeState.d.ts +7 -4
  235. package/src/tree/createTreeState.ts +390 -392
  236. package/src/tree/index.d.ts +10 -4
  237. package/src/tree/index.ts +13 -13
  238. package/src/tree/types.d.ts +131 -126
  239. package/src/tree/types.ts +183 -174
  240. package/src/utils/index.d.ts +2 -2
  241. package/src/utils/index.ts +1 -1
  242. package/src/utils/reactivity.d.ts +2 -2
  243. package/src/utils/reactivity.ts +3 -3
@@ -5,28 +5,33 @@
5
5
  * Based on @react-stately/calendar useRangeCalendarState
6
6
  */
7
7
 
8
- import { createSignal, createMemo, type Accessor } from 'solid-js';
8
+ import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
9
9
  import {
10
+ type Calendar as InternationalizedCalendar,
10
11
  type CalendarDate,
12
+ type CalendarIdentifier,
11
13
  type DateValue,
14
+ GregorianCalendar,
12
15
  today,
13
16
  getLocalTimeZone,
17
+ isEqualCalendar,
14
18
  isSameDay,
15
19
  isSameMonth,
16
20
  startOfMonth,
17
21
  endOfMonth,
18
22
  startOfWeek,
19
23
  getWeeksInMonth,
20
- getDayOfWeek,
21
24
  DateFormatter,
25
+ createCalendar as intlCreateCalendar,
26
+ toCalendar as intlToCalendar,
22
27
  toCalendarDate as intlToCalendarDate,
23
- } from '@internationalized/date';
24
- import { access, type MaybeAccessor } from '../utils';
25
- import type { ValidationState } from './createCalendarState';
28
+ } from "@internationalized/date";
29
+ import { access, type MaybeAccessor } from "../utils";
30
+ import type { CalendarDayOfWeek, ValidationState } from "./createCalendarState";
26
31
 
27
- // ============================================
28
- // TYPES
29
- // ============================================
32
+ const dayOfWeekNames: CalendarDayOfWeek[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
33
+ type RangeCalendarSelectionAlignment = "start" | "center" | "end";
34
+ type RangeCalendarPageBehavior = "single" | "visible";
30
35
 
31
36
  export interface DateRange {
32
37
  start: CalendarDate;
@@ -44,7 +49,7 @@ export interface RangeCalendarStateProps<T extends DateValue = DateValue> {
44
49
  /** The default value (uncontrolled). */
45
50
  defaultValue?: RangeValue<T> | null;
46
51
  /** Handler called when the value changes. */
47
- onChange?: (value: RangeValue<T>) => void;
52
+ onChange?: (value: RangeValue<T> | null) => void;
48
53
  /** The minimum allowed date. */
49
54
  minValue?: MaybeAccessor<DateValue | undefined>;
50
55
  /** The maximum allowed date. */
@@ -54,17 +59,23 @@ export interface RangeCalendarStateProps<T extends DateValue = DateValue> {
54
59
  /** Whether the calendar is read-only. */
55
60
  isReadOnly?: MaybeAccessor<boolean>;
56
61
  /** The date that is focused when the calendar first mounts. */
57
- focusedValue?: MaybeAccessor<DateValue | undefined>;
62
+ focusedValue?: MaybeAccessor<DateValue | null | undefined>;
58
63
  /** The default focused date (uncontrolled). */
59
64
  defaultFocusedValue?: DateValue;
60
65
  /** Handler called when the focused date changes. */
61
66
  onFocusChange?: (date: CalendarDate) => void;
62
67
  /** The locale to use for formatting. */
63
- locale?: string;
68
+ locale?: MaybeAccessor<string | undefined>;
69
+ /** Creates a calendar object for a locale calendar identifier. */
70
+ createCalendar?: (identifier: CalendarIdentifier) => InternationalizedCalendar;
64
71
  /** Callback to determine if a date is unavailable. */
65
72
  isDateUnavailable?: (date: DateValue) => boolean;
66
73
  /** The number of months to display at once. */
67
- visibleMonths?: number;
74
+ visibleMonths?: MaybeAccessor<number | undefined>;
75
+ /** Controls how the initial visible months are aligned around the current value. */
76
+ selectionAlignment?: MaybeAccessor<RangeCalendarSelectionAlignment | undefined>;
77
+ /** Controls whether pagination moves by one month or the visible month count. */
78
+ pageBehavior?: MaybeAccessor<RangeCalendarPageBehavior | undefined>;
68
79
  /** Controls which days are disabled. */
69
80
  isDateDisabled?: (date: DateValue) => boolean;
70
81
  /** Validation state. */
@@ -98,8 +109,14 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
98
109
  visibleRange: Accessor<{ start: CalendarDate; end: CalendarDate }>;
99
110
  /** The timezone used for date calculations. */
100
111
  timeZone: string;
112
+ /** The locale used for formatting and locale-specific week starts. */
113
+ locale: Accessor<string>;
114
+ /** The explicitly requested first weekday, or undefined for the locale default. */
115
+ firstDayOfWeek: Accessor<CalendarDayOfWeek | undefined>;
101
116
  /** The validation state. */
102
117
  validationState: Accessor<ValidationState | undefined>;
118
+ /** Whether the current value is invalid according to validation state. */
119
+ isValueInvalid: Accessor<boolean>;
103
120
  /** Whether a date is within the selected range. */
104
121
  isSelected: (date: DateValue) => boolean;
105
122
  /** Whether a date is the start of the selection. */
@@ -124,6 +141,10 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
124
141
  focusPreviousSection: () => void;
125
142
  /** Moves focus to the next section (year). */
126
143
  focusNextSection: () => void;
144
+ /** Whether the previous visible range would be outside the allowed date range. */
145
+ isPreviousVisibleRangeInvalid: () => boolean;
146
+ /** Whether the next visible range would be outside the allowed date range. */
147
+ isNextVisibleRangeInvalid: () => boolean;
127
148
  /** Moves focus to the previous day. */
128
149
  focusPreviousDay: () => void;
129
150
  /** Moves focus to the next day. */
@@ -160,60 +181,170 @@ export interface RangeCalendarState<T extends DateValue = DateValue> {
160
181
  setDragging: (dragging: boolean) => void;
161
182
  }
162
183
 
163
- // ============================================
164
- // IMPLEMENTATION
165
- // ============================================
166
-
167
184
  /**
168
185
  * Provides state management for a range calendar component.
169
186
  */
170
187
  export function createRangeCalendarState<T extends DateValue = CalendarDate>(
171
- props: RangeCalendarStateProps<T> = {}
188
+ props: RangeCalendarStateProps<T> = {},
172
189
  ): RangeCalendarState<T> {
173
190
  const timeZone = getLocalTimeZone();
174
- const locale = props.locale ?? 'en-US';
175
- const visibleMonths = props.visibleMonths ?? 1;
191
+ const locale = createMemo(() => access(props.locale) ?? "en-US");
192
+ const resolvedOptions = createMemo(() => new DateFormatter(locale()).resolvedOptions());
193
+ const calendar = createMemo(() =>
194
+ (props.createCalendar ?? intlCreateCalendar)(resolvedOptions().calendar as CalendarIdentifier),
195
+ );
196
+ const visibleMonths = Math.max(1, Number(access(props.visibleMonths) ?? 1));
197
+ const firstDayOfWeekName = (): CalendarDayOfWeek | undefined =>
198
+ props.firstDayOfWeek == null ? undefined : dayOfWeekNames[props.firstDayOfWeek];
199
+
200
+ const toDisplayCalendarDate = (date: DateValue): CalendarDate =>
201
+ intlToCalendar(toCalendarDate(date), calendar());
202
+
203
+ const constrainDate = (date: CalendarDate): CalendarDate => {
204
+ const minValue = access(props.minValue);
205
+ const maxValue = access(props.maxValue);
206
+
207
+ let constrained = date;
208
+
209
+ if (minValue && constrained.compare(toDisplayCalendarDate(minValue)) < 0) {
210
+ constrained = toDisplayCalendarDate(minValue);
211
+ }
212
+ if (maxValue && constrained.compare(toDisplayCalendarDate(maxValue)) > 0) {
213
+ constrained = toDisplayCalendarDate(maxValue);
214
+ }
215
+
216
+ return constrained;
217
+ };
218
+
219
+ const visibleRangeEndFromStart = (start: CalendarDate): CalendarDate => {
220
+ let end = endOfMonth(start);
221
+
222
+ for (let i = 1; i < visibleMonths; i++) {
223
+ end = endOfMonth(end.add({ months: 1 }));
224
+ }
225
+
226
+ return end;
227
+ };
228
+
229
+ const rawAlignStart = (date: CalendarDate): CalendarDate => startOfMonth(date);
230
+
231
+ const rawAlignEnd = (date: CalendarDate): CalendarDate =>
232
+ startOfMonth(date).subtract({ months: Math.max(visibleMonths - 1, 0) });
233
+
234
+ const constrainVisibleRangeStart = (date: CalendarDate, aligned: CalendarDate): CalendarDate => {
235
+ const minValue = access(props.minValue);
236
+ const maxValue = access(props.maxValue);
237
+ let constrained = aligned;
238
+
239
+ if (minValue && date.compare(toDisplayCalendarDate(minValue)) >= 0) {
240
+ const minStart = rawAlignStart(toDisplayCalendarDate(minValue));
241
+ if (minStart.compare(constrained) > 0) {
242
+ constrained = minStart;
243
+ }
244
+ }
245
+
246
+ if (maxValue && date.compare(toDisplayCalendarDate(maxValue)) <= 0) {
247
+ const maxStart = rawAlignEnd(toDisplayCalendarDate(maxValue));
248
+ if (maxStart.compare(constrained) < 0) {
249
+ constrained = maxStart;
250
+ }
251
+ }
252
+
253
+ return constrained;
254
+ };
255
+
256
+ const alignStart = (date: CalendarDate): CalendarDate =>
257
+ constrainVisibleRangeStart(date, rawAlignStart(date));
258
+
259
+ const alignEnd = (date: CalendarDate): CalendarDate =>
260
+ constrainVisibleRangeStart(date, rawAlignEnd(date));
261
+
262
+ const alignCenter = (date: CalendarDate): CalendarDate => {
263
+ let offset = Math.floor(visibleMonths / 2);
264
+ if (offset > 0 && visibleMonths % 2 === 0) {
265
+ offset--;
266
+ }
267
+
268
+ return constrainVisibleRangeStart(date, rawAlignStart(date).subtract({ months: offset }));
269
+ };
270
+
271
+ const defaultSelectionAlignment = (): RangeCalendarSelectionAlignment => {
272
+ const currentValue = access(props.value) ?? props.defaultValue;
273
+ if (!currentValue?.start || !currentValue?.end) {
274
+ return "center";
275
+ }
276
+
277
+ const start = alignCenter(toDisplayCalendarDate(currentValue.start));
278
+ const end = visibleRangeEndFromStart(start);
279
+ return toDisplayCalendarDate(currentValue.end).compare(end) > 0 ? "start" : "center";
280
+ };
281
+
282
+ const alignVisibleRangeStart = (date: CalendarDate): CalendarDate => {
283
+ switch (access(props.selectionAlignment) ?? defaultSelectionAlignment()) {
284
+ case "start":
285
+ return alignStart(date);
286
+ case "end":
287
+ return alignEnd(date);
288
+ case "center":
289
+ default:
290
+ return alignCenter(date);
291
+ }
292
+ };
176
293
 
177
294
  // Determine the initially focused date
178
295
  const getInitialFocusedDate = (): CalendarDate => {
179
296
  const controlledFocused = access(props.focusedValue);
180
297
  if (controlledFocused) {
181
- return toCalendarDate(controlledFocused);
298
+ return toDisplayCalendarDate(controlledFocused);
182
299
  }
183
300
  if (props.defaultFocusedValue) {
184
- return toCalendarDate(props.defaultFocusedValue);
301
+ return toDisplayCalendarDate(props.defaultFocusedValue);
185
302
  }
186
303
  const controlledValue = access(props.value);
187
304
  if (controlledValue?.start) {
188
- return toCalendarDate(controlledValue.start);
305
+ return toDisplayCalendarDate(controlledValue.start);
189
306
  }
190
307
  if (props.defaultValue?.start) {
191
- return toCalendarDate(props.defaultValue.start);
308
+ return toDisplayCalendarDate(props.defaultValue.start);
192
309
  }
193
- return today(timeZone);
310
+ return intlToCalendar(today(timeZone), calendar());
194
311
  };
195
312
 
196
313
  // State signals
197
314
  const [internalValue, setInternalValue] = createSignal<RangeValue<T> | null>(
198
- props.defaultValue ?? null
315
+ props.defaultValue ?? null,
199
316
  );
200
- const [focusedDate, setFocusedDateInternal] = createSignal<CalendarDate>(
201
- getInitialFocusedDate()
317
+ const initialFocusedDate = constrainDate(getInitialFocusedDate());
318
+ const [focusedDate, setFocusedDateInternal] = createSignal<CalendarDate>(initialFocusedDate);
319
+ const [visibleRangeStart, setVisibleRangeStart] = createSignal<CalendarDate>(
320
+ alignVisibleRangeStart(initialFocusedDate),
202
321
  );
203
322
  const [anchorDate, setAnchorDate] = createSignal<CalendarDate | null>(null);
204
323
  const [isFocused, setFocused] = createSignal(false);
205
324
  const [isDragging, setDragging] = createSignal(false);
206
325
 
207
326
  // Controlled vs uncontrolled value
208
- const value = createMemo<RangeValue<T> | null>(() => {
327
+ const sourceValue = createMemo<RangeValue<T> | null>(() => {
209
328
  const controlled = access(props.value);
210
329
  return controlled !== undefined ? controlled : internalValue();
211
330
  });
331
+ const value = createMemo<RangeValue<T> | null>(() => {
332
+ const currentValue = sourceValue();
333
+ if (!currentValue) {
334
+ return null;
335
+ }
336
+
337
+ return {
338
+ start: toDisplayCalendarDate(currentValue.start) as unknown as T,
339
+ end: toDisplayCalendarDate(currentValue.end) as unknown as T,
340
+ };
341
+ });
212
342
 
213
343
  // Derived states
214
344
  const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
215
345
  const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
216
346
  const validationState = createMemo(() => access(props.validationState));
347
+ const isValueInvalid = createMemo(() => validationState() === "invalid");
217
348
 
218
349
  // Highlighted range during selection
219
350
  const highlightedRange = createMemo<RangeValue<CalendarDate> | null>(() => {
@@ -222,8 +353,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
222
353
  const v = value();
223
354
  if (v) {
224
355
  return {
225
- start: toCalendarDate(v.start),
226
- end: toCalendarDate(v.end),
356
+ start: toDisplayCalendarDate(v.start),
357
+ end: toDisplayCalendarDate(v.end),
227
358
  };
228
359
  }
229
360
  return null;
@@ -236,28 +367,68 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
236
367
  return { start: focused, end: anchor };
237
368
  });
238
369
 
239
- // Visible range based on focused date
370
+ // Visible range based on React Stately's selection alignment rules.
240
371
  const visibleRange = createMemo(() => {
241
- const focused = focusedDate();
242
- const start = startOfMonth(focused);
243
- let end = endOfMonth(focused);
372
+ const start = visibleRangeStart();
373
+ return { start, end: visibleRangeEndFromStart(start) };
374
+ });
244
375
 
245
- for (let i = 1; i < visibleMonths; i++) {
246
- end = endOfMonth(end.add({ months: 1 }));
376
+ createEffect(() => {
377
+ const controlledFocused = access(props.focusedValue);
378
+ if (!controlledFocused) {
379
+ return;
380
+ }
381
+
382
+ const nextFocusedDate = constrainDate(toDisplayCalendarDate(controlledFocused));
383
+ const currentFocusedDate = focusedDate();
384
+
385
+ if (
386
+ nextFocusedDate.compare(currentFocusedDate) !== 0 ||
387
+ !isEqualCalendar(nextFocusedDate.calendar, currentFocusedDate.calendar)
388
+ ) {
389
+ setFocusedDateInternal(nextFocusedDate);
390
+ setVisibleRangeStart(alignVisibleRangeStart(nextFocusedDate));
391
+ }
392
+ });
393
+
394
+ createEffect(() => {
395
+ const controlledFocused = access(props.focusedValue);
396
+ if (controlledFocused) {
397
+ return;
398
+ }
399
+
400
+ const currentFocusedDate = focusedDate();
401
+ const nextFocusedDate = constrainDate(currentFocusedDate);
402
+
403
+ if (
404
+ nextFocusedDate.compare(currentFocusedDate) === 0 &&
405
+ isEqualCalendar(nextFocusedDate.calendar, currentFocusedDate.calendar)
406
+ ) {
407
+ return;
408
+ }
409
+
410
+ setFocusedDateInternal(nextFocusedDate);
411
+ setVisibleRangeStart(alignVisibleRangeStart(nextFocusedDate));
412
+ props.onFocusChange?.(nextFocusedDate);
413
+ });
414
+
415
+ createEffect(() => {
416
+ const currentFocusedDate = focusedDate();
417
+ const nextFocusedDate = toDisplayCalendarDate(currentFocusedDate);
418
+
419
+ if (isEqualCalendar(currentFocusedDate.calendar, nextFocusedDate.calendar)) {
420
+ return;
247
421
  }
248
422
 
249
- return { start, end };
423
+ setFocusedDateInternal(nextFocusedDate);
424
+ setVisibleRangeStart(alignVisibleRangeStart(nextFocusedDate));
250
425
  });
251
426
 
252
427
  // Format week days for headers
253
428
  const weekDays = createMemo(() => {
254
- const formatter = new DateFormatter(locale, { weekday: 'short' });
255
- const startDay = props.firstDayOfWeek ?? 0;
429
+ const formatter = new DateFormatter(locale(), { weekday: "narrow" });
256
430
  const days: string[] = [];
257
- const base = today(timeZone);
258
-
259
- const dayOfWeek = getDayOfWeek(base, locale);
260
- const weekStart = base.subtract({ days: (dayOfWeek - startDay + 7) % 7 });
431
+ const weekStart = startOfWeek(today(timeZone), locale(), firstDayOfWeekName());
261
432
 
262
433
  for (let i = 0; i < 7; i++) {
263
434
  const day = weekStart.add({ days: i });
@@ -269,42 +440,51 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
269
440
 
270
441
  // Title (formatted month/year)
271
442
  const title = createMemo(() => {
272
- const formatter = new DateFormatter(locale, {
273
- month: 'long',
274
- year: 'numeric',
443
+ const formatter = new DateFormatter(locale(), {
444
+ month: "long",
445
+ year: "numeric",
446
+ calendar: focusedDate().calendar.identifier,
275
447
  });
276
- return formatter.format(focusedDate().toDate(timeZone));
448
+ const date = focusedDate();
449
+ const formattableMonth = date.calendar.getFormattableMonth?.(date) ?? date;
450
+ return formatter.format(formattableMonth.toDate(timeZone));
277
451
  });
278
452
 
279
453
  // Set value with onChange callback
280
454
  const setValue = (newValue: RangeValue<T> | null) => {
281
455
  if (isDisabled() || isReadOnly()) return;
282
456
 
457
+ const oldValue = sourceValue();
458
+ const nextValue = newValue
459
+ ? {
460
+ start: convertValue(
461
+ constrainDate(toDisplayCalendarDate(newValue.start)),
462
+ oldValue?.start,
463
+ ) as T,
464
+ end: convertValue(constrainDate(toDisplayCalendarDate(newValue.end)), oldValue?.end) as T,
465
+ }
466
+ : null;
467
+
283
468
  const controlled = access(props.value);
284
469
  if (controlled === undefined) {
285
- setInternalValue(() => newValue);
470
+ setInternalValue(() => nextValue);
286
471
  }
287
472
 
288
- if (newValue && props.onChange) {
289
- props.onChange(newValue);
473
+ if (!Object.is(oldValue, nextValue) && props.onChange) {
474
+ props.onChange(nextValue);
290
475
  }
291
476
  };
292
477
 
293
478
  // Set focused date with constraints
294
479
  const setFocusedDate = (date: CalendarDate) => {
295
- const minValue = access(props.minValue);
296
- const maxValue = access(props.maxValue);
297
-
298
- let constrained = date;
480
+ const constrained = constrainDate(date);
299
481
 
300
- if (minValue && date.compare(toCalendarDate(minValue)) < 0) {
301
- constrained = toCalendarDate(minValue);
302
- }
303
- if (maxValue && date.compare(toCalendarDate(maxValue)) > 0) {
304
- constrained = toCalendarDate(maxValue);
482
+ if (Object.is(constrained, focusedDate())) {
483
+ return;
305
484
  }
306
485
 
307
486
  setFocusedDateInternal(constrained);
487
+ setVisibleRangeStart(alignVisibleRangeStart(constrained));
308
488
  props.onFocusChange?.(constrained);
309
489
  };
310
490
 
@@ -313,29 +493,27 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
313
493
  const range = highlightedRange();
314
494
  if (!range) return false;
315
495
 
316
- const calDate = toCalendarDate(date);
317
- return (
318
- calDate.compare(range.start) >= 0 && calDate.compare(range.end) <= 0
319
- );
496
+ const calDate = toDisplayCalendarDate(date);
497
+ return calDate.compare(range.start) >= 0 && calDate.compare(range.end) <= 0;
320
498
  };
321
499
 
322
500
  // Check if a date is the start of the selection
323
501
  const isSelectionStart = (date: DateValue): boolean => {
324
502
  const range = highlightedRange();
325
503
  if (!range) return false;
326
- return isSameDay(toCalendarDate(date), range.start);
504
+ return isSameDay(toDisplayCalendarDate(date), range.start);
327
505
  };
328
506
 
329
507
  // Check if a date is the end of the selection
330
508
  const isSelectionEnd = (date: DateValue): boolean => {
331
509
  const range = highlightedRange();
332
510
  if (!range) return false;
333
- return isSameDay(toCalendarDate(date), range.end);
511
+ return isSameDay(toDisplayCalendarDate(date), range.end);
334
512
  };
335
513
 
336
514
  // Check if a date is focused
337
515
  const isCellFocused = (date: DateValue): boolean => {
338
- return isSameDay(toCalendarDate(date), focusedDate());
516
+ return isSameDay(toDisplayCalendarDate(date), focusedDate());
339
517
  };
340
518
 
341
519
  // Check if a date is unavailable
@@ -350,10 +528,21 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
350
528
 
351
529
  const minValue = access(props.minValue);
352
530
  const maxValue = access(props.maxValue);
353
- const calDate = toCalendarDate(date);
531
+ const calDate = toDisplayCalendarDate(date);
532
+
533
+ if (minValue && calDate.compare(toDisplayCalendarDate(minValue)) < 0) return true;
534
+ if (maxValue && calDate.compare(toDisplayCalendarDate(maxValue)) > 0) return true;
535
+
536
+ return false;
537
+ };
538
+
539
+ const isDateOutsideAllowedRange = (date: DateValue): boolean => {
540
+ const minValue = access(props.minValue);
541
+ const maxValue = access(props.maxValue);
542
+ const calDate = toDisplayCalendarDate(date);
354
543
 
355
- if (minValue && calDate.compare(toCalendarDate(minValue)) < 0) return true;
356
- if (maxValue && calDate.compare(toCalendarDate(maxValue)) > 0) return true;
544
+ if (minValue && calDate.compare(toDisplayCalendarDate(minValue)) < 0) return true;
545
+ if (maxValue && calDate.compare(toDisplayCalendarDate(maxValue)) > 0) return true;
357
546
 
358
547
  return false;
359
548
  };
@@ -361,7 +550,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
361
550
  // Check if a date is outside the visible range
362
551
  const isOutsideVisibleRange = (date: DateValue): boolean => {
363
552
  const range = visibleRange();
364
- const calDate = toCalendarDate(date);
553
+ const calDate = toDisplayCalendarDate(date);
365
554
  return !isSameMonth(calDate, range.start) && !isSameMonth(calDate, range.end);
366
555
  };
367
556
 
@@ -372,11 +561,13 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
372
561
 
373
562
  // Navigation methods
374
563
  const focusPreviousPage = () => {
375
- setFocusedDate(focusedDate().subtract({ months: 1 }));
564
+ const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
565
+ setFocusedDate(focusedDate().subtract({ months: pageMonths }));
376
566
  };
377
567
 
378
568
  const focusNextPage = () => {
379
- setFocusedDate(focusedDate().add({ months: 1 }));
569
+ const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths;
570
+ setFocusedDate(focusedDate().add({ months: pageMonths }));
380
571
  };
381
572
 
382
573
  const focusPreviousSection = () => {
@@ -387,6 +578,18 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
387
578
  setFocusedDate(focusedDate().add({ years: 1 }));
388
579
  };
389
580
 
581
+ const isPreviousVisibleRangeInvalid = () => {
582
+ const start = visibleRange().start;
583
+ const previous = start.subtract({ days: 1 });
584
+ return isSameDay(previous, start) || isDateOutsideAllowedRange(previous);
585
+ };
586
+
587
+ const isNextVisibleRangeInvalid = () => {
588
+ const end = visibleRange().end;
589
+ const next = end.add({ days: 1 });
590
+ return isSameDay(next, end) || isDateOutsideAllowedRange(next);
591
+ };
592
+
390
593
  const focusPreviousDay = () => {
391
594
  setFocusedDate(focusedDate().subtract({ days: 1 }));
392
595
  };
@@ -454,12 +657,12 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
454
657
  // Get dates in a specific week
455
658
  const getDatesInWeek = (
456
659
  weekIndex: number,
457
- monthStartDate?: CalendarDate
660
+ monthStartDate?: CalendarDate,
458
661
  ): (CalendarDate | null)[] => {
459
662
  const startDate = monthStartDate ?? visibleRange().start;
460
663
 
461
664
  const monthStart = startOfMonth(startDate);
462
- const weekStart = startOfWeek(monthStart, locale);
665
+ const weekStart = startOfWeek(monthStart, locale(), firstDayOfWeekName());
463
666
 
464
667
  const week: (CalendarDate | null)[] = [];
465
668
  const firstDayOfWeek = weekStart.add({ weeks: weekIndex });
@@ -475,7 +678,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
475
678
  // Get number of weeks in a month
476
679
  const getWeeksInMonthFn = (date?: CalendarDate): number => {
477
680
  const monthDate = date ?? focusedDate();
478
- return getWeeksInMonth(monthDate, locale);
681
+ return getWeeksInMonth(monthDate, locale(), firstDayOfWeekName());
479
682
  };
480
683
 
481
684
  return {
@@ -490,7 +693,10 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
490
693
  isReadOnly,
491
694
  visibleRange,
492
695
  timeZone,
696
+ locale,
697
+ firstDayOfWeek: firstDayOfWeekName,
493
698
  validationState,
699
+ isValueInvalid,
494
700
  isSelected,
495
701
  isSelectionStart,
496
702
  isSelectionEnd,
@@ -503,6 +709,8 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
503
709
  focusNextPage,
504
710
  focusPreviousSection,
505
711
  focusNextSection,
712
+ isPreviousVisibleRangeInvalid,
713
+ isNextVisibleRangeInvalid,
506
714
  focusPreviousDay,
507
715
  focusNextDay,
508
716
  focusPreviousWeek,
@@ -523,13 +731,19 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
523
731
  };
524
732
  }
525
733
 
526
- // ============================================
527
- // HELPER FUNCTIONS
528
- // ============================================
529
-
530
734
  /**
531
735
  * Converts a DateValue to a CalendarDate.
532
736
  */
533
737
  function toCalendarDate(date: DateValue): CalendarDate {
534
738
  return intlToCalendarDate(date);
535
739
  }
740
+
741
+ function convertValue(newValue: CalendarDate, oldValue?: DateValue | null): DateValue {
742
+ const localValue = intlToCalendar(newValue, oldValue?.calendar ?? new GregorianCalendar());
743
+
744
+ if (oldValue && "hour" in oldValue) {
745
+ return oldValue.set(localValue);
746
+ }
747
+
748
+ return localValue;
749
+ }