@proyecto-viviana/solid-stately 0.3.0 → 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 (63) hide show
  1. package/dist/calendar/IncompleteDate.d.ts +61 -0
  2. package/dist/calendar/createCalendarState.d.ts +20 -10
  3. package/dist/calendar/createDateFieldState.d.ts +10 -3
  4. package/dist/calendar/createRangeCalendarState.d.ts +10 -2
  5. package/dist/calendar/index.d.ts +1 -1
  6. package/dist/collections/createListState.d.ts +4 -1
  7. package/dist/collections/createSelectionState.d.ts +17 -5
  8. package/dist/collections/getChildNodes.d.ts +11 -0
  9. package/dist/collections/index.d.ts +2 -1
  10. package/dist/combobox/createComboBoxState.d.ts +7 -0
  11. package/dist/dnd/index.d.ts +1 -1
  12. package/dist/dnd/types.d.ts +3 -1
  13. package/dist/grid/types.d.ts +17 -3
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.js +1570 -425
  16. package/dist/index.js.map +1 -1
  17. package/dist/select/createSelectState.d.ts +6 -0
  18. package/dist/selection/Selection.d.ts +16 -0
  19. package/dist/selection/SelectionManager.d.ts +110 -0
  20. package/dist/selection/createMultipleSelectionState.d.ts +59 -0
  21. package/dist/selection/index.d.ts +1 -0
  22. package/dist/table/TableCollection.d.ts +30 -0
  23. package/dist/table/createTreeGridState.d.ts +15 -0
  24. package/dist/table/index.d.ts +2 -1
  25. package/dist/table/types.d.ts +58 -0
  26. package/dist/toast/createToastState.d.ts +16 -0
  27. package/dist/toast/index.d.ts +1 -1
  28. package/dist/tree/types.d.ts +14 -6
  29. package/package.json +1 -1
  30. package/src/calendar/IncompleteDate.ts +430 -0
  31. package/src/calendar/createCalendarState.ts +104 -37
  32. package/src/calendar/createDateFieldState.ts +262 -261
  33. package/src/calendar/createRangeCalendarState.ts +131 -19
  34. package/src/calendar/index.ts +2 -0
  35. package/src/collections/createListState.ts +50 -19
  36. package/src/collections/createMenuState.ts +3 -0
  37. package/src/collections/createSelectionState.ts +38 -28
  38. package/src/collections/getChildNodes.ts +98 -0
  39. package/src/collections/index.ts +9 -0
  40. package/src/combobox/createComboBoxState.ts +8 -0
  41. package/src/dnd/index.d.ts +1 -0
  42. package/src/dnd/index.ts +1 -0
  43. package/src/dnd/types.d.ts +3 -1
  44. package/src/dnd/types.ts +4 -1
  45. package/src/grid/createGridState.ts +36 -2
  46. package/src/grid/types.ts +17 -3
  47. package/src/index.d.ts +1 -0
  48. package/src/index.ts +11 -0
  49. package/src/select/createSelectState.ts +15 -2
  50. package/src/selection/Selection.ts +28 -0
  51. package/src/selection/SelectionManager.ts +526 -0
  52. package/src/selection/createMultipleSelectionState.ts +247 -0
  53. package/src/selection/index.ts +6 -0
  54. package/src/table/TableCollection.ts +213 -43
  55. package/src/table/createTableState.ts +20 -0
  56. package/src/table/createTreeGridState.ts +146 -0
  57. package/src/table/index.ts +3 -0
  58. package/src/table/types.ts +66 -0
  59. package/src/tabs/createTabListState.ts +11 -9
  60. package/src/toast/createToastState.ts +43 -10
  61. package/src/toast/index.ts +1 -0
  62. package/src/tree/createTreeState.ts +37 -6
  63. package/src/tree/types.ts +14 -6
@@ -0,0 +1,430 @@
1
+ /**
2
+ * IncompleteDate for Solid-Stately
3
+ *
4
+ * Represents a date that is incomplete or otherwise invalid as a result of user
5
+ * editing (for example February 31st, if the day is edited before the month).
6
+ * This lets a date field hold an invalid intermediate value while typing and
7
+ * only constrain it on blur, matching upstream behavior.
8
+ *
9
+ * Based on @react-stately/datepicker IncompleteDate
10
+ */
11
+
12
+ import {
13
+ type AnyDateTime,
14
+ type Calendar,
15
+ CalendarDate,
16
+ type DateValue,
17
+ ZonedDateTime,
18
+ } from "@internationalized/date";
19
+ import type { DateSegmentType } from "./createDateFieldState";
20
+
21
+ type HourCycle = "h12" | "h11" | "h23" | "h24";
22
+
23
+ /**
24
+ * This class represents a date that is incomplete or otherwise invalid as a result of user editing.
25
+ * For example, it can represent temporary dates such as February 31st if the user edits the day
26
+ * before the month. Times are represented according to an hour cycle rather than always in 24 hour
27
+ * time. This enables the user to adjust the day period (e.g. am/pm) independently from the hour.
28
+ */
29
+ export class IncompleteDate {
30
+ calendar: Calendar;
31
+ era: string | null;
32
+ year: number | null;
33
+ month: number | null;
34
+ day: number | null;
35
+ hour: number | null;
36
+ hourCycle: HourCycle;
37
+ dayPeriod: number | null;
38
+ minute: number | null;
39
+ second: number | null;
40
+ millisecond: number | null;
41
+ offset: number | null;
42
+
43
+ constructor(
44
+ calendar: Calendar,
45
+ hourCycle: HourCycle,
46
+ dateValue?: Partial<Omit<AnyDateTime, "copy">> | null,
47
+ ) {
48
+ this.era = dateValue?.era ?? null;
49
+ this.calendar = calendar;
50
+ this.year = dateValue?.year ?? null;
51
+ this.month = dateValue?.month ?? null;
52
+ this.day = dateValue?.day ?? null;
53
+ this.hour = dateValue?.hour ?? null;
54
+ this.hourCycle = hourCycle;
55
+ this.dayPeriod = null;
56
+ this.minute = dateValue?.minute ?? null;
57
+ this.second = dateValue?.second ?? null;
58
+ this.millisecond = dateValue?.millisecond ?? null;
59
+ this.offset = "offset" in (dateValue ?? {}) ? (dateValue as { offset: number }).offset : null;
60
+
61
+ // Convert the hour from 24 hour time to the given hour cycle.
62
+ if (this.hour != null) {
63
+ const [dayPeriod, hour] = toHourCycle(this.hour, hourCycle);
64
+ this.dayPeriod = dayPeriod;
65
+ this.hour = hour;
66
+ }
67
+ }
68
+
69
+ copy(): IncompleteDate {
70
+ const res = new IncompleteDate(this.calendar, this.hourCycle);
71
+ res.era = this.era;
72
+ res.year = this.year;
73
+ res.month = this.month;
74
+ res.day = this.day;
75
+ res.hour = this.hour;
76
+ res.dayPeriod = this.dayPeriod;
77
+ res.minute = this.minute;
78
+ res.second = this.second;
79
+ res.millisecond = this.millisecond;
80
+ res.offset = this.offset;
81
+ return res;
82
+ }
83
+
84
+ /** Checks whether all the specified segments have a value. */
85
+ isComplete(segments: DateSegmentType[]): boolean {
86
+ return segments.every((segment) => this[segment as keyof IncompleteDate] != null);
87
+ }
88
+
89
+ /** Checks whether the given date value matches this value for the specified segments. */
90
+ validate(dt: DateValue, segments: DateSegmentType[]): boolean {
91
+ return segments.every((segment) => {
92
+ if ((segment === "hour" || segment === "dayPeriod") && "hour" in dt) {
93
+ const [dayPeriod, hour] = toHourCycle(dt.hour, this.hourCycle);
94
+ return this.dayPeriod === dayPeriod && this.hour === hour;
95
+ }
96
+ return this[segment as keyof IncompleteDate] === dt[segment as keyof DateValue];
97
+ });
98
+ }
99
+
100
+ /** Checks if the date is empty (i.e. all specified segments are null). */
101
+ isCleared(segments: DateSegmentType[]): boolean {
102
+ return segments.every((segment) => this[segment as keyof IncompleteDate] === null);
103
+ }
104
+
105
+ /** Sets the given field. */
106
+ set(field: DateSegmentType, value: number | string, placeholder: DateValue): IncompleteDate {
107
+ const result = this.copy();
108
+ (result as unknown as Record<string, unknown>)[field] = value;
109
+ if (field === "hour" && result.dayPeriod == null && "hour" in placeholder) {
110
+ result.dayPeriod = toHourCycle(placeholder.hour, this.hourCycle)[0];
111
+ }
112
+ if (field === "year" && result.era == null) {
113
+ result.era = placeholder.era;
114
+ }
115
+
116
+ // clear offset when a date/time field changes since it may no longer be valid
117
+ if (field !== "second" && field !== "literal" && field !== "timeZoneName") {
118
+ result.offset = null;
119
+ }
120
+ return result;
121
+ }
122
+
123
+ /** Sets the given field to null. */
124
+ clear(field: DateSegmentType): IncompleteDate {
125
+ const result = this.copy();
126
+ (result as unknown as Record<string, unknown>)[field] = null;
127
+ if (field === "year") {
128
+ result.era = null;
129
+ }
130
+
131
+ // clear offset when a field is cleared since it may no longer be valid
132
+ result.offset = null;
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Increments or decrements the given field. If it is null, then it is set to the placeholder
138
+ * value.
139
+ */
140
+ cycle(
141
+ field: DateSegmentType,
142
+ amount: number,
143
+ placeholder: DateValue,
144
+ displaySegments: DateSegmentType[],
145
+ ): IncompleteDate {
146
+ const res = this.copy();
147
+
148
+ // If field is null, default to placeholder.
149
+ if (res[field as keyof IncompleteDate] == null && field !== "dayPeriod" && field !== "era") {
150
+ if (field === "hour" && "hour" in placeholder) {
151
+ const [dayPeriod, hour] = toHourCycle(placeholder.hour, this.hourCycle);
152
+ res.dayPeriod = dayPeriod;
153
+ res.hour = hour;
154
+ } else {
155
+ (res as unknown as Record<string, unknown>)[field] = placeholder[field as keyof DateValue];
156
+ }
157
+ if (field === "year" && res.era == null) {
158
+ res.era = placeholder.era;
159
+ }
160
+
161
+ return res;
162
+ }
163
+
164
+ switch (field) {
165
+ case "era": {
166
+ const eras = this.calendar.getEras();
167
+ let index = eras.indexOf(res.era!);
168
+ index = cycleValue(index, amount, 0, eras.length - 1);
169
+ res.era = eras[index];
170
+ break;
171
+ }
172
+ case "year": {
173
+ // Use CalendarDate to cycle so that we update the era when going between 1 AD and 1 BC.
174
+ let date = new CalendarDate(
175
+ this.calendar,
176
+ this.era ?? placeholder.era,
177
+ this.year ?? placeholder.year,
178
+ this.month ?? 1,
179
+ this.day ?? 1,
180
+ );
181
+ date = date.cycle(field, amount, { round: field === "year" });
182
+ res.era = date.era;
183
+ res.year = date.year;
184
+ break;
185
+ }
186
+ case "month":
187
+ res.month = cycleValue(res.month ?? 1, amount, 1, this.calendar.getMaximumMonthsInYear());
188
+ break;
189
+ case "day":
190
+ // Allow incrementing up to the maximum number of days in any month.
191
+ res.day = cycleValue(res.day ?? 1, amount, 1, this.calendar.getMaximumDaysInMonth());
192
+ break;
193
+ case "hour": {
194
+ // if date is fully defined or it is just a time field, and we have a time zone, use toValue to get a ZonedDateTime to cycle
195
+ // so DST fallback is properly handled
196
+ const hasDateSegements = displaySegments.some((s) => ["year", "month", "day"].includes(s));
197
+ if (
198
+ "timeZone" in placeholder &&
199
+ (!hasDateSegements || (res.year != null && res.month != null && res.day != null))
200
+ ) {
201
+ let date = this.toValue(placeholder) as ZonedDateTime;
202
+ date = date.cycle("hour", amount, {
203
+ hourCycle: this.hourCycle === "h12" ? 12 : 24,
204
+ round: false,
205
+ });
206
+ const [dayPeriod, adjustedHour] = toHourCycle(date.hour, this.hourCycle);
207
+ res.hour = adjustedHour;
208
+ res.dayPeriod = dayPeriod;
209
+ res.offset = date.offset;
210
+ } else {
211
+ const hours = res.hour ?? 0;
212
+ const limits = this.getSegmentLimits("hour")!;
213
+ res.hour = cycleValue(hours, amount, limits.minValue, limits.maxValue);
214
+ if (res.dayPeriod == null && "hour" in placeholder) {
215
+ res.dayPeriod = toHourCycle(placeholder.hour, this.hourCycle)[0];
216
+ }
217
+ }
218
+ break;
219
+ }
220
+ case "dayPeriod":
221
+ res.dayPeriod = cycleValue(res.dayPeriod ?? 0, amount, 0, 1);
222
+ break;
223
+ case "minute":
224
+ res.minute = cycleValue(res.minute ?? 0, amount, 0, 59, true);
225
+ break;
226
+ case "second":
227
+ res.second = cycleValue(res.second ?? 0, amount, 0, 59, true);
228
+ break;
229
+ }
230
+
231
+ return res;
232
+ }
233
+
234
+ /**
235
+ * Converts the incomplete date to a full date value, using the provided value for any unset
236
+ * fields.
237
+ */
238
+ toValue(value: DateValue): DateValue {
239
+ if ("hour" in value) {
240
+ let hour = this.hour;
241
+ if (hour != null) {
242
+ hour = fromHourCycle(hour, this.dayPeriod ?? 0, this.hourCycle);
243
+ } else if (this.hourCycle === "h12" || this.hourCycle === "h11") {
244
+ hour = this.dayPeriod === 1 ? 12 : 0;
245
+ }
246
+
247
+ let res = value.set({
248
+ era: this.era ?? value.era,
249
+ year: this.year ?? value.year,
250
+ month: this.month ?? value.month,
251
+ day: this.day ?? value.day,
252
+ hour: hour ?? value.hour,
253
+ minute: this.minute ?? value.minute,
254
+ second: this.second ?? value.second,
255
+ millisecond: this.millisecond ?? value.millisecond,
256
+ });
257
+
258
+ if ("offset" in res && this.offset != null && res.offset !== this.offset) {
259
+ res = res.add({ milliseconds: res.offset - this.offset });
260
+ }
261
+
262
+ return res;
263
+ } else {
264
+ return value.set({
265
+ era: this.era ?? value.era,
266
+ year: this.year ?? value.year,
267
+ month: this.month ?? value.month,
268
+ day: this.day ?? value.day,
269
+ });
270
+ }
271
+ }
272
+
273
+ getSegmentLimits(
274
+ type: string,
275
+ ): { value: number | null; minValue: number; maxValue: number } | undefined {
276
+ switch (type) {
277
+ case "era": {
278
+ const eras = this.calendar.getEras();
279
+ return {
280
+ value: this.era != null ? eras.indexOf(this.era) : eras.length - 1,
281
+ minValue: 0,
282
+ maxValue: eras.length - 1,
283
+ };
284
+ }
285
+ case "year":
286
+ return {
287
+ value: this.year,
288
+ minValue: 1,
289
+ maxValue: 9999,
290
+ };
291
+ case "month":
292
+ return {
293
+ value: this.month,
294
+ minValue: 1,
295
+ maxValue: this.calendar.getMaximumMonthsInYear(),
296
+ };
297
+ case "day":
298
+ return {
299
+ value: this.day,
300
+ minValue: 1,
301
+ maxValue: this.calendar.getMaximumDaysInMonth(),
302
+ };
303
+ case "dayPeriod": {
304
+ return {
305
+ value: this.dayPeriod,
306
+ minValue: 0,
307
+ maxValue: 1,
308
+ };
309
+ }
310
+ case "hour": {
311
+ let minValue = 0;
312
+ let maxValue = 23;
313
+ if (this.hourCycle === "h12") {
314
+ minValue = 1;
315
+ maxValue = 12;
316
+ } else if (this.hourCycle === "h11") {
317
+ minValue = 0;
318
+ maxValue = 11;
319
+ }
320
+
321
+ return {
322
+ value: this.hour,
323
+ minValue,
324
+ maxValue,
325
+ };
326
+ }
327
+ case "minute":
328
+ return {
329
+ value: this.minute,
330
+ minValue: 0,
331
+ maxValue: 59,
332
+ };
333
+ case "second":
334
+ return {
335
+ value: this.second,
336
+ minValue: 0,
337
+ maxValue: 59,
338
+ };
339
+ }
340
+ }
341
+ }
342
+
343
+ function cycleValue(
344
+ value: number,
345
+ amount: number,
346
+ min: number,
347
+ max: number,
348
+ round = false,
349
+ ): number {
350
+ if (round) {
351
+ value += Math.sign(amount);
352
+
353
+ if (value < min) {
354
+ value = max;
355
+ }
356
+
357
+ const div = Math.abs(amount);
358
+ if (amount > 0) {
359
+ value = Math.ceil(value / div) * div;
360
+ } else {
361
+ value = Math.floor(value / div) * div;
362
+ }
363
+
364
+ if (value > max) {
365
+ value = min;
366
+ }
367
+ } else {
368
+ value += amount;
369
+ if (value < min) {
370
+ value = max - (min - value - 1);
371
+ } else if (value > max) {
372
+ value = min + (value - max - 1);
373
+ }
374
+ }
375
+
376
+ return value;
377
+ }
378
+
379
+ function toHourCycle(hour: number, hourCycle: HourCycle): [number | null, number] {
380
+ let dayPeriod: number | null = hour >= 12 ? 1 : 0;
381
+ switch (hourCycle) {
382
+ case "h11":
383
+ // Hours are numbered from 0 to 11. Used in Japan.
384
+ if (hour >= 12) {
385
+ hour -= 12;
386
+ }
387
+ break;
388
+ case "h12":
389
+ // Hours are numbered from 12 (representing 0) to 11.
390
+ if (hour === 0) {
391
+ hour = 12;
392
+ } else if (hour > 12) {
393
+ hour -= 12;
394
+ }
395
+ break;
396
+ case "h23":
397
+ // 24 hour time, numbered 0 to 23.
398
+ dayPeriod = null;
399
+ break;
400
+ case "h24":
401
+ // 24 hour time numbered 24 to 23. Unused but supported by Intl.DateTimeFormat.
402
+ hour += 1;
403
+ dayPeriod = null;
404
+ }
405
+
406
+ return [dayPeriod, hour];
407
+ }
408
+
409
+ function fromHourCycle(hour: number, dayPeriod: number, hourCycle: HourCycle): number {
410
+ switch (hourCycle) {
411
+ case "h11":
412
+ if (dayPeriod === 1) {
413
+ hour += 12;
414
+ }
415
+ break;
416
+ case "h12":
417
+ if (hour === 12) {
418
+ hour = 0;
419
+ }
420
+ if (dayPeriod === 1) {
421
+ hour += 12;
422
+ }
423
+ break;
424
+ case "h24":
425
+ hour -= 1;
426
+ break;
427
+ }
428
+
429
+ return hour;
430
+ }
@@ -35,13 +35,23 @@ export type CalendarDayOfWeek = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" |
35
35
 
36
36
  const dayOfWeekNames: CalendarDayOfWeek[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
37
37
 
38
- export interface CalendarStateProps<T extends DateValue = DateValue> {
38
+ export type CalendarSelectionMode = "single" | "multiple";
39
+ export type CalendarValueType<T, M extends CalendarSelectionMode> = M extends "single"
40
+ ? T
41
+ : readonly T[];
42
+
43
+ export interface CalendarStateProps<
44
+ T extends DateValue = DateValue,
45
+ M extends CalendarSelectionMode = "single",
46
+ > {
39
47
  /** The current value (controlled). */
40
- value?: MaybeAccessor<T | null>;
48
+ value?: MaybeAccessor<CalendarValueType<T | null, M> | undefined>;
41
49
  /** The default value (uncontrolled). */
42
- defaultValue?: T | null;
50
+ defaultValue?: CalendarValueType<T | null, M> | null;
43
51
  /** Handler called when the value changes. */
44
- onChange?: (value: T | null) => void;
52
+ onChange?: (value: CalendarValueType<T | null, M>) => void;
53
+ /** Whether single or multiple date selection is enabled. */
54
+ selectionMode?: MaybeAccessor<M>;
45
55
  /** The minimum allowed date. */
46
56
  minValue?: MaybeAccessor<DateValue | undefined>;
47
57
  /** The maximum allowed date. */
@@ -82,15 +92,24 @@ export interface CalendarStateProps<T extends DateValue = DateValue> {
82
92
  firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
83
93
  }
84
94
 
85
- export interface CalendarState<T extends DateValue = DateValue> {
86
- /** The currently selected date. */
87
- value: Accessor<T | null>;
88
- /** Sets the selected date. */
89
- setValue: (value: T | null) => void;
95
+ export interface CalendarState<
96
+ T extends DateValue = DateValue,
97
+ M extends CalendarSelectionMode = "single",
98
+ > {
99
+ /** Whether single or multiple selection is enabled. */
100
+ selectionMode: Accessor<CalendarSelectionMode>;
101
+ /** The currently selected date(s). */
102
+ value: Accessor<CalendarValueType<T | null, M>>;
103
+ /** Sets the selected date(s). */
104
+ setValue: (value: CalendarValueType<T | null, M>) => void;
90
105
  /** The currently focused date. */
91
106
  focusedDate: Accessor<CalendarDate>;
92
107
  /** Sets the focused date. */
93
108
  setFocusedDate: (date: CalendarDate) => void;
109
+ /** The minimum allowed date that a user may select. */
110
+ minValue: Accessor<DateValue | null>;
111
+ /** The maximum allowed date that a user may select. */
112
+ maxValue: Accessor<DateValue | null>;
94
113
  /** Whether the calendar is disabled. */
95
114
  isDisabled: Accessor<boolean>;
96
115
  /** Whether the calendar is read-only. */
@@ -164,11 +183,17 @@ export interface CalendarState<T extends DateValue = DateValue> {
164
183
  /**
165
184
  * Provides state management for a calendar component.
166
185
  */
167
- export function createCalendarState<T extends DateValue = CalendarDate>(
168
- props: CalendarStateProps<T> = {},
169
- ): CalendarState<T> {
186
+ export function createCalendarState<
187
+ T extends DateValue = CalendarDate,
188
+ M extends CalendarSelectionMode = "single",
189
+ >(props: CalendarStateProps<T, M> = {}): CalendarState<T, M> {
170
190
  const timeZone = getLocalTimeZone();
171
191
  const locale = createMemo(() => access(props.locale) ?? "en-US");
192
+ const minValueState = createMemo(() => access(props.minValue) ?? null);
193
+ const maxValueState = createMemo(() => access(props.maxValue) ?? null);
194
+ const selectionMode = createMemo<CalendarSelectionMode>(
195
+ () => access(props.selectionMode) ?? "single",
196
+ );
172
197
  const resolvedOptions = createMemo(() => new DateFormatter(locale()).resolvedOptions());
173
198
  const calendar = createMemo(() =>
174
199
  (props.createCalendar ?? intlCreateCalendar)(resolvedOptions().calendar as CalendarIdentifier),
@@ -181,19 +206,21 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
181
206
  intlToCalendar(toCalendarDate(date), calendar());
182
207
 
183
208
  const selectionAlignmentOffset = (): number => {
209
+ // Mirrors @react-stately/calendar useCalendarState, where selectionAlignment
210
+ // defaults to "center" (alignCenter) when not provided.
184
211
  switch (props.selectionAlignment) {
212
+ case "start":
213
+ return 0;
185
214
  case "end":
186
215
  return Math.max(visibleMonths - 1, 0);
187
- case "center": {
216
+ case "center":
217
+ default: {
188
218
  let halfDuration = Math.floor(visibleMonths / 2);
189
219
  if (halfDuration > 0 && visibleMonths % 2 === 0) {
190
220
  halfDuration--;
191
221
  }
192
222
  return halfDuration;
193
223
  }
194
- case "start":
195
- default:
196
- return 0;
197
224
  }
198
225
  };
199
226
 
@@ -238,18 +265,24 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
238
265
  return toDisplayCalendarDate(props.defaultFocusedValue);
239
266
  }
240
267
  const controlledValue = access(props.value);
241
- if (controlledValue) {
242
- return toDisplayCalendarDate(controlledValue);
268
+ const controlledFirst = Array.isArray(controlledValue) ? controlledValue[0] : controlledValue;
269
+ if (controlledFirst) {
270
+ return toDisplayCalendarDate(controlledFirst);
243
271
  }
244
- if (props.defaultValue) {
245
- return toDisplayCalendarDate(props.defaultValue);
272
+ const defaultFirst = Array.isArray(props.defaultValue)
273
+ ? props.defaultValue[0]
274
+ : props.defaultValue;
275
+ if (defaultFirst) {
276
+ return toDisplayCalendarDate(defaultFirst);
246
277
  }
247
278
  return intlToCalendar(today(timeZone), calendar());
248
279
  };
249
280
 
250
281
  // State signals
251
282
  const initialFocusedDate = constrainDate(getInitialFocusedDate());
252
- const [internalValue, setInternalValue] = createSignal<T | null>(props.defaultValue ?? null);
283
+ const [internalValue, setInternalValue] = createSignal<T | T[] | null>(
284
+ (props.defaultValue as T | T[] | null) ?? null,
285
+ );
253
286
  const [focusedDate, setFocusedDateInternal] = createSignal<CalendarDate>(initialFocusedDate);
254
287
  const [visibleRangeStart, setVisibleRangeStart] = createSignal<CalendarDate>(
255
288
  alignVisibleRangeStart(initialFocusedDate),
@@ -258,12 +291,15 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
258
291
  const [isPaginating, setIsPaginating] = createSignal(false);
259
292
 
260
293
  // Controlled vs uncontrolled value
261
- const sourceValue = createMemo<T | null>(() => {
294
+ const sourceValue = createMemo<T | T[] | null>(() => {
262
295
  const controlled = access(props.value);
263
- return controlled !== undefined ? controlled : internalValue();
296
+ return controlled !== undefined ? (controlled as T | T[] | null) : internalValue();
264
297
  });
265
- const value = createMemo<T | null>(() => {
298
+ const value = createMemo<T | T[] | null>(() => {
266
299
  const currentValue = sourceValue();
300
+ if (Array.isArray(currentValue)) {
301
+ return currentValue.map((v) => toDisplayCalendarDate(v) as unknown as T);
302
+ }
267
303
  return currentValue ? (toDisplayCalendarDate(currentValue) as unknown as T) : null;
268
304
  });
269
305
 
@@ -370,13 +406,22 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
370
406
  });
371
407
 
372
408
  // Set value with onChange callback
373
- const setValue = (newValue: T | null) => {
409
+ const setValue = (newValue: T | T[] | null) => {
374
410
  if (isDisabled() || isReadOnly()) return;
375
411
 
376
412
  const oldValue = sourceValue();
377
- const nextValue = newValue
378
- ? (convertValue(constrainDate(toDisplayCalendarDate(newValue)), oldValue) as T)
379
- : null;
413
+ const referenceValue = Array.isArray(oldValue) ? (oldValue[0] ?? null) : oldValue;
414
+
415
+ let nextValue: T | T[] | null;
416
+ if (newValue == null) {
417
+ nextValue = selectionMode() === "multiple" ? ([] as T[]) : null;
418
+ } else if (Array.isArray(newValue)) {
419
+ nextValue = newValue.map(
420
+ (v) => convertValue(constrainDate(toDisplayCalendarDate(v)), referenceValue) as T,
421
+ );
422
+ } else {
423
+ nextValue = convertValue(constrainDate(toDisplayCalendarDate(newValue)), referenceValue) as T;
424
+ }
380
425
 
381
426
  const controlled = access(props.value);
382
427
  if (controlled === undefined) {
@@ -384,7 +429,7 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
384
429
  }
385
430
 
386
431
  if (!Object.is(oldValue, nextValue) && props.onChange) {
387
- props.onChange(nextValue);
432
+ props.onChange(nextValue as CalendarValueType<T | null, M>);
388
433
  }
389
434
  };
390
435
 
@@ -404,8 +449,12 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
404
449
  // Check if a date is selected
405
450
  const isSelected = (date: DateValue): boolean => {
406
451
  const v = value();
407
- if (!v) return false;
408
- return isSameDay(toDisplayCalendarDate(date), toDisplayCalendarDate(v));
452
+ if (v == null) return false;
453
+ const target = toDisplayCalendarDate(date);
454
+ if (Array.isArray(v)) {
455
+ return v.some((d) => isSameDay(target, toDisplayCalendarDate(d)));
456
+ }
457
+ return isSameDay(target, toDisplayCalendarDate(v));
409
458
  };
410
459
 
411
460
  // Check if a date is focused
@@ -502,17 +551,32 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
502
551
  const selectFocusedDate = () => {
503
552
  if (isReadOnly() || isDisabled()) return;
504
553
  const date = focusedDate();
505
- if (!isCellDisabled(date) && !isCellUnavailable(date)) {
506
- setValue(date as unknown as T);
554
+ if (!isCellUnavailable(date)) {
555
+ selectDate(date);
507
556
  }
508
557
  };
509
558
 
510
559
  const selectDate = (date: CalendarDate) => {
511
560
  if (isReadOnly() || isDisabled()) return;
512
- if (!isCellDisabled(date) && !isCellUnavailable(date)) {
561
+ if (isCellDisabled(date) || isCellUnavailable(date)) return;
562
+
563
+ if (selectionMode() === "multiple") {
564
+ const current = value();
565
+ const base: CalendarDate[] = Array.isArray(current)
566
+ ? current.map((v) => toDisplayCalendarDate(v))
567
+ : current != null
568
+ ? [toDisplayCalendarDate(current)]
569
+ : [];
570
+ const target = toDisplayCalendarDate(date);
571
+ const index = base.findIndex((d) => isSameDay(d, target));
572
+ const nextValue =
573
+ index >= 0 ? [...base.slice(0, index), ...base.slice(index + 1)] : [...base, target];
574
+ setValue(nextValue as unknown as T[]);
575
+ } else {
513
576
  setValue(date as unknown as T);
514
- setFocusedDate(date);
515
577
  }
578
+
579
+ setFocusedDate(date);
516
580
  };
517
581
 
518
582
  // Get dates in a specific week
@@ -548,8 +612,9 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
548
612
  };
549
613
 
550
614
  return {
551
- value,
552
- setValue,
615
+ selectionMode,
616
+ value: value as unknown as Accessor<CalendarValueType<T | null, M>>,
617
+ setValue: setValue as unknown as (value: CalendarValueType<T | null, M>) => void,
553
618
  focusedDate,
554
619
  setFocusedDate,
555
620
  isDisabled,
@@ -557,6 +622,8 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
557
622
  visibleRange,
558
623
  timeZone,
559
624
  locale,
625
+ minValue: minValueState,
626
+ maxValue: maxValueState,
560
627
  firstDayOfWeek: firstDayOfWeekName,
561
628
  validationState,
562
629
  isValueInvalid,