@reachweb/alpine-calendar 0.1.1

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.
@@ -0,0 +1,2946 @@
1
+ class CalendarDate {
2
+ // 1-31
3
+ constructor(year, month, day) {
4
+ this.year = year;
5
+ this.month = month;
6
+ this.day = day;
7
+ }
8
+ // ---------------------------------------------------------------------------
9
+ // Factory methods
10
+ // ---------------------------------------------------------------------------
11
+ /** Resolve "today" in a given IANA timezone (fallback: browser default). */
12
+ static today(timezone) {
13
+ const now = /* @__PURE__ */ new Date();
14
+ if (timezone) {
15
+ return CalendarDate.fromNativeDate(now, timezone);
16
+ }
17
+ return new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
18
+ }
19
+ /** Create from a native Date, interpreting it in the given timezone. */
20
+ static fromNativeDate(date, timezone) {
21
+ if (timezone) {
22
+ const parts = new Intl.DateTimeFormat("en-US", {
23
+ timeZone: timezone,
24
+ year: "numeric",
25
+ month: "2-digit",
26
+ day: "2-digit"
27
+ }).formatToParts(date);
28
+ let year = 0;
29
+ let month = 0;
30
+ let day = 0;
31
+ for (const part of parts) {
32
+ if (part.type === "year") year = Number(part.value);
33
+ else if (part.type === "month") month = Number(part.value);
34
+ else if (part.type === "day") day = Number(part.value);
35
+ }
36
+ return new CalendarDate(year, month, day);
37
+ }
38
+ return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
39
+ }
40
+ /** Create from an ISO string (YYYY-MM-DD). Returns null for invalid formats or out-of-range values. */
41
+ static fromISO(iso) {
42
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
43
+ if (!match) return null;
44
+ const year = Number(match[1]);
45
+ const month = Number(match[2]);
46
+ const day = Number(match[3]);
47
+ if (month < 1 || month > 12) return null;
48
+ if (day < 1 || day > daysInMonth(year, month)) return null;
49
+ return new CalendarDate(year, month, day);
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Serialization
53
+ // ---------------------------------------------------------------------------
54
+ /** Convert to a native Date (midnight local time). */
55
+ toNativeDate() {
56
+ return new Date(this.year, this.month - 1, this.day);
57
+ }
58
+ /** Serialize to ISO string YYYY-MM-DD. */
59
+ toISO() {
60
+ const y = String(this.year).padStart(4, "0");
61
+ const m = String(this.month).padStart(2, "0");
62
+ const d = String(this.day).padStart(2, "0");
63
+ return `${y}-${m}-${d}`;
64
+ }
65
+ /** Unique string key for use in Sets/Maps. */
66
+ toKey() {
67
+ return this.toISO();
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Comparison helpers
71
+ // ---------------------------------------------------------------------------
72
+ isSame(other) {
73
+ return this.year === other.year && this.month === other.month && this.day === other.day;
74
+ }
75
+ isBefore(other) {
76
+ if (this.year !== other.year) return this.year < other.year;
77
+ if (this.month !== other.month) return this.month < other.month;
78
+ return this.day < other.day;
79
+ }
80
+ isAfter(other) {
81
+ if (this.year !== other.year) return this.year > other.year;
82
+ if (this.month !== other.month) return this.month > other.month;
83
+ return this.day > other.day;
84
+ }
85
+ /** Inclusive range check: start <= this <= end. */
86
+ isBetween(start, end) {
87
+ return !this.isBefore(start) && !this.isAfter(end);
88
+ }
89
+ /**
90
+ * Number of days from this date to another.
91
+ * Positive when `other` is after `this`, negative when before.
92
+ * Uses UTC to avoid DST issues.
93
+ */
94
+ diffDays(other) {
95
+ const a = Date.UTC(this.year, this.month - 1, this.day);
96
+ const b = Date.UTC(other.year, other.month - 1, other.day);
97
+ return Math.round((b - a) / 864e5);
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Arithmetic (returns new CalendarDate — immutable)
101
+ // ---------------------------------------------------------------------------
102
+ addDays(days) {
103
+ const d = this.toNativeDate();
104
+ d.setDate(d.getDate() + days);
105
+ return CalendarDate.fromNativeDate(d);
106
+ }
107
+ /** Add months with day clamping (e.g. Jan 31 + 1 month = Feb 28/29). */
108
+ addMonths(months) {
109
+ let newMonth = this.month - 1 + months;
110
+ const newYear = this.year + Math.floor(newMonth / 12);
111
+ newMonth = (newMonth % 12 + 12) % 12;
112
+ const maxDay = daysInMonth(newYear, newMonth + 1);
113
+ const newDay = Math.min(this.day, maxDay);
114
+ return new CalendarDate(newYear, newMonth + 1, newDay);
115
+ }
116
+ addYears(years) {
117
+ const maxDay = daysInMonth(this.year + years, this.month);
118
+ const newDay = Math.min(this.day, maxDay);
119
+ return new CalendarDate(this.year + years, this.month, newDay);
120
+ }
121
+ startOfMonth() {
122
+ return new CalendarDate(this.year, this.month, 1);
123
+ }
124
+ endOfMonth() {
125
+ return new CalendarDate(this.year, this.month, daysInMonth(this.year, this.month));
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Formatting
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * Format the date using Intl.DateTimeFormat options.
132
+ *
133
+ * @param options - Intl.DateTimeFormat options (e.g. { month: 'long', year: 'numeric' })
134
+ * @param locale - BCP 47 locale string (default: browser locale)
135
+ */
136
+ format(options, locale) {
137
+ const d = this.toNativeDate();
138
+ return new Intl.DateTimeFormat(locale, options).format(d);
139
+ }
140
+ }
141
+ function daysInMonth(year, month) {
142
+ return new Date(year, month, 0).getDate();
143
+ }
144
+ function getISOWeekNumber(date) {
145
+ const d = new Date(Date.UTC(date.year, date.month - 1, date.day));
146
+ const dayNum = d.getUTCDay() || 7;
147
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
148
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
149
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
150
+ }
151
+ function precomputeSets(opts) {
152
+ return {
153
+ disabledKeys: opts.disabledDates ? new Set(opts.disabledDates.map((d) => d.toKey())) : void 0,
154
+ disabledDays: opts.disabledDaysOfWeek ? new Set(opts.disabledDaysOfWeek) : void 0,
155
+ enabledKeys: opts.enabledDates ? new Set(opts.enabledDates.map((d) => d.toKey())) : void 0,
156
+ enabledDays: opts.enabledDaysOfWeek ? new Set(opts.enabledDaysOfWeek) : void 0,
157
+ disabledMonths: opts.disabledMonths ? new Set(opts.disabledMonths) : void 0,
158
+ enabledMonths: opts.enabledMonths ? new Set(opts.enabledMonths) : void 0,
159
+ disabledYears: opts.disabledYears ? new Set(opts.disabledYears) : void 0,
160
+ enabledYears: opts.enabledYears ? new Set(opts.enabledYears) : void 0
161
+ };
162
+ }
163
+ function checkDisabled(date, minDate, maxDate, sets) {
164
+ if (minDate && date.isBefore(minDate)) return true;
165
+ if (maxDate && date.isAfter(maxDate)) return true;
166
+ if (sets.enabledKeys && sets.enabledKeys.has(date.toKey())) return false;
167
+ if (sets.enabledYears && !sets.enabledYears.has(date.year)) return true;
168
+ if (sets.disabledYears && sets.disabledYears.has(date.year)) return true;
169
+ if (sets.enabledMonths && !sets.enabledMonths.has(date.month)) return true;
170
+ if (sets.disabledMonths && sets.disabledMonths.has(date.month)) return true;
171
+ const needsDow = sets.enabledDays !== void 0 || sets.disabledDays !== void 0;
172
+ const dow = needsDow ? date.toNativeDate().getDay() : -1;
173
+ if (sets.enabledDays && !sets.enabledDays.has(dow)) return true;
174
+ if (sets.disabledKeys && sets.disabledKeys.has(date.toKey())) return true;
175
+ if (sets.disabledDays && sets.disabledDays.has(dow)) return true;
176
+ return false;
177
+ }
178
+ function matchesRule(rule, date) {
179
+ if (rule.from && rule.to) {
180
+ return date.isBetween(rule.from, rule.to);
181
+ }
182
+ if (rule.recurringMonths) {
183
+ return rule.recurringMonths.has(date.month);
184
+ }
185
+ return false;
186
+ }
187
+ function findBestRule(rules, date) {
188
+ let best;
189
+ for (const rule of rules) {
190
+ if (!matchesRule(rule, date)) continue;
191
+ if (!best || rule.priority > best.priority) {
192
+ best = rule;
193
+ }
194
+ }
195
+ return best;
196
+ }
197
+ function createDateConstraint(options) {
198
+ const { minDate, maxDate, rules } = options;
199
+ const globalSets = precomputeSets(options);
200
+ const precomputedRules = rules == null ? void 0 : rules.map((rule, index) => ({
201
+ from: rule.from,
202
+ to: rule.to,
203
+ recurringMonths: rule.months ? new Set(rule.months) : void 0,
204
+ minDate: rule.minDate,
205
+ maxDate: rule.maxDate,
206
+ minRange: rule.minRange,
207
+ maxRange: rule.maxRange,
208
+ sets: precomputeSets(rule),
209
+ hasMinDate: rule.minDate !== void 0,
210
+ hasMaxDate: rule.maxDate !== void 0,
211
+ priority: rule.priority ?? 0,
212
+ originalIndex: index
213
+ }));
214
+ if (!precomputedRules || precomputedRules.length === 0) {
215
+ return (date) => checkDisabled(date, minDate, maxDate, globalSets);
216
+ }
217
+ return (date) => {
218
+ const rule = findBestRule(precomputedRules, date);
219
+ if (!rule) {
220
+ return checkDisabled(date, minDate, maxDate, globalSets);
221
+ }
222
+ const effectiveMinDate = rule.hasMinDate ? rule.minDate : minDate;
223
+ const effectiveMaxDate = rule.hasMaxDate ? rule.maxDate : maxDate;
224
+ const mergedSets = {
225
+ disabledKeys: rule.sets.disabledKeys !== void 0 ? rule.sets.disabledKeys : globalSets.disabledKeys,
226
+ disabledDays: rule.sets.disabledDays !== void 0 ? rule.sets.disabledDays : globalSets.disabledDays,
227
+ enabledKeys: rule.sets.enabledKeys !== void 0 ? rule.sets.enabledKeys : globalSets.enabledKeys,
228
+ enabledDays: rule.sets.enabledDays !== void 0 ? rule.sets.enabledDays : globalSets.enabledDays,
229
+ disabledMonths: rule.sets.disabledMonths !== void 0 ? rule.sets.disabledMonths : globalSets.disabledMonths,
230
+ enabledMonths: rule.sets.enabledMonths !== void 0 ? rule.sets.enabledMonths : globalSets.enabledMonths,
231
+ disabledYears: rule.sets.disabledYears !== void 0 ? rule.sets.disabledYears : globalSets.disabledYears,
232
+ enabledYears: rule.sets.enabledYears !== void 0 ? rule.sets.enabledYears : globalSets.enabledYears
233
+ };
234
+ return checkDisabled(date, effectiveMinDate, effectiveMaxDate, mergedSets);
235
+ };
236
+ }
237
+ function createRangeValidator(options) {
238
+ const { minRange, maxRange, rules } = options;
239
+ if (minRange === void 0 && maxRange === void 0 && (!rules || rules.every((r) => r.minRange === void 0 && r.maxRange === void 0))) {
240
+ return () => true;
241
+ }
242
+ const rangeRules = rules == null ? void 0 : rules.map((rule, index) => ({
243
+ from: rule.from,
244
+ to: rule.to,
245
+ recurringMonths: rule.months ? new Set(rule.months) : void 0,
246
+ minDate: rule.minDate,
247
+ maxDate: rule.maxDate,
248
+ minRange: rule.minRange,
249
+ maxRange: rule.maxRange,
250
+ sets: precomputeSets(rule),
251
+ hasMinDate: rule.minDate !== void 0,
252
+ hasMaxDate: rule.maxDate !== void 0,
253
+ priority: rule.priority ?? 0,
254
+ originalIndex: index
255
+ }));
256
+ return (start, end) => {
257
+ let effectiveMinRange = minRange;
258
+ let effectiveMaxRange = maxRange;
259
+ if (rangeRules) {
260
+ const rule = findBestRule(rangeRules, start);
261
+ if (rule) {
262
+ if (rule.minRange !== void 0) effectiveMinRange = rule.minRange;
263
+ if (rule.maxRange !== void 0) effectiveMaxRange = rule.maxRange;
264
+ }
265
+ }
266
+ const length = start.diffDays(end) + 1;
267
+ if (effectiveMinRange !== void 0 && length < effectiveMinRange) return false;
268
+ if (effectiveMaxRange !== void 0 && length > effectiveMaxRange) return false;
269
+ return true;
270
+ };
271
+ }
272
+ function createMonthConstraint(options) {
273
+ const { minDate, maxDate } = options;
274
+ const disabledMonthSet = options.disabledMonths ? new Set(options.disabledMonths) : void 0;
275
+ const enabledMonthSet = options.enabledMonths ? new Set(options.enabledMonths) : void 0;
276
+ const disabledYearSet = options.disabledYears ? new Set(options.disabledYears) : void 0;
277
+ const enabledYearSet = options.enabledYears ? new Set(options.enabledYears) : void 0;
278
+ return (year, month) => {
279
+ if (minDate) {
280
+ const endOfMonth = new CalendarDate(year, month, 1).endOfMonth();
281
+ if (endOfMonth.isBefore(minDate)) return true;
282
+ }
283
+ if (maxDate) {
284
+ const startOfMonth = new CalendarDate(year, month, 1);
285
+ if (startOfMonth.isAfter(maxDate)) return true;
286
+ }
287
+ if (enabledYearSet && !enabledYearSet.has(year)) return true;
288
+ if (disabledYearSet && disabledYearSet.has(year)) return true;
289
+ if (enabledMonthSet && !enabledMonthSet.has(month)) return true;
290
+ if (disabledMonthSet && disabledMonthSet.has(month)) return true;
291
+ return false;
292
+ };
293
+ }
294
+ function createYearConstraint(options) {
295
+ const { minDate, maxDate } = options;
296
+ const disabledYearSet = options.disabledYears ? new Set(options.disabledYears) : void 0;
297
+ const enabledYearSet = options.enabledYears ? new Set(options.enabledYears) : void 0;
298
+ return (year) => {
299
+ if (minDate) {
300
+ const endOfYear = new CalendarDate(year, 12, 31);
301
+ if (endOfYear.isBefore(minDate)) return true;
302
+ }
303
+ if (maxDate) {
304
+ const startOfYear = new CalendarDate(year, 1, 1);
305
+ if (startOfYear.isAfter(maxDate)) return true;
306
+ }
307
+ if (enabledYearSet && !enabledYearSet.has(year)) return true;
308
+ if (disabledYearSet && disabledYearSet.has(year)) return true;
309
+ return false;
310
+ };
311
+ }
312
+ function isDateDisabled(date, options) {
313
+ return createDateConstraint(options)(date);
314
+ }
315
+ const DEFAULT_MESSAGES = {
316
+ beforeMinDate: "Before the earliest available date",
317
+ afterMaxDate: "After the latest available date",
318
+ disabledDate: "This date is not available",
319
+ disabledDayOfWeek: "This day of the week is not available",
320
+ disabledMonth: "This month is not available",
321
+ disabledYear: "This year is not available",
322
+ notEnabledDate: "This date is not available",
323
+ notEnabledDayOfWeek: "This day of the week is not available",
324
+ notEnabledMonth: "This month is not available",
325
+ notEnabledYear: "This year is not available"
326
+ };
327
+ function collectReasons(date, minDate, maxDate, sets, msgs) {
328
+ const reasons = [];
329
+ if (minDate && date.isBefore(minDate)) {
330
+ reasons.push(msgs.beforeMinDate);
331
+ return reasons;
332
+ }
333
+ if (maxDate && date.isAfter(maxDate)) {
334
+ reasons.push(msgs.afterMaxDate);
335
+ return reasons;
336
+ }
337
+ if (sets.enabledKeys && sets.enabledKeys.has(date.toKey())) return reasons;
338
+ if (sets.enabledYears && !sets.enabledYears.has(date.year)) {
339
+ reasons.push(msgs.notEnabledYear);
340
+ return reasons;
341
+ }
342
+ if (sets.disabledYears && sets.disabledYears.has(date.year)) {
343
+ reasons.push(msgs.disabledYear);
344
+ return reasons;
345
+ }
346
+ if (sets.enabledMonths && !sets.enabledMonths.has(date.month)) {
347
+ reasons.push(msgs.notEnabledMonth);
348
+ return reasons;
349
+ }
350
+ if (sets.disabledMonths && sets.disabledMonths.has(date.month)) {
351
+ reasons.push(msgs.disabledMonth);
352
+ return reasons;
353
+ }
354
+ const needsDow = sets.enabledDays !== void 0 || sets.disabledDays !== void 0;
355
+ const dow = needsDow ? date.toNativeDate().getDay() : -1;
356
+ if (sets.enabledDays && !sets.enabledDays.has(dow)) {
357
+ reasons.push(msgs.notEnabledDayOfWeek);
358
+ }
359
+ if (sets.disabledKeys && sets.disabledKeys.has(date.toKey())) {
360
+ reasons.push(msgs.disabledDate);
361
+ }
362
+ if (sets.disabledDays && sets.disabledDays.has(dow)) {
363
+ reasons.push(msgs.disabledDayOfWeek);
364
+ }
365
+ return reasons;
366
+ }
367
+ function createDisabledReasons(options, messages) {
368
+ const { minDate, maxDate, rules } = options;
369
+ const msgs = { ...DEFAULT_MESSAGES, ...messages };
370
+ const globalSets = precomputeSets(options);
371
+ const precomputedRules = rules == null ? void 0 : rules.map((rule, index) => ({
372
+ from: rule.from,
373
+ to: rule.to,
374
+ recurringMonths: rule.months ? new Set(rule.months) : void 0,
375
+ minDate: rule.minDate,
376
+ maxDate: rule.maxDate,
377
+ minRange: rule.minRange,
378
+ maxRange: rule.maxRange,
379
+ sets: precomputeSets(rule),
380
+ hasMinDate: rule.minDate !== void 0,
381
+ hasMaxDate: rule.maxDate !== void 0,
382
+ priority: rule.priority ?? 0,
383
+ originalIndex: index
384
+ }));
385
+ if (!precomputedRules || precomputedRules.length === 0) {
386
+ return (date) => collectReasons(date, minDate, maxDate, globalSets, msgs);
387
+ }
388
+ return (date) => {
389
+ const rule = findBestRule(precomputedRules, date);
390
+ if (!rule) {
391
+ return collectReasons(date, minDate, maxDate, globalSets, msgs);
392
+ }
393
+ const effectiveMinDate = rule.hasMinDate ? rule.minDate : minDate;
394
+ const effectiveMaxDate = rule.hasMaxDate ? rule.maxDate : maxDate;
395
+ const mergedSets = {
396
+ disabledKeys: rule.sets.disabledKeys !== void 0 ? rule.sets.disabledKeys : globalSets.disabledKeys,
397
+ disabledDays: rule.sets.disabledDays !== void 0 ? rule.sets.disabledDays : globalSets.disabledDays,
398
+ enabledKeys: rule.sets.enabledKeys !== void 0 ? rule.sets.enabledKeys : globalSets.enabledKeys,
399
+ enabledDays: rule.sets.enabledDays !== void 0 ? rule.sets.enabledDays : globalSets.enabledDays,
400
+ disabledMonths: rule.sets.disabledMonths !== void 0 ? rule.sets.disabledMonths : globalSets.disabledMonths,
401
+ enabledMonths: rule.sets.enabledMonths !== void 0 ? rule.sets.enabledMonths : globalSets.enabledMonths,
402
+ disabledYears: rule.sets.disabledYears !== void 0 ? rule.sets.disabledYears : globalSets.disabledYears,
403
+ enabledYears: rule.sets.enabledYears !== void 0 ? rule.sets.enabledYears : globalSets.enabledYears
404
+ };
405
+ return collectReasons(date, effectiveMinDate, effectiveMaxDate, mergedSets, msgs);
406
+ };
407
+ }
408
+ function generateMonth(year, month, firstDayOfWeek = 0, today, isDisabled) {
409
+ const todayRef = today ?? CalendarDate.today();
410
+ const disabledFn = isDisabled ?? (() => false);
411
+ const firstOfMonth = new CalendarDate(year, month, 1);
412
+ const firstDow = firstOfMonth.toNativeDate().getDay();
413
+ const offset = (firstDow - firstDayOfWeek + 7) % 7;
414
+ const gridStart = firstOfMonth.addDays(-offset);
415
+ const rows = [];
416
+ const weekNumbers = [];
417
+ for (let row = 0; row < 6; row++) {
418
+ const cells = [];
419
+ for (let col = 0; col < 7; col++) {
420
+ const dayIndex = row * 7 + col;
421
+ const date = gridStart.addDays(dayIndex);
422
+ cells.push({
423
+ date,
424
+ isCurrentMonth: date.month === month && date.year === year,
425
+ isToday: date.isSame(todayRef),
426
+ isDisabled: disabledFn(date)
427
+ });
428
+ }
429
+ const firstCell = cells[0];
430
+ if (firstCell) weekNumbers.push(getISOWeekNumber(firstCell.date));
431
+ rows.push(cells);
432
+ }
433
+ return { year, month, rows, weekNumbers };
434
+ }
435
+ function generateMonthGrid(year, today, locale, isMonthDisabled) {
436
+ const todayRef = today ?? CalendarDate.today();
437
+ const disabledFn = isMonthDisabled ?? (() => false);
438
+ const rows = [];
439
+ for (let row = 0; row < 3; row++) {
440
+ const cells = [];
441
+ for (let col = 0; col < 4; col++) {
442
+ const month = row * 4 + col + 1;
443
+ const d = new CalendarDate(year, month, 1);
444
+ cells.push({
445
+ month,
446
+ year,
447
+ label: d.format({ month: "short" }, locale),
448
+ isCurrentMonth: todayRef.month === month && todayRef.year === year,
449
+ isDisabled: disabledFn(year, month)
450
+ });
451
+ }
452
+ rows.push(cells);
453
+ }
454
+ return rows;
455
+ }
456
+ function generateYearGrid(centerYear, today, isYearDisabled) {
457
+ const todayRef = today ?? CalendarDate.today();
458
+ const disabledFn = isYearDisabled ?? (() => false);
459
+ const startYear = Math.floor(centerYear / 12) * 12;
460
+ const rows = [];
461
+ for (let row = 0; row < 3; row++) {
462
+ const cells = [];
463
+ for (let col = 0; col < 4; col++) {
464
+ const year = startYear + row * 4 + col;
465
+ cells.push({
466
+ year,
467
+ label: String(year),
468
+ isCurrentYear: todayRef.year === year,
469
+ isDisabled: disabledFn(year)
470
+ });
471
+ }
472
+ rows.push(cells);
473
+ }
474
+ return rows;
475
+ }
476
+ function generateMonths(year, month, count, firstDayOfWeek = 0, today, isDisabled) {
477
+ const grids = [];
478
+ for (let i = 0; i < count; i++) {
479
+ let targetMonth = month + i;
480
+ let targetYear = year;
481
+ while (targetMonth > 12) {
482
+ targetMonth -= 12;
483
+ targetYear++;
484
+ }
485
+ grids.push(generateMonth(targetYear, targetMonth, firstDayOfWeek, today, isDisabled));
486
+ }
487
+ return grids;
488
+ }
489
+ class SingleSelection {
490
+ constructor(initial) {
491
+ this.selected = null;
492
+ this.selected = initial ?? null;
493
+ }
494
+ isSelected(date) {
495
+ return this.selected !== null && this.selected.isSame(date);
496
+ }
497
+ /** Toggle: select the date, or deselect if it's already selected. */
498
+ toggle(date) {
499
+ if (this.selected !== null && this.selected.isSame(date)) {
500
+ this.selected = null;
501
+ } else {
502
+ this.selected = date;
503
+ }
504
+ }
505
+ clear() {
506
+ this.selected = null;
507
+ }
508
+ toArray() {
509
+ return this.selected !== null ? [this.selected] : [];
510
+ }
511
+ /** Returns the ISO string of the selected date, or empty string. */
512
+ toValue() {
513
+ return this.selected !== null ? this.selected.toISO() : "";
514
+ }
515
+ /** Direct access to the current selection. */
516
+ getSelected() {
517
+ return this.selected;
518
+ }
519
+ }
520
+ class MultipleSelection {
521
+ constructor(initial) {
522
+ this.keys = /* @__PURE__ */ new Set();
523
+ if (initial) {
524
+ for (const date of initial) {
525
+ this.keys.add(date.toKey());
526
+ }
527
+ }
528
+ }
529
+ isSelected(date) {
530
+ return this.keys.has(date.toKey());
531
+ }
532
+ /** Toggle: add the date if absent, remove if present. */
533
+ toggle(date) {
534
+ const key = date.toKey();
535
+ if (this.keys.has(key)) {
536
+ this.keys.delete(key);
537
+ } else {
538
+ this.keys.add(key);
539
+ }
540
+ }
541
+ clear() {
542
+ this.keys.clear();
543
+ }
544
+ /** Returns selected dates sorted chronologically. */
545
+ toArray() {
546
+ return [...this.keys].sort().map((key) => CalendarDate.fromISO(key));
547
+ }
548
+ /** Returns comma-separated ISO strings, sorted chronologically. */
549
+ toValue() {
550
+ return [...this.keys].sort().join(", ");
551
+ }
552
+ /** Number of currently selected dates. */
553
+ get count() {
554
+ return this.keys.size;
555
+ }
556
+ }
557
+ class RangeSelection {
558
+ constructor(start, end) {
559
+ this.start = null;
560
+ this.end = null;
561
+ this.start = start ?? null;
562
+ this.end = end ?? null;
563
+ }
564
+ isSelected(date) {
565
+ if (this.start !== null && this.start.isSame(date)) return true;
566
+ if (this.end !== null && this.end.isSame(date)) return true;
567
+ return false;
568
+ }
569
+ /**
570
+ * Toggle behavior for range selection:
571
+ * 1. Nothing selected → set as start
572
+ * 2. Only start selected → set as end (swap if before start)
573
+ * 3. Both selected → clear and set as new start
574
+ */
575
+ toggle(date) {
576
+ if (this.start === null) {
577
+ this.start = date;
578
+ } else if (this.end === null) {
579
+ if (date.isBefore(this.start)) {
580
+ this.end = this.start;
581
+ this.start = date;
582
+ } else if (date.isSame(this.start)) {
583
+ this.start = null;
584
+ } else {
585
+ this.end = date;
586
+ }
587
+ } else {
588
+ this.start = date;
589
+ this.end = null;
590
+ }
591
+ }
592
+ /**
593
+ * Directly set both start and end of the range.
594
+ * Useful for programmatic assignment where `toggle()` semantics are undesirable
595
+ * (e.g., same-day ranges where toggling the same date would deselect).
596
+ */
597
+ setRange(start, end) {
598
+ this.start = start;
599
+ this.end = end;
600
+ }
601
+ clear() {
602
+ this.start = null;
603
+ this.end = null;
604
+ }
605
+ /** Returns [start, end] if both set, [start] if partial, or [] if empty. */
606
+ toArray() {
607
+ if (this.start === null) return [];
608
+ if (this.end === null) return [this.start];
609
+ return [this.start, this.end];
610
+ }
611
+ /** Returns "start – end" ISO strings, or just start, or empty string. */
612
+ toValue() {
613
+ if (this.start === null) return "";
614
+ if (this.end === null) return this.start.toISO();
615
+ return `${this.start.toISO()} – ${this.end.toISO()}`;
616
+ }
617
+ /**
618
+ * Check if a date falls within the range (inclusive), with optional
619
+ * hover preview for visual feedback during range building.
620
+ *
621
+ * When only start is selected and the user hovers over a date,
622
+ * pass that date as `hoverDate` to preview the range.
623
+ */
624
+ isInRange(date, hoverDate) {
625
+ if (this.start !== null && this.end !== null) {
626
+ return date.isBetween(this.start, this.end);
627
+ }
628
+ if (this.start !== null && hoverDate !== void 0) {
629
+ const rangeStart = this.start.isBefore(hoverDate) ? this.start : hoverDate;
630
+ const rangeEnd = this.start.isBefore(hoverDate) ? hoverDate : this.start;
631
+ return date.isBetween(rangeStart, rangeEnd);
632
+ }
633
+ return false;
634
+ }
635
+ /** Direct access to the range endpoints. */
636
+ getStart() {
637
+ return this.start;
638
+ }
639
+ getEnd() {
640
+ return this.end;
641
+ }
642
+ /** Whether the range is partially selected (start only, no end). */
643
+ isPartial() {
644
+ return this.start !== null && this.end === null;
645
+ }
646
+ /** Whether the range is fully selected (both start and end). */
647
+ isComplete() {
648
+ return this.start !== null && this.end !== null;
649
+ }
650
+ }
651
+ const TOKEN_FORMATTERS = {
652
+ YYYY: (d) => String(d.year).padStart(4, "0"),
653
+ YY: (d) => String(d.year % 100).padStart(2, "0"),
654
+ MM: (d) => String(d.month).padStart(2, "0"),
655
+ M: (d) => String(d.month),
656
+ DD: (d) => String(d.day).padStart(2, "0"),
657
+ D: (d) => String(d.day)
658
+ };
659
+ const TOKEN_NAMES$2 = Object.keys(TOKEN_FORMATTERS).sort((a, b) => b.length - a.length);
660
+ function formatDate(date, format) {
661
+ let remaining = format;
662
+ let result = "";
663
+ while (remaining.length > 0) {
664
+ let matched = false;
665
+ for (const token of TOKEN_NAMES$2) {
666
+ if (remaining.startsWith(token)) {
667
+ const formatter = TOKEN_FORMATTERS[token];
668
+ if (formatter) {
669
+ result += formatter(date);
670
+ remaining = remaining.slice(token.length);
671
+ matched = true;
672
+ break;
673
+ }
674
+ }
675
+ }
676
+ if (!matched) {
677
+ result += remaining[0];
678
+ remaining = remaining.slice(1);
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ function formatRange(start, end, format) {
684
+ return `${formatDate(start, format)} – ${formatDate(end, format)}`;
685
+ }
686
+ function formatMultiple(dates, format, maxDisplay) {
687
+ if (dates.length === 0) return "";
688
+ if (maxDisplay !== void 0 && dates.length > maxDisplay) {
689
+ return `${dates.length} dates selected`;
690
+ }
691
+ return dates.map((d) => formatDate(d, format)).join(", ");
692
+ }
693
+ const TOKENS = {
694
+ YYYY: { pattern: "(\\d{4})", extract: (m) => Number(m) },
695
+ YY: { pattern: "(\\d{2})", extract: (m) => 2e3 + Number(m) },
696
+ MM: { pattern: "(\\d{1,2})", extract: (m) => Number(m) },
697
+ M: { pattern: "(\\d{1,2})", extract: (m) => Number(m) },
698
+ DD: { pattern: "(\\d{1,2})", extract: (m) => Number(m) },
699
+ D: { pattern: "(\\d{1,2})", extract: (m) => Number(m) }
700
+ };
701
+ const TOKEN_NAMES$1 = Object.keys(TOKENS).sort((a, b) => b.length - a.length);
702
+ function compileFormat(format) {
703
+ let remaining = format;
704
+ let regexStr = "^";
705
+ const extractors = [];
706
+ let groupIndex = 1;
707
+ while (remaining.length > 0) {
708
+ let matched = false;
709
+ for (const name of TOKEN_NAMES$1) {
710
+ if (remaining.startsWith(name)) {
711
+ const def = TOKENS[name];
712
+ if (def) {
713
+ regexStr += def.pattern;
714
+ extractors.push({ token: name, index: groupIndex });
715
+ groupIndex++;
716
+ remaining = remaining.slice(name.length);
717
+ matched = true;
718
+ break;
719
+ }
720
+ }
721
+ }
722
+ if (!matched) {
723
+ const char = remaining[0];
724
+ regexStr += escapeRegex(char);
725
+ remaining = remaining.slice(1);
726
+ }
727
+ }
728
+ regexStr += "$";
729
+ return { regex: new RegExp(regexStr), extractors };
730
+ }
731
+ function escapeRegex(str) {
732
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
733
+ }
734
+ function isValidDate(year, month, day) {
735
+ if (month < 1 || month > 12) return false;
736
+ if (day < 1) return false;
737
+ if (day > daysInMonth(year, month)) return false;
738
+ if (year < 1) return false;
739
+ return true;
740
+ }
741
+ function parseDate(input, format) {
742
+ const trimmed = input.trim();
743
+ if (trimmed === "") return null;
744
+ const { regex, extractors } = compileFormat(format);
745
+ const match = regex.exec(trimmed);
746
+ if (!match) return null;
747
+ let year = 0;
748
+ let month = 0;
749
+ let day = 0;
750
+ for (const { token, index } of extractors) {
751
+ const value = match[index];
752
+ if (value === void 0) return null;
753
+ const def = TOKENS[token];
754
+ if (!def) return null;
755
+ const extracted = def.extract(value);
756
+ if (token === "YYYY" || token === "YY") {
757
+ year = extracted;
758
+ } else if (token === "MM" || token === "M") {
759
+ month = extracted;
760
+ } else if (token === "DD" || token === "D") {
761
+ day = extracted;
762
+ }
763
+ }
764
+ if (!isValidDate(year, month, day)) return null;
765
+ return new CalendarDate(year, month, day);
766
+ }
767
+ function parseDateRange(input, format) {
768
+ const trimmed = input.trim();
769
+ if (trimmed === "") return null;
770
+ const separators = [" – ", " — ", " - "];
771
+ for (const sep of separators) {
772
+ const idx = trimmed.indexOf(sep);
773
+ if (idx !== -1) {
774
+ const startStr = trimmed.slice(0, idx);
775
+ const endStr = trimmed.slice(idx + sep.length);
776
+ const start = parseDate(startStr, format) ?? CalendarDate.fromISO(startStr.trim());
777
+ const end = parseDate(endStr, format) ?? CalendarDate.fromISO(endStr.trim());
778
+ if (start && end) return [start, end];
779
+ }
780
+ }
781
+ return null;
782
+ }
783
+ function parseDateMultiple(input, format) {
784
+ const trimmed = input.trim();
785
+ if (trimmed === "") return [];
786
+ const parts = trimmed.split(",");
787
+ const dates = [];
788
+ for (const part of parts) {
789
+ const date = parseDate(part, format) ?? CalendarDate.fromISO(part.trim());
790
+ if (date) dates.push(date);
791
+ }
792
+ return dates;
793
+ }
794
+ const TOKEN_DIGIT_COUNT = {
795
+ YYYY: 4,
796
+ YY: 2,
797
+ MM: 2,
798
+ M: 2,
799
+ DD: 2,
800
+ D: 2
801
+ };
802
+ const TOKEN_NAMES = Object.keys(TOKEN_DIGIT_COUNT).sort((a, b) => b.length - a.length);
803
+ function extractDigits(str) {
804
+ return str.replace(/\D/g, "");
805
+ }
806
+ function countDigits(str) {
807
+ let count = 0;
808
+ for (const ch of str) {
809
+ if (ch >= "0" && ch <= "9") count++;
810
+ }
811
+ return count;
812
+ }
813
+ function applyMaskSlots(digits, slots) {
814
+ let result = "";
815
+ let digitIndex = 0;
816
+ for (const slot of slots) {
817
+ if (slot.type === "digit") {
818
+ if (digitIndex >= digits.length) break;
819
+ result += digits[digitIndex];
820
+ digitIndex++;
821
+ } else {
822
+ if (digitIndex > 0) {
823
+ result += slot.char;
824
+ } else {
825
+ break;
826
+ }
827
+ }
828
+ }
829
+ return result;
830
+ }
831
+ function cursorAfterNDigits(masked, slots, digitCount) {
832
+ if (digitCount <= 0) return 0;
833
+ let seen = 0;
834
+ for (let i = 0; i < masked.length && i < slots.length; i++) {
835
+ const slot = slots[i];
836
+ if (slot && slot.type === "digit") {
837
+ seen++;
838
+ if (seen === digitCount) {
839
+ let pos = i + 1;
840
+ while (pos < slots.length && pos < masked.length) {
841
+ const next = slots[pos];
842
+ if (!next || next.type !== "literal") break;
843
+ pos++;
844
+ }
845
+ return pos;
846
+ }
847
+ }
848
+ }
849
+ return masked.length;
850
+ }
851
+ function parseFormatToSlots(format) {
852
+ const slots = [];
853
+ let remaining = format;
854
+ while (remaining.length > 0) {
855
+ let matched = false;
856
+ for (const token of TOKEN_NAMES) {
857
+ if (remaining.startsWith(token)) {
858
+ const count = TOKEN_DIGIT_COUNT[token];
859
+ if (count === void 0) continue;
860
+ for (let i = 0; i < count; i++) {
861
+ slots.push({ type: "digit", char: "9" });
862
+ }
863
+ remaining = remaining.slice(token.length);
864
+ matched = true;
865
+ break;
866
+ }
867
+ }
868
+ if (!matched) {
869
+ slots.push({ type: "literal", char: remaining[0] });
870
+ remaining = remaining.slice(1);
871
+ }
872
+ }
873
+ return slots;
874
+ }
875
+ function createMask(format) {
876
+ const slots = parseFormatToSlots(format);
877
+ const maxDigits = slots.filter((s) => s.type === "digit").length;
878
+ const pattern = slots.map((s) => s.type === "digit" ? "9" : s.char).join("");
879
+ return {
880
+ apply(rawInput) {
881
+ const digits = extractDigits(rawInput).slice(0, maxDigits);
882
+ return applyMaskSlots(digits, slots);
883
+ },
884
+ get pattern() {
885
+ return pattern;
886
+ },
887
+ get format() {
888
+ return format;
889
+ },
890
+ get length() {
891
+ return slots.length;
892
+ },
893
+ get slots() {
894
+ return slots;
895
+ },
896
+ get maxDigits() {
897
+ return maxDigits;
898
+ }
899
+ };
900
+ }
901
+ function createMaskHandlers(mask) {
902
+ const { slots, maxDigits } = mask;
903
+ function onInput(e) {
904
+ const el = e.target;
905
+ const rawValue = el.value;
906
+ const cursorPos = el.selectionStart ?? rawValue.length;
907
+ const digitsBefore = countDigits(rawValue.slice(0, cursorPos));
908
+ const digits = extractDigits(rawValue).slice(0, maxDigits);
909
+ const masked = applyMaskSlots(digits, slots);
910
+ el.value = masked;
911
+ const newCursor = cursorAfterNDigits(masked, slots, digitsBefore);
912
+ el.setSelectionRange(newCursor, newCursor);
913
+ }
914
+ function onKeyDown(e) {
915
+ const el = e.target;
916
+ const pos = el.selectionStart ?? 0;
917
+ const selEnd = el.selectionEnd ?? pos;
918
+ if (pos !== selEnd) return;
919
+ if (e.key === "Backspace" && pos > 0) {
920
+ const prevSlot = pos <= slots.length ? slots[pos - 1] : void 0;
921
+ if (prevSlot && prevSlot.type === "literal") {
922
+ e.preventDefault();
923
+ let targetPos = pos - 1;
924
+ while (targetPos > 0) {
925
+ const s = slots[targetPos - 1];
926
+ if (!s || s.type !== "literal") break;
927
+ targetPos--;
928
+ }
929
+ if (targetPos > 0) {
930
+ const currentDigits = extractDigits(el.value);
931
+ const digitIndex = countDigits(el.value.slice(0, targetPos));
932
+ if (digitIndex > 0) {
933
+ const newDigits = currentDigits.slice(0, digitIndex - 1) + currentDigits.slice(digitIndex);
934
+ const masked = applyMaskSlots(newDigits, slots);
935
+ el.value = masked;
936
+ const newCursor = cursorAfterNDigits(masked, slots, digitIndex - 1);
937
+ el.setSelectionRange(newCursor, newCursor);
938
+ }
939
+ }
940
+ }
941
+ }
942
+ if (e.key === "Delete" && pos < el.value.length) {
943
+ const curSlot = pos < slots.length ? slots[pos] : void 0;
944
+ if (curSlot && curSlot.type === "literal") {
945
+ e.preventDefault();
946
+ let targetPos = pos + 1;
947
+ while (targetPos < slots.length) {
948
+ const s = slots[targetPos];
949
+ if (!s || s.type !== "literal") break;
950
+ targetPos++;
951
+ }
952
+ if (targetPos < el.value.length) {
953
+ const currentDigits = extractDigits(el.value);
954
+ const digitIndex = countDigits(el.value.slice(0, targetPos));
955
+ const newDigits = currentDigits.slice(0, digitIndex) + currentDigits.slice(digitIndex + 1);
956
+ const masked = applyMaskSlots(newDigits, slots);
957
+ el.value = masked;
958
+ const newCursor = cursorAfterNDigits(masked, slots, digitIndex);
959
+ el.setSelectionRange(newCursor, newCursor);
960
+ }
961
+ }
962
+ }
963
+ }
964
+ function onPaste(e) {
965
+ var _a;
966
+ e.preventDefault();
967
+ const el = e.target;
968
+ const pasted = ((_a = e.clipboardData) == null ? void 0 : _a.getData("text")) ?? "";
969
+ const pastedDigits = extractDigits(pasted);
970
+ if (pastedDigits.length === 0) return;
971
+ const pos = el.selectionStart ?? 0;
972
+ const end = el.selectionEnd ?? pos;
973
+ const currentDigits = extractDigits(el.value);
974
+ const digitsBefore = countDigits(el.value.slice(0, pos));
975
+ const digitsInSelection = countDigits(el.value.slice(pos, end));
976
+ const newDigits = (currentDigits.slice(0, digitsBefore) + pastedDigits + currentDigits.slice(digitsBefore + digitsInSelection)).slice(0, maxDigits);
977
+ const masked = applyMaskSlots(newDigits, slots);
978
+ el.value = masked;
979
+ const targetDigitCount = Math.min(digitsBefore + pastedDigits.length, maxDigits);
980
+ const newCursor = cursorAfterNDigits(masked, slots, targetDigitCount);
981
+ el.setSelectionRange(newCursor, newCursor);
982
+ }
983
+ return { onInput, onKeyDown, onPaste };
984
+ }
985
+ function attachMask(input, format) {
986
+ const mask = createMask(format);
987
+ const handlers = createMaskHandlers(mask);
988
+ input.addEventListener("input", handlers.onInput);
989
+ input.addEventListener("keydown", handlers.onKeyDown);
990
+ input.addEventListener("paste", handlers.onPaste);
991
+ if (input.value) {
992
+ input.value = mask.apply(input.value);
993
+ }
994
+ return () => {
995
+ input.removeEventListener("input", handlers.onInput);
996
+ input.removeEventListener("keydown", handlers.onKeyDown);
997
+ input.removeEventListener("paste", handlers.onPaste);
998
+ };
999
+ }
1000
+ const closeSvg = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>';
1001
+ const GRID_ROWS = 6;
1002
+ function dayRow(ri, showWeekNumbers) {
1003
+ const rowClass = showWeekNumbers ? "rc-row rc-row--week-numbers" : "rc-row";
1004
+ const weekNumCell = showWeekNumbers ? `<div class="rc-week-number" x-text="mg.weekNumbers[${ri}]"></div>` : "";
1005
+ return `<div class="${rowClass}" x-show="mg.rows.length > ${ri}">
1006
+ ${weekNumCell}<template x-for="cell in (mg.rows[${ri}] || [])" :key="cell.date.toISO()">
1007
+ <div :class="dayClasses(cell)" :id="'day-' + cell.date.toISO()" :aria-selected="isSelected(cell.date)" :aria-disabled="cell.isDisabled" :title="dayTitle(cell)" role="option" tabindex="-1" @click="!cell.isDisabled && selectDate(cell.date)" @mouseenter="hoverDate = cell.date" @mouseleave="hoverDate = null" x-text="cell.date.day"></div>
1008
+ </template>
1009
+ </div>`;
1010
+ }
1011
+ function dayRows(showWeekNumbers) {
1012
+ return Array.from({ length: GRID_ROWS }, (_, ri) => dayRow(ri, showWeekNumbers)).join("\n ");
1013
+ }
1014
+ function yearPickerView() {
1015
+ return `<template x-if="view === 'years'">
1016
+ <div class="rc-view-enter">
1017
+ <div class="rc-header">
1018
+ <button class="rc-header__nav" @click="prev()" :disabled="!canGoPrev" aria-label="Previous decade">&#8249;</button>
1019
+ <span class="rc-header__label" x-text="decadeLabel"></span>
1020
+ <button class="rc-header__nav" @click="next()" :disabled="!canGoNext" aria-label="Next decade">&#8250;</button>
1021
+ </div>
1022
+ <div class="rc-year-grid" role="group" :aria-label="decadeLabel">
1023
+ <template x-for="cell in yearGrid.flat()" :key="cell.year">
1024
+ <div :class="yearClasses(cell)" :aria-disabled="cell.isDisabled" tabindex="-1" @click="!cell.isDisabled && selectYear(cell.year)" x-text="cell.label"></div>
1025
+ </template>
1026
+ </div>
1027
+ </div>
1028
+ </template>`;
1029
+ }
1030
+ function monthPickerView() {
1031
+ return `<template x-if="view === 'months'">
1032
+ <div class="rc-view-enter">
1033
+ <div class="rc-header">
1034
+ <button class="rc-header__nav" @click="prev()" :disabled="!canGoPrev" aria-label="Previous year">&#8249;</button>
1035
+ <button class="rc-header__label" @click="setView('years')" aria-label="Change view" x-text="yearLabel"></button>
1036
+ <button class="rc-header__nav" @click="next()" :disabled="!canGoNext" aria-label="Next year">&#8250;</button>
1037
+ </div>
1038
+ <div class="rc-month-grid" role="group" :aria-label="yearLabel">
1039
+ <template x-for="cell in monthGrid.flat()" :key="cell.month">
1040
+ <div :class="monthClasses(cell)" :aria-disabled="cell.isDisabled" tabindex="-1" @click="!cell.isDisabled && selectMonth(cell.month)" x-text="cell.label"></div>
1041
+ </template>
1042
+ </div>
1043
+ </div>
1044
+ </template>`;
1045
+ }
1046
+ function dayView(isDual, showWeekNumbers) {
1047
+ const prevStyle = isDual ? ` :style="gi > 0 ? 'visibility:hidden' : ''"` : "";
1048
+ const nextStyle = isDual ? ` :style="gi < grid.length - 1 ? 'visibility:hidden' : ''"` : "";
1049
+ const monthsClass = isDual ? ` :class="{ 'rc-months--dual': monthCount === 2 }"` : "";
1050
+ const gridClassBinding = `:class="{ 'rc-grid--slide-next': _navDirection === 'next', 'rc-grid--slide-prev': _navDirection === 'prev' }"`;
1051
+ const weekdayBlock = showWeekNumbers ? `<div class="rc-weekdays rc-weekdays--week-numbers">
1052
+ <span class="rc-weekday rc-week-label"></span>
1053
+ <template x-for="wd in weekdayHeaders" :key="wd">
1054
+ <span class="rc-weekday" x-text="wd"></span>
1055
+ </template>
1056
+ </div>` : `<div class="rc-weekdays">
1057
+ <template x-for="wd in weekdayHeaders" :key="wd">
1058
+ <span class="rc-weekday" x-text="wd"></span>
1059
+ </template>
1060
+ </div>`;
1061
+ const gridClass = showWeekNumbers ? '"rc-grid rc-grid--week-numbers"' : '"rc-grid"';
1062
+ const rows = dayRows(showWeekNumbers);
1063
+ return `<template x-if="view === 'days'">
1064
+ <div class="rc-months${isDual ? "" : " rc-view-enter"}"${monthsClass}${isDual ? "" : ""}>
1065
+ <template x-for="(mg, gi) in grid" :key="mg.year + '-' + mg.month">
1066
+ <div${isDual ? "" : ""}>
1067
+ <div class="rc-header">
1068
+ <button class="rc-header__nav" @click="prev()" :disabled="!canGoPrev" aria-label="Previous month"${prevStyle}>&#8249;</button>
1069
+ <button class="rc-header__label" @click="setView('months')" aria-label="Change view" x-text="monthYearLabel(gi)"></button>
1070
+ <button class="rc-header__nav" @click="next()" :disabled="!canGoNext" aria-label="Next month"${nextStyle}>&#8250;</button>
1071
+ </div>
1072
+ ${weekdayBlock}
1073
+ <div class="rc-grid-wrapper">
1074
+ <div class=${gridClass} ${gridClassBinding} @animationend="_navDirection = ''" role="listbox" :aria-label="monthYearLabel(gi)">
1075
+ ${rows}
1076
+ </div>
1077
+ </div>
1078
+ </div>
1079
+ </template>
1080
+ </div>
1081
+ </template>`;
1082
+ }
1083
+ function scrollableDayView(showWeekNumbers, scrollHeight) {
1084
+ const weekdayBlock = showWeekNumbers ? `<div class="rc-weekdays rc-weekdays--week-numbers">
1085
+ <span class="rc-weekday rc-week-label"></span>
1086
+ <template x-for="wd in weekdayHeaders" :key="wd">
1087
+ <span class="rc-weekday" x-text="wd"></span>
1088
+ </template>
1089
+ </div>` : `<div class="rc-weekdays">
1090
+ <template x-for="wd in weekdayHeaders" :key="wd">
1091
+ <span class="rc-weekday" x-text="wd"></span>
1092
+ </template>
1093
+ </div>`;
1094
+ const gridClass = showWeekNumbers ? '"rc-grid rc-grid--week-numbers"' : '"rc-grid"';
1095
+ const rows = dayRows(showWeekNumbers);
1096
+ return `<template x-if="view === 'days'">
1097
+ <div>
1098
+ <div class="rc-header rc-header--scroll-sticky">
1099
+ <span class="rc-header__label rc-header__label--scroll" x-text="scrollHeaderLabel"></span>
1100
+ </div>
1101
+ ${weekdayBlock}
1102
+ <div class="rc-months rc-months--scroll" style="max-height: ${scrollHeight}px">
1103
+ <template x-for="(mg, gi) in grid" :key="mg.year + '-' + mg.month">
1104
+ <div :data-month-id="'month-' + mg.year + '-' + mg.month">
1105
+ <div class="rc-header rc-header--scroll" x-show="gi > 0">
1106
+ <span class="rc-header__label rc-header__label--scroll" x-text="monthYearLabel(gi)"></span>
1107
+ </div>
1108
+ <div class=${gridClass} role="listbox" :aria-label="monthYearLabel(gi)">
1109
+ ${rows}
1110
+ </div>
1111
+ </div>
1112
+ </template>
1113
+ </div>
1114
+ </div>
1115
+ </template>`;
1116
+ }
1117
+ function wizardChrome() {
1118
+ return `<div class="rc-wizard-steps">
1119
+ <template x-for="step in wizardTotalSteps" :key="step">
1120
+ <div class="rc-wizard-step" :class="{ 'rc-wizard-step--active': wizardStep === step, 'rc-wizard-step--done': wizardStep > step }"></div>
1121
+ </template>
1122
+ </div>
1123
+ <div class="rc-wizard-label" x-text="wizardStepLabel"></div>
1124
+ <template x-if="wizardStep > 1">
1125
+ <button class="rc-wizard-back" @click="wizardBack()" aria-label="Go back">&#8249; Back</button>
1126
+ </template>`;
1127
+ }
1128
+ function wizardSummary() {
1129
+ return `<div class="rc-wizard-summary" x-show="wizardSummary" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform translate-y-1" x-transition:enter-end="opacity-100 transform translate-y-0" x-text="wizardSummary"></div>`;
1130
+ }
1131
+ function popupWrapper(content) {
1132
+ return `<div x-ref="popup" x-show="isOpen" :style="popupStyle" class="rc-popup-overlay" @click.self="close()" role="dialog" aria-modal="true" :aria-label="popupAriaLabel" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
1133
+ ${content}
1134
+ </div>`;
1135
+ }
1136
+ function popupHeader(isWizard) {
1137
+ const title = isWizard ? '<span class="rc-popup-header__title" x-text="wizardStepLabel"></span>' : '<span class="rc-popup-header__title">Select Date</span>';
1138
+ return `<div class="rc-popup-header">
1139
+ ${title}
1140
+ <button class="rc-popup-header__close" @click="close()" aria-label="Close calendar">${closeSvg}</button>
1141
+ </div>`;
1142
+ }
1143
+ function presetsBlock() {
1144
+ return `<div class="rc-presets" role="group" aria-label="Quick select">
1145
+ <template x-for="(preset, pi) in presets" :key="pi">
1146
+ <button class="rc-preset" @click="applyPreset(pi)" x-text="preset.label"></button>
1147
+ </template>
1148
+ </div>`;
1149
+ }
1150
+ function hiddenInputs() {
1151
+ return `<template x-if="inputName">
1152
+ <template x-for="val in hiddenInputValues" :key="val">
1153
+ <input type="hidden" :name="inputName" :value="val">
1154
+ </template>
1155
+ </template>`;
1156
+ }
1157
+ function generateCalendarTemplate(options) {
1158
+ const { display, isDualMonth, isWizard, hasName, showWeekNumbers, hasPresets, isScrollable, scrollHeight } = options;
1159
+ const isPopup = display === "popup";
1160
+ const calendarClass = isWizard ? "rc-calendar rc-calendar--wizard" : "rc-calendar";
1161
+ const ariaLabel = isWizard ? "Birth date wizard" : "Calendar";
1162
+ const parts = [];
1163
+ if (isPopup) {
1164
+ parts.push(popupHeader(isWizard));
1165
+ }
1166
+ if (isWizard) {
1167
+ parts.push(wizardChrome());
1168
+ }
1169
+ parts.push(yearPickerView());
1170
+ parts.push(monthPickerView());
1171
+ if (isScrollable) {
1172
+ parts.push(scrollableDayView(showWeekNumbers, scrollHeight));
1173
+ } else {
1174
+ parts.push(dayView(isDualMonth, showWeekNumbers));
1175
+ }
1176
+ if (hasPresets) {
1177
+ parts.push(presetsBlock());
1178
+ }
1179
+ if (isWizard) {
1180
+ parts.push(wizardSummary());
1181
+ }
1182
+ if (hasName) {
1183
+ parts.push(hiddenInputs());
1184
+ }
1185
+ const calendarInner = parts.join("\n");
1186
+ const calendarEl = `<div class="${calendarClass}" @keydown="handleKeydown($event)" tabindex="0" :aria-activedescendant="focusedDateISO ? 'day-' + focusedDateISO : null" role="application" aria-label="${ariaLabel}">
1187
+ <div class="rc-sr-only" role="status" aria-live="polite" aria-atomic="true" x-text="_statusMessage"></div>
1188
+ ${calendarInner}
1189
+ </div>`;
1190
+ if (isPopup) {
1191
+ return popupWrapper(calendarEl);
1192
+ }
1193
+ return calendarEl;
1194
+ }
1195
+ function alpine(self) {
1196
+ return self;
1197
+ }
1198
+ function parseISODate(s) {
1199
+ return CalendarDate.fromISO(s);
1200
+ }
1201
+ function parseISODates(arr) {
1202
+ return arr.map((s) => CalendarDate.fromISO(s)).filter((d) => d !== null);
1203
+ }
1204
+ function parseConfigRule(rule) {
1205
+ const from = rule.from ? parseISODate(rule.from) : null;
1206
+ const to = rule.to ? parseISODate(rule.to) : null;
1207
+ const hasDateRange = from !== null && to !== null;
1208
+ const hasMonths = Array.isArray(rule.months) && rule.months.length > 0;
1209
+ if (!hasDateRange && !hasMonths) return null;
1210
+ const result = {};
1211
+ if (hasDateRange) {
1212
+ result.from = from;
1213
+ result.to = to;
1214
+ }
1215
+ if (hasMonths) {
1216
+ result.months = rule.months;
1217
+ }
1218
+ if (rule.priority !== void 0) {
1219
+ result.priority = rule.priority;
1220
+ }
1221
+ if (rule.minDate) {
1222
+ const d = parseISODate(rule.minDate);
1223
+ if (d) result.minDate = d;
1224
+ }
1225
+ if (rule.maxDate) {
1226
+ const d = parseISODate(rule.maxDate);
1227
+ if (d) result.maxDate = d;
1228
+ }
1229
+ if (rule.disabledDates) {
1230
+ result.disabledDates = parseISODates(rule.disabledDates);
1231
+ }
1232
+ if (rule.disabledDaysOfWeek) {
1233
+ result.disabledDaysOfWeek = rule.disabledDaysOfWeek;
1234
+ }
1235
+ if (rule.enabledDates) {
1236
+ result.enabledDates = parseISODates(rule.enabledDates);
1237
+ }
1238
+ if (rule.enabledDaysOfWeek) {
1239
+ result.enabledDaysOfWeek = rule.enabledDaysOfWeek;
1240
+ }
1241
+ if (rule.disabledMonths) {
1242
+ result.disabledMonths = rule.disabledMonths;
1243
+ }
1244
+ if (rule.enabledMonths) {
1245
+ result.enabledMonths = rule.enabledMonths;
1246
+ }
1247
+ if (rule.disabledYears) {
1248
+ result.disabledYears = rule.disabledYears;
1249
+ }
1250
+ if (rule.enabledYears) {
1251
+ result.enabledYears = rule.enabledYears;
1252
+ }
1253
+ if (rule.minRange !== void 0) {
1254
+ result.minRange = rule.minRange;
1255
+ }
1256
+ if (rule.maxRange !== void 0) {
1257
+ result.maxRange = rule.maxRange;
1258
+ }
1259
+ return result;
1260
+ }
1261
+ function validateConfig(config) {
1262
+ const warn = (msg) => console.warn(`[reach-calendar] ${msg}`);
1263
+ if (config.months !== void 0 && (config.months < 1 || !Number.isInteger(config.months))) {
1264
+ warn(`months must be a positive integer, got: ${config.months}`);
1265
+ }
1266
+ if (config.wizard && config.months && config.months >= 3) {
1267
+ warn("months >= 3 (scrollable) is not compatible with wizard mode; using months: 1");
1268
+ }
1269
+ if (config.firstDay !== void 0 && (config.firstDay < 0 || config.firstDay > 6)) {
1270
+ warn(`firstDay must be 0-6, got: ${config.firstDay}`);
1271
+ }
1272
+ if (config.minDate && !CalendarDate.fromISO(config.minDate)) {
1273
+ warn(`invalid minDate: "${config.minDate}"`);
1274
+ }
1275
+ if (config.maxDate && !CalendarDate.fromISO(config.maxDate)) {
1276
+ warn(`invalid maxDate: "${config.maxDate}"`);
1277
+ }
1278
+ if (config.minDate && config.maxDate) {
1279
+ const min = CalendarDate.fromISO(config.minDate);
1280
+ const max = CalendarDate.fromISO(config.maxDate);
1281
+ if (min && max && min.isAfter(max)) {
1282
+ warn(`minDate "${config.minDate}" is after maxDate "${config.maxDate}"`);
1283
+ }
1284
+ }
1285
+ if (config.minRange !== void 0 && config.maxRange !== void 0 && config.minRange > config.maxRange) {
1286
+ warn(`minRange (${config.minRange}) exceeds maxRange (${config.maxRange})`);
1287
+ }
1288
+ if (config.wizard && config.mode && config.mode !== "single") {
1289
+ warn(`wizard mode is designed for single selection; mode "${config.mode}" may not work as expected`);
1290
+ }
1291
+ if (config.timezone) {
1292
+ try {
1293
+ Intl.DateTimeFormat(void 0, { timeZone: config.timezone });
1294
+ } catch {
1295
+ warn(`invalid timezone: "${config.timezone}"`);
1296
+ }
1297
+ }
1298
+ }
1299
+ function buildConstraints(cfg, messages) {
1300
+ const opts = {};
1301
+ if (cfg.minDate) {
1302
+ const d = CalendarDate.fromISO(cfg.minDate);
1303
+ if (d) opts.minDate = d;
1304
+ }
1305
+ if (cfg.maxDate) {
1306
+ const d = CalendarDate.fromISO(cfg.maxDate);
1307
+ if (d) opts.maxDate = d;
1308
+ }
1309
+ if (cfg.disabledDates) {
1310
+ opts.disabledDates = parseISODates(cfg.disabledDates);
1311
+ }
1312
+ if (cfg.disabledDaysOfWeek) {
1313
+ opts.disabledDaysOfWeek = cfg.disabledDaysOfWeek;
1314
+ }
1315
+ if (cfg.enabledDates) {
1316
+ opts.enabledDates = parseISODates(cfg.enabledDates);
1317
+ }
1318
+ if (cfg.enabledDaysOfWeek) {
1319
+ opts.enabledDaysOfWeek = cfg.enabledDaysOfWeek;
1320
+ }
1321
+ if (cfg.disabledMonths) {
1322
+ opts.disabledMonths = cfg.disabledMonths;
1323
+ }
1324
+ if (cfg.enabledMonths) {
1325
+ opts.enabledMonths = cfg.enabledMonths;
1326
+ }
1327
+ if (cfg.disabledYears) {
1328
+ opts.disabledYears = cfg.disabledYears;
1329
+ }
1330
+ if (cfg.enabledYears) {
1331
+ opts.enabledYears = cfg.enabledYears;
1332
+ }
1333
+ if (cfg.minRange !== void 0) {
1334
+ opts.minRange = cfg.minRange;
1335
+ }
1336
+ if (cfg.maxRange !== void 0) {
1337
+ opts.maxRange = cfg.maxRange;
1338
+ }
1339
+ if (cfg.rules) {
1340
+ const parsedRules = cfg.rules.map((r) => parseConfigRule(r)).filter((r) => r !== null);
1341
+ if (parsedRules.length > 0) {
1342
+ opts.rules = parsedRules;
1343
+ }
1344
+ }
1345
+ return {
1346
+ isDisabledDate: createDateConstraint(opts),
1347
+ isRangeValid: createRangeValidator(opts),
1348
+ isMonthDisabled: createMonthConstraint(opts),
1349
+ isYearDisabled: createYearConstraint(opts),
1350
+ getDisabledReasons: createDisabledReasons(opts, messages)
1351
+ };
1352
+ }
1353
+ const CONSTRAINT_KEYS = [
1354
+ "minDate",
1355
+ "maxDate",
1356
+ "disabledDates",
1357
+ "disabledDaysOfWeek",
1358
+ "enabledDates",
1359
+ "enabledDaysOfWeek",
1360
+ "disabledMonths",
1361
+ "enabledMonths",
1362
+ "disabledYears",
1363
+ "enabledYears",
1364
+ "minRange",
1365
+ "maxRange",
1366
+ "rules"
1367
+ ];
1368
+ function extractConstraintConfig(cfg) {
1369
+ const result = {};
1370
+ for (const key of CONSTRAINT_KEYS) {
1371
+ if (cfg[key] !== void 0) {
1372
+ result[key] = cfg[key];
1373
+ }
1374
+ }
1375
+ return result;
1376
+ }
1377
+ function createCalendarData(config = {}, Alpine) {
1378
+ validateConfig(config);
1379
+ const mode = config.mode ?? "single";
1380
+ const display = config.display ?? "inline";
1381
+ const format = config.format ?? "DD/MM/YYYY";
1382
+ const firstDay = config.firstDay ?? 0;
1383
+ let timezone = config.timezone;
1384
+ if (timezone) {
1385
+ try {
1386
+ Intl.DateTimeFormat(void 0, { timeZone: timezone });
1387
+ } catch {
1388
+ timezone = void 0;
1389
+ }
1390
+ }
1391
+ const wizardConfig = config.wizard ?? false;
1392
+ const rawMonthCount = config.months ?? 1;
1393
+ const monthCount = wizardConfig && rawMonthCount >= 3 ? 1 : rawMonthCount;
1394
+ const isScrollable = monthCount >= 3;
1395
+ const scrollHeight = config.scrollHeight ?? 400;
1396
+ const wizard = !!wizardConfig;
1397
+ const wizardMode = wizardConfig === true ? "full" : wizardConfig === false ? "none" : wizardConfig;
1398
+ const wizardTotalSteps = wizardMode === "none" ? 0 : wizardMode === "full" ? 3 : 2;
1399
+ const wizardStartView = wizardMode === "full" || wizardMode === "year-month" ? "years" : wizardMode === "month-day" ? "months" : "days";
1400
+ const useMask = config.mask ?? true;
1401
+ const showWeekNumbers = config.showWeekNumbers ?? false;
1402
+ const presets = config.presets ?? [];
1403
+ const inputName = config.name ?? "";
1404
+ const inputId = config.inputId ?? null;
1405
+ const inputRef = config.inputRef ?? "rc-input";
1406
+ const locale = config.locale;
1407
+ const closeOnSelect = config.closeOnSelect ?? true;
1408
+ const beforeSelectCb = config.beforeSelect ?? null;
1409
+ const constraintMessages = config.constraintMessages;
1410
+ const constraints = buildConstraints(config, constraintMessages);
1411
+ function buildSelection() {
1412
+ if (mode === "multiple") return new MultipleSelection();
1413
+ if (mode === "range") return new RangeSelection();
1414
+ return new SingleSelection();
1415
+ }
1416
+ const selection = buildSelection();
1417
+ if (config.value) {
1418
+ if (mode === "single") {
1419
+ const d = parseDate(config.value, format) ?? CalendarDate.fromISO(config.value);
1420
+ if (d && !constraints.isDisabledDate(d)) selection.toggle(d);
1421
+ } else if (mode === "range") {
1422
+ const range = parseDateRange(config.value, format);
1423
+ if (range) {
1424
+ let [start, end] = range;
1425
+ if (end.isBefore(start)) {
1426
+ const tmp = start;
1427
+ start = end;
1428
+ end = tmp;
1429
+ }
1430
+ if (!constraints.isDisabledDate(start) && !constraints.isDisabledDate(end) && constraints.isRangeValid(start, end)) {
1431
+ selection.toggle(start);
1432
+ selection.toggle(end);
1433
+ }
1434
+ }
1435
+ } else if (mode === "multiple") {
1436
+ const dates = parseDateMultiple(config.value, format);
1437
+ for (const d of dates) {
1438
+ if (!constraints.isDisabledDate(d)) selection.toggle(d);
1439
+ }
1440
+ }
1441
+ }
1442
+ const today = CalendarDate.today(timezone);
1443
+ const initialDates = selection.toArray();
1444
+ const defaultViewDate = initialDates.length > 0 ? initialDates[0] : today;
1445
+ const viewDate = wizardMode === "full" || wizardMode === "year-month" ? new CalendarDate(today.year - 30, today.month, today.day) : defaultViewDate;
1446
+ function computeFormattedValue(sel) {
1447
+ const dates = sel.toArray();
1448
+ if (dates.length === 0) return "";
1449
+ const first = dates[0];
1450
+ if (mode === "range" && dates.length === 2) {
1451
+ return formatRange(first, dates[1], format);
1452
+ }
1453
+ if (mode === "multiple") {
1454
+ return formatMultiple(dates, format);
1455
+ }
1456
+ return formatDate(first, format);
1457
+ }
1458
+ return {
1459
+ // --- Config (exposed to templates) ---
1460
+ mode,
1461
+ display,
1462
+ format,
1463
+ monthCount,
1464
+ firstDay,
1465
+ wizard,
1466
+ wizardMode,
1467
+ wizardTotalSteps,
1468
+ showWeekNumbers,
1469
+ presets,
1470
+ inputName,
1471
+ // --- Reactive state ---
1472
+ month: viewDate.month,
1473
+ year: viewDate.year,
1474
+ view: wizard ? wizardStartView : "days",
1475
+ isOpen: display === "inline",
1476
+ grid: [],
1477
+ monthGrid: [],
1478
+ yearGrid: [],
1479
+ inputValue: computeFormattedValue(selection),
1480
+ popupStyle: display === "popup" ? "position:fixed;inset:0;z-index:50;" : "",
1481
+ focusedDate: null,
1482
+ hoverDate: null,
1483
+ wizardStep: wizard ? 1 : 0,
1484
+ _wizardYear: null,
1485
+ _wizardMonth: null,
1486
+ _wizardDay: null,
1487
+ // --- Internal state ---
1488
+ _navDirection: "",
1489
+ _selection: selection,
1490
+ _today: today,
1491
+ _constraintConfig: extractConstraintConfig(config),
1492
+ _isDisabledDate: constraints.isDisabledDate,
1493
+ _isRangeValid: constraints.isRangeValid,
1494
+ _isMonthDisabled: constraints.isMonthDisabled,
1495
+ _isYearDisabled: constraints.isYearDisabled,
1496
+ _getDisabledReasons: constraints.getDisabledReasons,
1497
+ _inputEl: null,
1498
+ _detachInput: null,
1499
+ _syncing: false,
1500
+ _suppressFocusOpen: false,
1501
+ _Alpine: Alpine ?? null,
1502
+ _autoRendered: false,
1503
+ // Scrollable multi-month state
1504
+ isScrollable,
1505
+ _scrollHeight: scrollHeight,
1506
+ _scrollContainerEl: null,
1507
+ _scrollObserver: null,
1508
+ /** Index into grid[] of the month currently visible at top of scroll viewport. */
1509
+ _scrollVisibleIndex: 0,
1510
+ /**
1511
+ * Reactive counter bumped on selection changes. Read by dayClasses() so Alpine
1512
+ * re-evaluates class bindings without needing a full grid rebuild.
1513
+ */
1514
+ _selectionRev: 0,
1515
+ // --- Accessibility: live region ---
1516
+ _statusMessage: "",
1517
+ // --- Getters ---
1518
+ get selectedDates() {
1519
+ return this._selection.toArray();
1520
+ },
1521
+ get formattedValue() {
1522
+ return computeFormattedValue(this._selection);
1523
+ },
1524
+ /** ISO string values for hidden form inputs. */
1525
+ get hiddenInputValues() {
1526
+ return this._selection.toArray().map((d) => d.toISO());
1527
+ },
1528
+ /** ISO string of focused date for aria-activedescendant binding. */
1529
+ get focusedDateISO() {
1530
+ return this.focusedDate ? this.focusedDate.toISO() : "";
1531
+ },
1532
+ /** ID for the popup input element (for external label association). */
1533
+ get inputId() {
1534
+ return inputId;
1535
+ },
1536
+ /** Accessible label for the popup input element. */
1537
+ get inputAriaLabel() {
1538
+ if (mode === "range") return "Select date range";
1539
+ if (mode === "multiple") return "Select dates";
1540
+ if (wizard) return "Select birth date";
1541
+ return "Select date";
1542
+ },
1543
+ /** Accessible label for the popup dialog. */
1544
+ get popupAriaLabel() {
1545
+ if (wizard) return "Birth date wizard";
1546
+ if (mode === "range") return "Date range picker";
1547
+ return "Date picker";
1548
+ },
1549
+ /** Label for the current wizard step (e.g. "Select Year"). */
1550
+ get wizardStepLabel() {
1551
+ if (!this.wizard) return "";
1552
+ if (wizardMode === "full") {
1553
+ if (this.wizardStep === 1) return "Select Year";
1554
+ if (this.wizardStep === 2) return "Select Month";
1555
+ return "Select Day";
1556
+ }
1557
+ if (wizardMode === "year-month") {
1558
+ if (this.wizardStep === 1) return "Select Year";
1559
+ return "Select Month";
1560
+ }
1561
+ if (wizardMode === "month-day") {
1562
+ if (this.wizardStep === 1) return "Select Month";
1563
+ return "Select Day";
1564
+ }
1565
+ return "";
1566
+ },
1567
+ /** Summary of wizard selections so far (e.g. "1995 · June · 15"). */
1568
+ get wizardSummary() {
1569
+ if (!wizard) return "";
1570
+ const parts = [];
1571
+ if (this._wizardYear !== null) parts.push(String(this._wizardYear));
1572
+ if (this._wizardMonth !== null) {
1573
+ const d = new CalendarDate(this._wizardYear ?? this.year, this._wizardMonth, 1);
1574
+ parts.push(d.format({ month: "long" }, locale));
1575
+ }
1576
+ if (this._wizardDay !== null) parts.push(String(this._wizardDay));
1577
+ return parts.join(" · ");
1578
+ },
1579
+ /**
1580
+ * Localized short weekday headers in the correct order for the current `firstDay`.
1581
+ * Returns an array of 7 short names (e.g., ["Mo", "Tu", "We", ...]).
1582
+ */
1583
+ get weekdayHeaders() {
1584
+ const headers = [];
1585
+ const refSunday = new Date(2026, 0, 4);
1586
+ for (let i = 0; i < 7; i++) {
1587
+ const dayIndex = (this.firstDay + i) % 7;
1588
+ const d = new Date(refSunday);
1589
+ d.setDate(refSunday.getDate() + dayIndex);
1590
+ headers.push(
1591
+ new Intl.DateTimeFormat(locale, { weekday: "short" }).format(d)
1592
+ );
1593
+ }
1594
+ return headers;
1595
+ },
1596
+ // --- Lifecycle ---
1597
+ init() {
1598
+ var _a;
1599
+ const el = alpine(this).$el;
1600
+ const hasCalendar = el.querySelector(".rc-calendar") !== null;
1601
+ if (!hasCalendar && config.template !== false && ((_a = this._Alpine) == null ? void 0 : _a.initTree)) {
1602
+ const fragment = document.createRange().createContextualFragment(
1603
+ generateCalendarTemplate({
1604
+ display: this.display,
1605
+ isDualMonth: this.monthCount === 2,
1606
+ isWizard: this.wizardMode !== "none",
1607
+ hasName: !!this.inputName,
1608
+ showWeekNumbers: this.showWeekNumbers,
1609
+ hasPresets: this.presets.length > 0,
1610
+ isScrollable: this.isScrollable,
1611
+ scrollHeight: this._scrollHeight
1612
+ })
1613
+ );
1614
+ const newElements = Array.from(fragment.children).filter(
1615
+ (c) => c instanceof HTMLElement
1616
+ );
1617
+ el.appendChild(fragment);
1618
+ for (const child of newElements) {
1619
+ this._Alpine.initTree(child);
1620
+ }
1621
+ this._autoRendered = true;
1622
+ }
1623
+ this._rebuildGrid();
1624
+ this._rebuildMonthGrid();
1625
+ this._rebuildYearGrid();
1626
+ alpine(this).$watch("month", () => {
1627
+ this._rebuildGrid();
1628
+ this._emitNavigate();
1629
+ this._announceNavigation();
1630
+ if (this.isScrollable) {
1631
+ alpine(this).$nextTick(() => {
1632
+ this._rebindScrollObserver();
1633
+ });
1634
+ }
1635
+ });
1636
+ alpine(this).$watch("year", () => {
1637
+ this._rebuildGrid();
1638
+ this._rebuildMonthGrid();
1639
+ this._rebuildYearGrid();
1640
+ this._emitNavigate();
1641
+ this._announceNavigation();
1642
+ if (this.isScrollable) {
1643
+ alpine(this).$nextTick(() => {
1644
+ this._rebindScrollObserver();
1645
+ });
1646
+ }
1647
+ });
1648
+ alpine(this).$watch("view", () => {
1649
+ this._emitViewChange();
1650
+ this._announceViewChange();
1651
+ });
1652
+ alpine(this).$nextTick(() => {
1653
+ const refs = alpine(this).$refs;
1654
+ if (refs && refs[inputRef] && refs[inputRef] instanceof HTMLInputElement) {
1655
+ this.bindInput(refs[inputRef]);
1656
+ } else if (this.display === "popup") {
1657
+ console.warn(
1658
+ `[reach-calendar] Popup mode requires an <input x-ref="${inputRef}"> inside the component container.`
1659
+ );
1660
+ }
1661
+ if (this.isScrollable) {
1662
+ this._initScrollListener();
1663
+ }
1664
+ });
1665
+ },
1666
+ destroy() {
1667
+ var _a, _b;
1668
+ if (this._scrollObserver) {
1669
+ this._scrollObserver.disconnect();
1670
+ this._scrollObserver = null;
1671
+ }
1672
+ this._scrollContainerEl = null;
1673
+ if (this._detachInput) {
1674
+ this._detachInput();
1675
+ this._detachInput = null;
1676
+ }
1677
+ this._inputEl = null;
1678
+ if (this._autoRendered) {
1679
+ const el = alpine(this).$el;
1680
+ (_a = el.querySelector(".rc-popup-overlay")) == null ? void 0 : _a.remove();
1681
+ (_b = el.querySelector(".rc-calendar")) == null ? void 0 : _b.remove();
1682
+ this._autoRendered = false;
1683
+ }
1684
+ },
1685
+ // --- Grid ---
1686
+ _rebuildGrid() {
1687
+ this.grid = generateMonths(
1688
+ this.year,
1689
+ this.month,
1690
+ this.monthCount,
1691
+ this.firstDay,
1692
+ this._today,
1693
+ this._isDisabledDate
1694
+ );
1695
+ },
1696
+ /** Rebuild the 3×4 month picker grid for the current year. */
1697
+ _rebuildMonthGrid() {
1698
+ this.monthGrid = generateMonthGrid(this.year, this._today, locale, this._isMonthDisabled);
1699
+ },
1700
+ /** Rebuild the 3×4 year picker grid for the current year's 12-year block. */
1701
+ _rebuildYearGrid() {
1702
+ this.yearGrid = generateYearGrid(this.year, this._today, this._isYearDisabled);
1703
+ },
1704
+ /**
1705
+ * Build a flat array of grid items for template rendering, interleaving week number
1706
+ * markers with day cells. Used by the auto-rendered template when `showWeekNumbers` is true.
1707
+ */
1708
+ dayGridItems(mg) {
1709
+ const items = [];
1710
+ for (let ri = 0; ri < mg.rows.length; ri++) {
1711
+ const row = mg.rows[ri];
1712
+ if (!row || row.length === 0) continue;
1713
+ const firstCell = row[0];
1714
+ items.push({
1715
+ isWeekNumber: true,
1716
+ weekNumber: mg.weekNumbers[ri] ?? 0,
1717
+ // Placeholder cell — template guards prevent access when isWeekNumber is true
1718
+ cell: firstCell,
1719
+ key: `wn-${ri}`
1720
+ });
1721
+ for (const cell of row) {
1722
+ items.push({
1723
+ isWeekNumber: false,
1724
+ weekNumber: 0,
1725
+ cell,
1726
+ key: cell.date.toISO()
1727
+ });
1728
+ }
1729
+ }
1730
+ return items;
1731
+ },
1732
+ /** Year label for the month view header (e.g. "2026"). */
1733
+ get yearLabel() {
1734
+ return String(this.year);
1735
+ },
1736
+ /** Decade range label for the year view header (e.g. "2016 – 2027"). */
1737
+ get decadeLabel() {
1738
+ const startYear = Math.floor(this.year / 12) * 12;
1739
+ return `${startYear} – ${startYear + 11}`;
1740
+ },
1741
+ /**
1742
+ * Whether backward navigation is possible from the current position.
1743
+ *
1744
+ * - Days view: checks if the previous month has any selectable dates.
1745
+ * - Months view: checks if the previous year has any selectable months.
1746
+ * - Years view: checks if the previous 12-year block has any selectable years.
1747
+ *
1748
+ * Usage in Alpine template:
1749
+ * ```html
1750
+ * <button @click="prev()" :disabled="!canGoPrev">←</button>
1751
+ * ```
1752
+ */
1753
+ get canGoPrev() {
1754
+ if (this.view === "days") {
1755
+ if (this.isScrollable) return false;
1756
+ const d = new CalendarDate(this.year, this.month, 1).addMonths(-1);
1757
+ return !this._isMonthDisabled(d.year, d.month);
1758
+ }
1759
+ if (this.view === "months") {
1760
+ return !this._isYearDisabled(this.year - 1);
1761
+ }
1762
+ if (this.view === "years") {
1763
+ const blockStart = Math.floor(this.year / 12) * 12;
1764
+ for (let y = blockStart - 12; y < blockStart; y++) {
1765
+ if (!this._isYearDisabled(y)) return true;
1766
+ }
1767
+ return false;
1768
+ }
1769
+ return true;
1770
+ },
1771
+ /**
1772
+ * Whether forward navigation is possible from the current position.
1773
+ *
1774
+ * - Days view: checks if the next month has any selectable dates.
1775
+ * - Months view: checks if the next year has any selectable months.
1776
+ * - Years view: checks if the next 12-year block has any selectable years.
1777
+ *
1778
+ * Usage in Alpine template:
1779
+ * ```html
1780
+ * <button @click="next()" :disabled="!canGoNext">→</button>
1781
+ * ```
1782
+ */
1783
+ get canGoNext() {
1784
+ if (this.view === "days") {
1785
+ if (this.isScrollable) return false;
1786
+ const d = new CalendarDate(this.year, this.month, 1).addMonths(1);
1787
+ return !this._isMonthDisabled(d.year, d.month);
1788
+ }
1789
+ if (this.view === "months") {
1790
+ return !this._isYearDisabled(this.year + 1);
1791
+ }
1792
+ if (this.view === "years") {
1793
+ const blockStart = Math.floor(this.year / 12) * 12;
1794
+ for (let y = blockStart + 12; y < blockStart + 24; y++) {
1795
+ if (!this._isYearDisabled(y)) return true;
1796
+ }
1797
+ return false;
1798
+ }
1799
+ return true;
1800
+ },
1801
+ /**
1802
+ * Compute CSS class object for a year cell in the year picker view.
1803
+ */
1804
+ yearClasses(cell) {
1805
+ const selected = this.year === cell.year && this.view === "months";
1806
+ return {
1807
+ "rc-year": true,
1808
+ "rc-year--current": cell.isCurrentYear,
1809
+ "rc-year--selected": selected,
1810
+ "rc-year--disabled": cell.isDisabled
1811
+ };
1812
+ },
1813
+ /**
1814
+ * Compute CSS class object for a month cell in the month picker view.
1815
+ */
1816
+ monthClasses(cell) {
1817
+ const selected = this.month === cell.month && this.view === "days";
1818
+ return {
1819
+ "rc-month": true,
1820
+ "rc-month--current": cell.isCurrentMonth,
1821
+ "rc-month--selected": selected,
1822
+ "rc-month--disabled": cell.isDisabled
1823
+ };
1824
+ },
1825
+ /**
1826
+ * Get a localized "Month Year" label for a specific month grid.
1827
+ * @param gridIndex - Index into the `grid` array (0 for first month, 1 for second).
1828
+ */
1829
+ monthYearLabel(gridIndex) {
1830
+ const g = this.grid[gridIndex];
1831
+ if (!g) return "";
1832
+ const d = new CalendarDate(g.year, g.month, 1);
1833
+ return d.format({ month: "long", year: "numeric" }, locale);
1834
+ },
1835
+ /**
1836
+ * Compute CSS class object for a day cell.
1837
+ * Returns an object keyed by class name with boolean values, suitable for Alpine `:class`.
1838
+ */
1839
+ dayClasses(cell) {
1840
+ void this._selectionRev;
1841
+ const d = cell.date;
1842
+ const selected = this.isSelected(d);
1843
+ const rangeStart = this.isRangeStart(d);
1844
+ const rangeEnd = this.isRangeEnd(d);
1845
+ const inRange = this.isInRange(d, this.hoverDate ?? void 0);
1846
+ const isOtherMonth = !cell.isCurrentMonth;
1847
+ let rangeInvalid = false;
1848
+ if (mode === "range" && this.hoverDate !== null && !cell.isDisabled && !selected && this._selection.isPartial()) {
1849
+ rangeInvalid = !this.isDateSelectableForRange(d);
1850
+ }
1851
+ return {
1852
+ "rc-day": true,
1853
+ "rc-day--today": cell.isToday,
1854
+ "rc-day--selected": selected,
1855
+ "rc-day--range-start": rangeStart,
1856
+ "rc-day--range-end": rangeEnd,
1857
+ "rc-day--in-range": inRange && !rangeStart && !rangeEnd,
1858
+ "rc-day--disabled": cell.isDisabled,
1859
+ "rc-day--other-month": isOtherMonth,
1860
+ "rc-day--hidden": isOtherMonth && this.monthCount > 1,
1861
+ "rc-day--focused": this.focusedDate !== null && this.focusedDate.isSame(d),
1862
+ "rc-day--range-invalid": rangeInvalid
1863
+ };
1864
+ },
1865
+ /**
1866
+ * Get tooltip text for a day cell explaining why it is disabled.
1867
+ * Returns an empty string for enabled dates.
1868
+ */
1869
+ dayTitle(cell) {
1870
+ if (!cell.isDisabled) return "";
1871
+ return this._getDisabledReasons(cell.date).join("; ");
1872
+ },
1873
+ // --- Input binding ---
1874
+ /**
1875
+ * Bind an input element to the calendar.
1876
+ *
1877
+ * Sets up input masking (if enabled), syncs the initial value, and
1878
+ * attaches an input listener to keep `inputValue` in sync.
1879
+ *
1880
+ * Usage:
1881
+ * ```html
1882
+ * <!-- Alpine component root with calendar() -->
1883
+ * <input x-ref="rc-input" type="text">
1884
+ * ```
1885
+ * The component auto-binds to the ref named by `config.inputRef` (default: `rc-input`) during init().
1886
+ * For custom refs, set `inputRef` in config or call `bindInput($refs.myInput)` explicitly.
1887
+ */
1888
+ bindInput(el) {
1889
+ if (this._detachInput) {
1890
+ this._detachInput();
1891
+ this._detachInput = null;
1892
+ }
1893
+ this._inputEl = el;
1894
+ el.value = this.inputValue;
1895
+ if (useMask) {
1896
+ this._detachInput = attachMask(el, format);
1897
+ this.inputValue = el.value;
1898
+ }
1899
+ const syncHandler = () => {
1900
+ if (!this._syncing) {
1901
+ this.inputValue = el.value;
1902
+ }
1903
+ };
1904
+ el.addEventListener("input", syncHandler);
1905
+ const focusHandler = () => this.handleFocus();
1906
+ const blurHandler = () => this.handleBlur();
1907
+ el.addEventListener("focus", focusHandler);
1908
+ el.addEventListener("blur", blurHandler);
1909
+ if (display === "popup") {
1910
+ el.setAttribute("role", "combobox");
1911
+ el.setAttribute("aria-haspopup", "dialog");
1912
+ el.setAttribute("aria-expanded", String(this.isOpen));
1913
+ el.setAttribute("autocomplete", "off");
1914
+ if (inputId && !el.id) {
1915
+ el.id = inputId;
1916
+ }
1917
+ if (!el.getAttribute("aria-label")) {
1918
+ el.setAttribute("aria-label", this.inputAriaLabel);
1919
+ }
1920
+ }
1921
+ const prevDetach = this._detachInput;
1922
+ this._detachInput = () => {
1923
+ prevDetach == null ? void 0 : prevDetach();
1924
+ el.removeEventListener("input", syncHandler);
1925
+ el.removeEventListener("focus", focusHandler);
1926
+ el.removeEventListener("blur", blurHandler);
1927
+ if (display === "popup") {
1928
+ el.removeAttribute("role");
1929
+ el.removeAttribute("aria-haspopup");
1930
+ el.removeAttribute("aria-expanded");
1931
+ el.removeAttribute("autocomplete");
1932
+ }
1933
+ };
1934
+ if (!el.placeholder) {
1935
+ el.placeholder = format.toLowerCase();
1936
+ }
1937
+ },
1938
+ /**
1939
+ * Handle input event for unbound inputs (using `:value` + `@input`).
1940
+ * When using `bindInput()`, this is handled automatically.
1941
+ */
1942
+ handleInput(e) {
1943
+ const el = e.target;
1944
+ this.inputValue = el.value;
1945
+ },
1946
+ /**
1947
+ * Handle focus on the input element.
1948
+ * Opens the calendar popup in popup display mode.
1949
+ */
1950
+ handleFocus() {
1951
+ if (this._suppressFocusOpen) {
1952
+ this._suppressFocusOpen = false;
1953
+ return;
1954
+ }
1955
+ this.open();
1956
+ },
1957
+ /**
1958
+ * Handle blur on the input element.
1959
+ * Parses the typed value, updates selection if valid, and reformats the input.
1960
+ */
1961
+ handleBlur() {
1962
+ const value = this._inputEl ? this._inputEl.value : this.inputValue;
1963
+ if (!value.trim()) {
1964
+ if (this._selection.toArray().length > 0) {
1965
+ this._selection.clear();
1966
+ this._selectionRev++;
1967
+ this._emitChange();
1968
+ }
1969
+ this._syncInputFromSelection();
1970
+ return;
1971
+ }
1972
+ let changed = false;
1973
+ if (mode === "single") {
1974
+ const parsed = parseDate(value, format) ?? CalendarDate.fromISO(value);
1975
+ if (parsed && !this._isDisabledDate(parsed)) {
1976
+ this._selection.clear();
1977
+ this._selection.toggle(parsed);
1978
+ this.month = parsed.month;
1979
+ this.year = parsed.year;
1980
+ changed = true;
1981
+ }
1982
+ } else if (mode === "range") {
1983
+ const range = parseDateRange(value, format);
1984
+ if (range) {
1985
+ let [start, end] = range;
1986
+ if (end.isBefore(start)) {
1987
+ const tmp = start;
1988
+ start = end;
1989
+ end = tmp;
1990
+ }
1991
+ if (!this._isDisabledDate(start) && !this._isDisabledDate(end) && this._isRangeValid(start, end)) {
1992
+ this._selection.clear();
1993
+ this._selection.toggle(start);
1994
+ this._selection.toggle(end);
1995
+ this.month = start.month;
1996
+ this.year = start.year;
1997
+ changed = true;
1998
+ }
1999
+ }
2000
+ } else if (mode === "multiple") {
2001
+ const dates = parseDateMultiple(value, format);
2002
+ const valid = dates.filter((d) => !this._isDisabledDate(d));
2003
+ if (valid.length > 0) {
2004
+ this._selection.clear();
2005
+ for (const d of valid) {
2006
+ this._selection.toggle(d);
2007
+ }
2008
+ const first = valid[0];
2009
+ this.month = first.month;
2010
+ this.year = first.year;
2011
+ changed = true;
2012
+ }
2013
+ }
2014
+ if (changed) {
2015
+ this._selectionRev++;
2016
+ this._emitChange();
2017
+ }
2018
+ this._syncInputFromSelection();
2019
+ },
2020
+ // --- Internal: input sync ---
2021
+ /** Update inputValue and bound input element from current selection. */
2022
+ _syncInputFromSelection() {
2023
+ this._syncing = true;
2024
+ this.inputValue = this.formattedValue;
2025
+ if (this._inputEl) {
2026
+ this._inputEl.value = this.inputValue;
2027
+ }
2028
+ this._syncing = false;
2029
+ },
2030
+ /** Announce a message to screen readers via the aria-live status region. */
2031
+ _announce(message) {
2032
+ this._statusMessage = "";
2033
+ alpine(this).$nextTick(() => {
2034
+ this._statusMessage = message;
2035
+ });
2036
+ },
2037
+ /** Announce the current navigation context (month/year/decade) for screen readers. */
2038
+ _announceNavigation() {
2039
+ if (this.view === "days") {
2040
+ if (this.monthCount > 1 && !this.isScrollable) {
2041
+ const labels = [];
2042
+ for (let i = 0; i < this.grid.length; i++) {
2043
+ labels.push(this.monthYearLabel(i));
2044
+ }
2045
+ this._announce(labels.join(" – "));
2046
+ } else {
2047
+ this._announce(this.monthYearLabel(0));
2048
+ }
2049
+ } else if (this.view === "months") {
2050
+ this._announce("Year: " + this.year);
2051
+ } else if (this.view === "years") {
2052
+ this._announce("Decade: " + this.decadeLabel);
2053
+ }
2054
+ },
2055
+ /** Announce the new view after a view switch for screen readers. */
2056
+ _announceViewChange() {
2057
+ if (this.view === "days") {
2058
+ this._announce(this.monthYearLabel(0));
2059
+ } else if (this.view === "months") {
2060
+ this._announce("Select month, " + this.year);
2061
+ } else if (this.view === "years") {
2062
+ this._announce("Select year, " + this.decadeLabel);
2063
+ }
2064
+ },
2065
+ /** Dispatch calendar:change event with current selection state. */
2066
+ _emitChange() {
2067
+ alpine(this).$dispatch("calendar:change", {
2068
+ value: this._selection.toValue(),
2069
+ dates: this._selection.toArray().map((d) => d.toISO()),
2070
+ formatted: this.formattedValue
2071
+ });
2072
+ },
2073
+ /** Dispatch calendar:navigate event on month/year change. */
2074
+ _emitNavigate() {
2075
+ alpine(this).$dispatch("calendar:navigate", {
2076
+ year: this.year,
2077
+ month: this.month,
2078
+ view: this.view
2079
+ });
2080
+ },
2081
+ /** Dispatch calendar:view-change event on view switch. */
2082
+ _emitViewChange() {
2083
+ alpine(this).$dispatch("calendar:view-change", {
2084
+ view: this.view,
2085
+ year: this.year,
2086
+ month: this.month
2087
+ });
2088
+ },
2089
+ // --- Navigation ---
2090
+ prev() {
2091
+ this._navDirection = "prev";
2092
+ if (this.view === "days") {
2093
+ if (this.isScrollable) return;
2094
+ const d = new CalendarDate(this.year, this.month, 1).addMonths(-1);
2095
+ this.month = d.month;
2096
+ this.year = d.year;
2097
+ } else if (this.view === "months") {
2098
+ this.year--;
2099
+ } else if (this.view === "years") {
2100
+ this.year -= 12;
2101
+ }
2102
+ },
2103
+ next() {
2104
+ this._navDirection = "next";
2105
+ if (this.view === "days") {
2106
+ if (this.isScrollable) return;
2107
+ const d = new CalendarDate(this.year, this.month, 1).addMonths(1);
2108
+ this.month = d.month;
2109
+ this.year = d.year;
2110
+ } else if (this.view === "months") {
2111
+ this.year++;
2112
+ } else if (this.view === "years") {
2113
+ this.year += 12;
2114
+ }
2115
+ },
2116
+ goToToday() {
2117
+ this.month = this._today.month;
2118
+ this.year = this._today.year;
2119
+ this.view = "days";
2120
+ },
2121
+ // --- Selection ---
2122
+ selectDate(dateOrISO) {
2123
+ const date = typeof dateOrISO === "string" ? CalendarDate.fromISO(dateOrISO) : dateOrISO;
2124
+ if (!date) return;
2125
+ if (this._isDisabledDate(date)) return;
2126
+ if (mode === "range") {
2127
+ const range = this._selection;
2128
+ const rangeStart = range.getStart();
2129
+ if (range.isPartial() && rangeStart && !date.isSame(rangeStart)) {
2130
+ let start = rangeStart;
2131
+ let end = date;
2132
+ if (end.isBefore(start)) {
2133
+ const tmp = start;
2134
+ start = end;
2135
+ end = tmp;
2136
+ }
2137
+ if (!this._isRangeValid(start, end)) return;
2138
+ }
2139
+ }
2140
+ if (beforeSelectCb) {
2141
+ const action = this._selection.isSelected(date) ? "deselect" : "select";
2142
+ const result = beforeSelectCb(date, {
2143
+ mode,
2144
+ selectedDates: this._selection.toArray(),
2145
+ action
2146
+ });
2147
+ if (result === false) return;
2148
+ }
2149
+ const wasSelected = this._selection.isSelected(date);
2150
+ const wasPartialRange = mode === "range" && this._selection.isPartial();
2151
+ this._selection.toggle(date);
2152
+ if (wizard) this._wizardDay = date.day;
2153
+ this._selectionRev++;
2154
+ this._emitChange();
2155
+ this._syncInputFromSelection();
2156
+ const dateLabel = date.format({ weekday: "long", month: "long", day: "numeric", year: "numeric" }, locale);
2157
+ if (mode === "single") {
2158
+ this._announce(dateLabel + " selected");
2159
+ } else if (mode === "multiple") {
2160
+ const count = this._selection.toArray().length;
2161
+ if (wasSelected) {
2162
+ this._announce(dateLabel + " deselected. " + count + " dates selected");
2163
+ } else {
2164
+ this._announce(dateLabel + " selected. " + count + " dates selected");
2165
+ }
2166
+ } else if (mode === "range") {
2167
+ const range = this._selection;
2168
+ if (range.isPartial()) {
2169
+ this._announce("Range start: " + dateLabel + ". Select end date");
2170
+ } else if (wasPartialRange) {
2171
+ const dates = range.toArray();
2172
+ if (dates.length === 2) {
2173
+ const startLabel = dates[0].format({ weekday: "long", month: "long", day: "numeric", year: "numeric" }, locale);
2174
+ const endLabel = dates[1].format({ weekday: "long", month: "long", day: "numeric", year: "numeric" }, locale);
2175
+ this._announce("Range: " + startLabel + " to " + endLabel);
2176
+ }
2177
+ }
2178
+ }
2179
+ if (closeOnSelect && display === "popup" && this.isOpen) {
2180
+ const isComplete = mode === "single" || mode === "range" && !this._selection.isPartial() || mode === "multiple";
2181
+ if (isComplete) this.close();
2182
+ }
2183
+ },
2184
+ isSelected(date) {
2185
+ return this._selection.isSelected(date);
2186
+ },
2187
+ isInRange(date, hoverDate) {
2188
+ if (mode !== "range") return false;
2189
+ return this._selection.isInRange(date, hoverDate);
2190
+ },
2191
+ isRangeStart(date) {
2192
+ if (mode !== "range") return false;
2193
+ const start = this._selection.getStart();
2194
+ return start !== null && date.isSame(start);
2195
+ },
2196
+ isRangeEnd(date) {
2197
+ if (mode !== "range") return false;
2198
+ const end = this._selection.getEnd();
2199
+ return end !== null && date.isSame(end);
2200
+ },
2201
+ /**
2202
+ * Check whether selecting `date` as a range endpoint would produce a valid range.
2203
+ *
2204
+ * Returns `true` when:
2205
+ * - The date is not disabled AND
2206
+ * - Either no range start is selected yet (any non-disabled date can start), OR
2207
+ * - Completing the range with this date would satisfy minRange/maxRange constraints.
2208
+ *
2209
+ * Returns `false` for non-range modes.
2210
+ *
2211
+ * Useful for dimming dates in the UI that would result in an invalid range.
2212
+ */
2213
+ isDateSelectableForRange(date) {
2214
+ if (mode !== "range") return false;
2215
+ if (this._isDisabledDate(date)) return false;
2216
+ const range = this._selection;
2217
+ if (!range.isPartial()) return true;
2218
+ const rangeStart = range.getStart();
2219
+ if (!rangeStart) return true;
2220
+ if (date.isSame(rangeStart)) return true;
2221
+ let start = rangeStart;
2222
+ let end = date;
2223
+ if (end.isBefore(start)) {
2224
+ const tmp = start;
2225
+ start = end;
2226
+ end = tmp;
2227
+ }
2228
+ return this._isRangeValid(start, end);
2229
+ },
2230
+ /**
2231
+ * Get human-readable reasons why a date is disabled.
2232
+ * Returns an empty array for enabled dates.
2233
+ * Accepts a CalendarDate or ISO string.
2234
+ */
2235
+ getDisabledReason(date) {
2236
+ const d = typeof date === "string" ? CalendarDate.fromISO(date) : date;
2237
+ if (!d) return [];
2238
+ return this._getDisabledReasons(d);
2239
+ },
2240
+ clearSelection() {
2241
+ this._selection.clear();
2242
+ this._selectionRev++;
2243
+ this._emitChange();
2244
+ this._syncInputFromSelection();
2245
+ this._announce("Selection cleared");
2246
+ },
2247
+ // --- Programmatic control ---
2248
+ /**
2249
+ * Set the calendar selection programmatically.
2250
+ *
2251
+ * Accepts a single ISO date string, an array of ISO strings, or CalendarDate(s).
2252
+ * The calendar navigates to show the first selected date.
2253
+ *
2254
+ * Usage:
2255
+ * ```js
2256
+ * // Single mode
2257
+ * $data.setValue('2025-06-15')
2258
+ *
2259
+ * // Multiple mode
2260
+ * $data.setValue(['2025-06-15', '2025-06-20'])
2261
+ *
2262
+ * // Range mode
2263
+ * $data.setValue(['2025-06-15', '2025-06-20'])
2264
+ *
2265
+ * // With CalendarDate
2266
+ * $data.setValue(new CalendarDate(2025, 6, 15))
2267
+ * ```
2268
+ */
2269
+ setValue(value) {
2270
+ this._selection.clear();
2271
+ const dates = [];
2272
+ if (typeof value === "string") {
2273
+ const d = CalendarDate.fromISO(value) ?? parseDate(value, format);
2274
+ if (d && !this._isDisabledDate(d)) {
2275
+ dates.push(d);
2276
+ }
2277
+ } else if (Array.isArray(value)) {
2278
+ for (const v of value) {
2279
+ const d = typeof v === "string" ? CalendarDate.fromISO(v) ?? parseDate(v, format) : v;
2280
+ if (d && !this._isDisabledDate(d)) {
2281
+ dates.push(d);
2282
+ }
2283
+ }
2284
+ } else if (value instanceof CalendarDate) {
2285
+ if (!this._isDisabledDate(value)) {
2286
+ dates.push(value);
2287
+ }
2288
+ }
2289
+ if (mode === "range" && dates.length === 2) {
2290
+ let [start, end] = dates;
2291
+ if (end.isBefore(start)) {
2292
+ const tmp = start;
2293
+ start = end;
2294
+ end = tmp;
2295
+ }
2296
+ if (!this._isRangeValid(start, end)) {
2297
+ this._selectionRev++;
2298
+ this._emitChange();
2299
+ this._syncInputFromSelection();
2300
+ return;
2301
+ }
2302
+ this._selection.setRange(start, end);
2303
+ } else {
2304
+ for (const d of dates) {
2305
+ this._selection.toggle(d);
2306
+ }
2307
+ }
2308
+ const selected = this._selection.toArray();
2309
+ if (selected.length > 0) {
2310
+ const first = selected[0];
2311
+ this.month = first.month;
2312
+ this.year = first.year;
2313
+ }
2314
+ this._selectionRev++;
2315
+ this._emitChange();
2316
+ this._syncInputFromSelection();
2317
+ },
2318
+ /**
2319
+ * Clear the current selection. Alias for `clearSelection()`.
2320
+ */
2321
+ clear() {
2322
+ this.clearSelection();
2323
+ },
2324
+ /**
2325
+ * Navigate the calendar to a specific year and month without changing selection.
2326
+ *
2327
+ * Usage:
2328
+ * ```js
2329
+ * $data.goTo(2025, 6) // Navigate to June 2025
2330
+ * $data.goTo(2030) // Navigate to the current month in 2030
2331
+ * ```
2332
+ */
2333
+ goTo(year, month) {
2334
+ this.year = year;
2335
+ if (month !== void 0) {
2336
+ this.month = month;
2337
+ }
2338
+ this.view = "days";
2339
+ if (this.isScrollable) {
2340
+ this._rebuildGrid();
2341
+ alpine(this).$nextTick(() => {
2342
+ this._rebindScrollObserver();
2343
+ this._scrollToMonth(year, month ?? this.month);
2344
+ });
2345
+ }
2346
+ },
2347
+ /**
2348
+ * Get the current selection as an array of CalendarDate objects.
2349
+ *
2350
+ * Returns a new array each time (safe to mutate).
2351
+ *
2352
+ * Usage:
2353
+ * ```js
2354
+ * const dates = $data.getSelection()
2355
+ * console.log(dates.map(d => d.toISO()))
2356
+ * ```
2357
+ */
2358
+ getSelection() {
2359
+ return [...this._selection.toArray()];
2360
+ },
2361
+ /**
2362
+ * Apply a predefined range preset by index.
2363
+ *
2364
+ * Evaluates the preset's `value()` function, sets the selection to the
2365
+ * returned `[start, end]` range, navigates to the start date, and
2366
+ * emits a change event. In popup mode with `closeOnSelect`, the popup closes.
2367
+ *
2368
+ * Usage in Alpine template:
2369
+ * ```html
2370
+ * <template x-for="(preset, pi) in presets" :key="pi">
2371
+ * <button @click="applyPreset(pi)" x-text="preset.label"></button>
2372
+ * </template>
2373
+ * ```
2374
+ */
2375
+ applyPreset(index) {
2376
+ const preset = this.presets[index];
2377
+ if (!preset) return;
2378
+ const [start, end] = preset.value();
2379
+ if (mode === "single") {
2380
+ this.setValue(start);
2381
+ } else {
2382
+ this.setValue([start, end]);
2383
+ }
2384
+ if (this.display === "popup" && closeOnSelect) {
2385
+ this.close();
2386
+ }
2387
+ },
2388
+ // --- Runtime config ---
2389
+ /**
2390
+ * Update constraint-related configuration at runtime.
2391
+ *
2392
+ * Rebuilds all constraint functions and refreshes grids. Accepts the same
2393
+ * constraint properties as `CalendarConfig` (minDate, maxDate, disabledDates,
2394
+ * disabledDaysOfWeek, enabledDates, enabledDaysOfWeek, disabledMonths,
2395
+ * enabledMonths, disabledYears, enabledYears, minRange, maxRange, rules).
2396
+ *
2397
+ * Updates are **merged** with the existing constraint config. To remove a
2398
+ * previously set constraint, pass `undefined` explicitly (e.g.
2399
+ * `{ disabledDaysOfWeek: undefined }`).
2400
+ *
2401
+ * Usage:
2402
+ * ```js
2403
+ * $data.updateConstraints({ minDate: '2025-06-01', disabledDaysOfWeek: [0, 6] })
2404
+ * ```
2405
+ */
2406
+ updateConstraints(updates) {
2407
+ const merged = { ...this._constraintConfig };
2408
+ for (const key of CONSTRAINT_KEYS) {
2409
+ if (key in updates) {
2410
+ merged[key] = updates[key];
2411
+ }
2412
+ }
2413
+ this._constraintConfig = merged;
2414
+ const c = buildConstraints(merged, constraintMessages);
2415
+ this._isDisabledDate = c.isDisabledDate;
2416
+ this._isRangeValid = c.isRangeValid;
2417
+ this._isMonthDisabled = c.isMonthDisabled;
2418
+ this._isYearDisabled = c.isYearDisabled;
2419
+ this._getDisabledReasons = c.getDisabledReasons;
2420
+ this._rebuildGrid();
2421
+ this._rebuildMonthGrid();
2422
+ this._rebuildYearGrid();
2423
+ if (this.isScrollable) {
2424
+ alpine(this).$nextTick(() => {
2425
+ this._rebindScrollObserver();
2426
+ });
2427
+ }
2428
+ },
2429
+ // --- View switching ---
2430
+ setView(newView) {
2431
+ this.view = newView;
2432
+ },
2433
+ selectMonth(targetMonth) {
2434
+ if (this._isMonthDisabled(this.year, targetMonth)) return;
2435
+ this.month = targetMonth;
2436
+ this._wizardMonth = targetMonth;
2437
+ if (wizardMode === "year-month") {
2438
+ this.wizardStep = wizardTotalSteps;
2439
+ this.selectDate(new CalendarDate(this.year, targetMonth, 1));
2440
+ return;
2441
+ }
2442
+ this.view = "days";
2443
+ if (wizard) {
2444
+ this.wizardStep = wizardMode === "month-day" ? 2 : 3;
2445
+ }
2446
+ },
2447
+ selectYear(targetYear) {
2448
+ if (this._isYearDisabled(targetYear)) return;
2449
+ this.year = targetYear;
2450
+ this._wizardYear = targetYear;
2451
+ this.view = "months";
2452
+ if (this.wizard) this.wizardStep = 2;
2453
+ },
2454
+ /** Navigate the wizard back one step. No-op if not in wizard mode. */
2455
+ wizardBack() {
2456
+ if (!this.wizard) return;
2457
+ if (wizardMode === "full") {
2458
+ if (this.wizardStep === 3) {
2459
+ this.wizardStep = 2;
2460
+ this.view = "months";
2461
+ this._wizardMonth = null;
2462
+ this._wizardDay = null;
2463
+ } else if (this.wizardStep === 2) {
2464
+ this.wizardStep = 1;
2465
+ this.view = "years";
2466
+ this._wizardYear = null;
2467
+ this._wizardMonth = null;
2468
+ }
2469
+ } else if (wizardMode === "year-month") {
2470
+ if (this.wizardStep === 2) {
2471
+ this.wizardStep = 1;
2472
+ this.view = "years";
2473
+ this._wizardYear = null;
2474
+ this._wizardMonth = null;
2475
+ }
2476
+ } else if (wizardMode === "month-day") {
2477
+ if (this.wizardStep === 2) {
2478
+ this.wizardStep = 1;
2479
+ this.view = "months";
2480
+ this._wizardMonth = null;
2481
+ }
2482
+ }
2483
+ },
2484
+ // --- Popup ---
2485
+ open() {
2486
+ var _a;
2487
+ if (this.display !== "popup") return;
2488
+ if (wizard) {
2489
+ this.wizardStep = 1;
2490
+ this.view = wizardStartView;
2491
+ this._wizardYear = null;
2492
+ this._wizardMonth = null;
2493
+ this._wizardDay = null;
2494
+ if (wizardMode !== "month-day") {
2495
+ this.year = this._today.year - 30;
2496
+ this._rebuildYearGrid();
2497
+ }
2498
+ }
2499
+ this.isOpen = true;
2500
+ (_a = this._inputEl) == null ? void 0 : _a.setAttribute("aria-expanded", "true");
2501
+ alpine(this).$dispatch("calendar:open");
2502
+ alpine(this).$nextTick(() => {
2503
+ });
2504
+ },
2505
+ close() {
2506
+ var _a;
2507
+ if (this.display !== "popup") return;
2508
+ this.isOpen = false;
2509
+ (_a = this._inputEl) == null ? void 0 : _a.setAttribute("aria-expanded", "false");
2510
+ alpine(this).$dispatch("calendar:close");
2511
+ },
2512
+ toggle() {
2513
+ if (this.isOpen) {
2514
+ this.close();
2515
+ } else {
2516
+ this.open();
2517
+ }
2518
+ },
2519
+ /**
2520
+ * Handle keydown events on the calendar container.
2521
+ *
2522
+ * Supports full keyboard navigation per ARIA grid pattern:
2523
+ * - Arrow keys: move focus between days (±1 day / ±7 days)
2524
+ * - Enter/Space: select the focused day
2525
+ * - PageUp/PageDown: navigate prev/next month (+ Shift: prev/next year)
2526
+ * - Home/End: jump to first/last day of current month
2527
+ * - Escape: return to day view from month/year picker, or close popup
2528
+ * - Tab: natural tab order (exits calendar)
2529
+ *
2530
+ * Uses `aria-activedescendant` pattern: the calendar grid container holds
2531
+ * focus (`tabindex="0"`), and `focusedDate` drives which cell is visually
2532
+ * highlighted. Day cells should have `id="day-{ISO}"` in the template.
2533
+ *
2534
+ * Usage in Alpine template:
2535
+ * ```html
2536
+ * <div @keydown="handleKeydown($event)" tabindex="0"
2537
+ * :aria-activedescendant="focusedDateISO ? 'day-' + focusedDateISO : null">
2538
+ * ```
2539
+ */
2540
+ handleKeydown(e) {
2541
+ if (e.key === "Escape") {
2542
+ e.preventDefault();
2543
+ e.stopPropagation();
2544
+ if (this.wizard && this.wizardStep > 1) {
2545
+ this.wizardBack();
2546
+ return;
2547
+ }
2548
+ if (this.view === "months" || this.view === "years") {
2549
+ this.view = "days";
2550
+ return;
2551
+ }
2552
+ if (display === "popup" && this.isOpen) {
2553
+ this.close();
2554
+ if (this._inputEl) {
2555
+ this._suppressFocusOpen = true;
2556
+ this._inputEl.focus();
2557
+ }
2558
+ }
2559
+ return;
2560
+ }
2561
+ if (this.view === "days") {
2562
+ switch (e.key) {
2563
+ // Navigation keys: auto-initialize focusedDate if not set
2564
+ case "ArrowRight":
2565
+ case "ArrowLeft":
2566
+ case "ArrowDown":
2567
+ case "ArrowUp":
2568
+ case "PageDown":
2569
+ case "PageUp":
2570
+ case "Home":
2571
+ case "End": {
2572
+ e.preventDefault();
2573
+ if (!this.focusedDate) {
2574
+ const selected = this._selection.toArray();
2575
+ if (selected.length > 0) {
2576
+ this.focusedDate = selected[0];
2577
+ } else {
2578
+ this.focusedDate = new CalendarDate(this.year, this.month, 1);
2579
+ }
2580
+ }
2581
+ if (e.key === "ArrowRight") this._moveFocus(1);
2582
+ else if (e.key === "ArrowLeft") this._moveFocus(-1);
2583
+ else if (e.key === "ArrowDown") this._moveFocus(7);
2584
+ else if (e.key === "ArrowUp") this._moveFocus(-7);
2585
+ else if (e.key === "PageDown") this._moveFocusByMonths(e.shiftKey ? 12 : 1);
2586
+ else if (e.key === "PageUp") this._moveFocusByMonths(e.shiftKey ? -12 : -1);
2587
+ else if (e.key === "Home")
2588
+ this._setFocusedDate(new CalendarDate(this.year, this.month, 1));
2589
+ else if (e.key === "End")
2590
+ this._setFocusedDate(new CalendarDate(this.year, this.month, 1).endOfMonth());
2591
+ return;
2592
+ }
2593
+ // Selection keys: only act when focus is already established
2594
+ case "Enter":
2595
+ case " ":
2596
+ if (this.focusedDate) {
2597
+ e.preventDefault();
2598
+ this.selectDate(this.focusedDate);
2599
+ }
2600
+ return;
2601
+ }
2602
+ }
2603
+ },
2604
+ // --- Internal: keyboard focus management ---
2605
+ /**
2606
+ * Move focusedDate by a number of days, navigating months as needed.
2607
+ * Skips disabled dates in the direction of movement (up to 31 attempts).
2608
+ */
2609
+ _moveFocus(deltaDays) {
2610
+ if (!this.focusedDate) return;
2611
+ let candidate = this.focusedDate.addDays(deltaDays);
2612
+ let attempts = 0;
2613
+ while (this._isDisabledDate(candidate) && attempts < 31) {
2614
+ candidate = candidate.addDays(deltaDays > 0 ? 1 : -1);
2615
+ attempts++;
2616
+ }
2617
+ if (this._isDisabledDate(candidate)) return;
2618
+ this._setFocusedDate(candidate);
2619
+ },
2620
+ /**
2621
+ * Move focusedDate by a number of months, clamping the day to valid range.
2622
+ */
2623
+ _moveFocusByMonths(deltaMonths) {
2624
+ if (!this.focusedDate) return;
2625
+ const candidate = this.focusedDate.addMonths(deltaMonths);
2626
+ this._setFocusedDate(candidate);
2627
+ },
2628
+ /**
2629
+ * Set focusedDate and navigate the calendar view if needed.
2630
+ */
2631
+ _setFocusedDate(date) {
2632
+ this.focusedDate = date;
2633
+ if (this.isScrollable) {
2634
+ alpine(this).$nextTick(() => {
2635
+ this._scrollToMonth(date.year, date.month);
2636
+ });
2637
+ return;
2638
+ }
2639
+ if (date.month !== this.month || date.year !== this.year) {
2640
+ this.month = date.month;
2641
+ this.year = date.year;
2642
+ }
2643
+ },
2644
+ // --- Internal: scrollable multi-month ---
2645
+ /** Label for the sticky header in scrollable mode — tracks the topmost visible month. */
2646
+ get scrollHeaderLabel() {
2647
+ if (!this.isScrollable) return "";
2648
+ const mg = this.grid[this._scrollVisibleIndex];
2649
+ if (!mg) return "";
2650
+ const d = new CalendarDate(mg.year, mg.month, 1);
2651
+ return d.format({ month: "long", year: "numeric" }, locale);
2652
+ },
2653
+ /** Attach an IntersectionObserver that updates the sticky header as the user scrolls. */
2654
+ _initScrollListener() {
2655
+ if (typeof IntersectionObserver === "undefined") return;
2656
+ const el = alpine(this).$el;
2657
+ const container = el.querySelector(".rc-months--scroll");
2658
+ if (!container) return;
2659
+ this._scrollContainerEl = container;
2660
+ const monthEls = container.querySelectorAll("[data-month-id]");
2661
+ if (monthEls.length === 0) return;
2662
+ const indexMap = /* @__PURE__ */ new Map();
2663
+ monthEls.forEach((el2, i) => indexMap.set(el2, i));
2664
+ const visible = /* @__PURE__ */ new Set();
2665
+ const observer = new IntersectionObserver(
2666
+ (entries) => {
2667
+ for (const entry of entries) {
2668
+ const idx = indexMap.get(entry.target);
2669
+ if (idx === void 0) continue;
2670
+ if (entry.isIntersecting) {
2671
+ visible.add(idx);
2672
+ } else {
2673
+ visible.delete(idx);
2674
+ }
2675
+ }
2676
+ if (visible.size > 0) {
2677
+ const minIdx = Math.min(...visible);
2678
+ if (this._scrollVisibleIndex !== minIdx) {
2679
+ this._scrollVisibleIndex = minIdx;
2680
+ }
2681
+ }
2682
+ },
2683
+ { root: container, rootMargin: "0px 0px -90% 0px", threshold: 0 }
2684
+ );
2685
+ monthEls.forEach((el2) => observer.observe(el2));
2686
+ this._scrollObserver = observer;
2687
+ },
2688
+ /** Disconnect and re-initialize the scroll observer after DOM rebuild. */
2689
+ _rebindScrollObserver() {
2690
+ if (this._scrollObserver) {
2691
+ this._scrollObserver.disconnect();
2692
+ this._scrollObserver = null;
2693
+ }
2694
+ this._initScrollListener();
2695
+ },
2696
+ /** Smooth scroll a specific month into view inside the scroll container. */
2697
+ _scrollToMonth(year, month) {
2698
+ if (!this._scrollContainerEl) return;
2699
+ const el = this._scrollContainerEl.querySelector(
2700
+ `[data-month-id="month-${year}-${month}"]`
2701
+ );
2702
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
2703
+ }
2704
+ };
2705
+ }
2706
+ function presetToday(label = "Today", timezone) {
2707
+ return {
2708
+ label,
2709
+ value: () => {
2710
+ const today = CalendarDate.today(timezone);
2711
+ return [today, today];
2712
+ }
2713
+ };
2714
+ }
2715
+ function presetYesterday(label = "Yesterday", timezone) {
2716
+ return {
2717
+ label,
2718
+ value: () => {
2719
+ const yesterday = CalendarDate.today(timezone).addDays(-1);
2720
+ return [yesterday, yesterday];
2721
+ }
2722
+ };
2723
+ }
2724
+ function presetLastNDays(days, label, timezone) {
2725
+ return {
2726
+ label: label ?? `Last ${days} Days`,
2727
+ value: () => {
2728
+ const today = CalendarDate.today(timezone);
2729
+ return [today.addDays(-(days - 1)), today];
2730
+ }
2731
+ };
2732
+ }
2733
+ function presetThisWeek(label = "This Week", firstDay = 1, timezone) {
2734
+ return {
2735
+ label,
2736
+ value: () => {
2737
+ const today = CalendarDate.today(timezone);
2738
+ const dow = today.toNativeDate().getDay();
2739
+ const diff = (dow - firstDay + 7) % 7;
2740
+ const weekStart = today.addDays(-diff);
2741
+ return [weekStart, today];
2742
+ }
2743
+ };
2744
+ }
2745
+ function presetLastWeek(label = "Last Week", firstDay = 1, timezone) {
2746
+ return {
2747
+ label,
2748
+ value: () => {
2749
+ const today = CalendarDate.today(timezone);
2750
+ const dow = today.toNativeDate().getDay();
2751
+ const diff = (dow - firstDay + 7) % 7;
2752
+ const thisWeekStart = today.addDays(-diff);
2753
+ const lastWeekStart = thisWeekStart.addDays(-7);
2754
+ const lastWeekEnd = thisWeekStart.addDays(-1);
2755
+ return [lastWeekStart, lastWeekEnd];
2756
+ }
2757
+ };
2758
+ }
2759
+ function presetThisMonth(label = "This Month", timezone) {
2760
+ return {
2761
+ label,
2762
+ value: () => {
2763
+ const today = CalendarDate.today(timezone);
2764
+ return [today.startOfMonth(), today];
2765
+ }
2766
+ };
2767
+ }
2768
+ function presetLastMonth(label = "Last Month", timezone) {
2769
+ return {
2770
+ label,
2771
+ value: () => {
2772
+ const today = CalendarDate.today(timezone);
2773
+ const prevMonth = today.addMonths(-1);
2774
+ return [
2775
+ prevMonth.startOfMonth(),
2776
+ new CalendarDate(prevMonth.year, prevMonth.month, daysInMonth(prevMonth.year, prevMonth.month))
2777
+ ];
2778
+ }
2779
+ };
2780
+ }
2781
+ function presetThisYear(label = "This Year", timezone) {
2782
+ return {
2783
+ label,
2784
+ value: () => {
2785
+ const today = CalendarDate.today(timezone);
2786
+ return [new CalendarDate(today.year, 1, 1), today];
2787
+ }
2788
+ };
2789
+ }
2790
+ function presetLastYear(label = "Last Year", timezone) {
2791
+ return {
2792
+ label,
2793
+ value: () => {
2794
+ const today = CalendarDate.today(timezone);
2795
+ const prevYear = today.year - 1;
2796
+ return [new CalendarDate(prevYear, 1, 1), new CalendarDate(prevYear, 12, 31)];
2797
+ }
2798
+ };
2799
+ }
2800
+ function computePosition(reference, floating, options = {}) {
2801
+ const placement = options.placement ?? "bottom-start";
2802
+ const offset = options.offset ?? 4;
2803
+ const flip = options.flip ?? true;
2804
+ const refRect = reference.getBoundingClientRect();
2805
+ const floatRect = floating.getBoundingClientRect();
2806
+ const viewportHeight = window.innerHeight;
2807
+ const viewportWidth = window.innerWidth;
2808
+ let x;
2809
+ const side = placement.includes("start") ? "start" : "end";
2810
+ if (side === "start") {
2811
+ x = refRect.left;
2812
+ } else {
2813
+ x = refRect.right - floatRect.width;
2814
+ }
2815
+ x = clamp(x, 0, Math.max(0, viewportWidth - floatRect.width));
2816
+ const isBottom = placement.startsWith("bottom");
2817
+ let resolvedPlacement = placement;
2818
+ if (isBottom) {
2819
+ const yBelow = refRect.bottom + offset;
2820
+ const fitsBelow = yBelow + floatRect.height <= viewportHeight;
2821
+ if (!fitsBelow && flip) {
2822
+ const yAbove2 = refRect.top - offset - floatRect.height;
2823
+ const fitsAbove = yAbove2 >= 0;
2824
+ if (fitsAbove) {
2825
+ resolvedPlacement = placement.replace("bottom", "top");
2826
+ return { x, y: yAbove2, placement: resolvedPlacement };
2827
+ }
2828
+ }
2829
+ return { x, y: yBelow, placement: resolvedPlacement };
2830
+ }
2831
+ const yAbove = refRect.top - offset - floatRect.height;
2832
+ if (yAbove < 0 && flip) {
2833
+ const yBelow = refRect.bottom + offset;
2834
+ const fitsBelow = yBelow + floatRect.height <= viewportHeight;
2835
+ if (fitsBelow) {
2836
+ resolvedPlacement = placement.replace("top", "bottom");
2837
+ return { x, y: yBelow, placement: resolvedPlacement };
2838
+ }
2839
+ }
2840
+ return { x, y: Math.max(0, yAbove), placement: resolvedPlacement };
2841
+ }
2842
+ function autoUpdate(reference, update, throttleMs = 16) {
2843
+ let ticking = false;
2844
+ let rafId = 0;
2845
+ let timerId = 0;
2846
+ const onEvent = () => {
2847
+ if (ticking) return;
2848
+ ticking = true;
2849
+ if (throttleMs <= 16) {
2850
+ rafId = requestAnimationFrame(() => {
2851
+ update();
2852
+ ticking = false;
2853
+ });
2854
+ } else {
2855
+ timerId = window.setTimeout(() => {
2856
+ update();
2857
+ ticking = false;
2858
+ }, throttleMs);
2859
+ }
2860
+ };
2861
+ window.addEventListener("resize", onEvent, { passive: true });
2862
+ const scrollParents = getScrollParents(reference);
2863
+ for (const parent of scrollParents) {
2864
+ parent.addEventListener("scroll", onEvent, { passive: true });
2865
+ }
2866
+ return () => {
2867
+ cancelAnimationFrame(rafId);
2868
+ clearTimeout(timerId);
2869
+ window.removeEventListener("resize", onEvent);
2870
+ for (const parent of scrollParents) {
2871
+ parent.removeEventListener("scroll", onEvent);
2872
+ }
2873
+ };
2874
+ }
2875
+ function clamp(value, min, max) {
2876
+ return Math.max(min, Math.min(max, value));
2877
+ }
2878
+ function getScrollParents(element) {
2879
+ const parents = [];
2880
+ let current = element.parentElement;
2881
+ while (current) {
2882
+ const style = getComputedStyle(current);
2883
+ const overflow = style.overflow + style.overflowX + style.overflowY;
2884
+ if (/auto|scroll|overlay/.test(overflow)) {
2885
+ parents.push(current);
2886
+ }
2887
+ current = current.parentElement;
2888
+ }
2889
+ parents.push(document);
2890
+ return parents;
2891
+ }
2892
+ let globalDefaults = {};
2893
+ function calendarPlugin(Alpine) {
2894
+ Alpine.data(
2895
+ "calendar",
2896
+ (config = {}) => createCalendarData({ ...globalDefaults, ...config }, Alpine)
2897
+ );
2898
+ }
2899
+ calendarPlugin.defaults = (config) => {
2900
+ globalDefaults = { ...globalDefaults, ...config };
2901
+ };
2902
+ calendarPlugin.getDefaults = () => ({ ...globalDefaults });
2903
+ calendarPlugin.resetDefaults = () => {
2904
+ globalDefaults = {};
2905
+ };
2906
+ export {
2907
+ CalendarDate,
2908
+ MultipleSelection,
2909
+ RangeSelection,
2910
+ SingleSelection,
2911
+ attachMask,
2912
+ autoUpdate,
2913
+ calendarPlugin,
2914
+ computePosition,
2915
+ createCalendarData,
2916
+ createDateConstraint,
2917
+ createDisabledReasons,
2918
+ createMask,
2919
+ createMaskHandlers,
2920
+ createRangeValidator,
2921
+ daysInMonth,
2922
+ calendarPlugin as default,
2923
+ formatDate,
2924
+ formatMultiple,
2925
+ formatRange,
2926
+ generateMonth,
2927
+ generateMonthGrid,
2928
+ generateMonths,
2929
+ generateYearGrid,
2930
+ getISOWeekNumber,
2931
+ isDateDisabled,
2932
+ parseDate,
2933
+ parseDateMultiple,
2934
+ parseDateRange,
2935
+ parseFormatToSlots,
2936
+ presetLastMonth,
2937
+ presetLastNDays,
2938
+ presetLastWeek,
2939
+ presetLastYear,
2940
+ presetThisMonth,
2941
+ presetThisWeek,
2942
+ presetThisYear,
2943
+ presetToday,
2944
+ presetYesterday
2945
+ };
2946
+ //# sourceMappingURL=alpine-calendar.es.js.map