@calchemy/date-react 0.1.0 → 0.1.2

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.
package/dist/index.js CHANGED
@@ -1,3 +1,1674 @@
1
- export { Calchemy, useCalchemyCalendar, useCalchemyContext } from "./components/Calchemy";
2
- export { useCalchemy } from "./hooks/useCalchemy";
1
+ import {
2
+ CalchemyContext,
3
+ CalendarContext,
4
+ CalendarPeriodContext,
5
+ CalendarScrollContext,
6
+ useCalchemyCalendar,
7
+ useCalchemyContext,
8
+ useCalendarPeriod
9
+ } from "./chunk-NGJGJXY4.js";
10
+
11
+ // src/hooks/useCalchemy.ts
12
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
13
+ import { resolveExpectedDateValue } from "@calchemy/date-core";
14
+
15
+ // src/inline-completion.ts
16
+ function composeInlineCompletion(inputValue, completion) {
17
+ return `${inputValue}${completion.suffix}`;
18
+ }
19
+ function formatInlineCompletionDescription(inputValue, completion) {
20
+ return `Suggestion: ${composeInlineCompletion(inputValue, completion)}. Press Tab to accept.`;
21
+ }
22
+
23
+ // src/hooks/useDebouncedValue.ts
24
+ import { useEffect, useState } from "react";
25
+ var PARSE_QUERY_DEBOUNCE_MS = 150;
26
+ function useDebouncedValue(value, delayMs = PARSE_QUERY_DEBOUNCE_MS) {
27
+ const [debouncedValue, setDebouncedValue] = useState(value);
28
+ useEffect(() => {
29
+ const timeoutId = setTimeout(() => {
30
+ setDebouncedValue(value);
31
+ }, delayMs);
32
+ return () => {
33
+ clearTimeout(timeoutId);
34
+ };
35
+ }, [value, delayMs]);
36
+ return debouncedValue;
37
+ }
38
+
39
+ // src/hooks/useCalchemy.ts
40
+ function useCalchemy(options) {
41
+ const [uncontrolledInputValue, setUncontrolledInputValue] = useState2(options.defaultInputValue ?? "");
42
+ const [uncontrolledValue, setUncontrolledValue] = useState2(options.defaultValue ?? null);
43
+ const [uncontrolledInputMode, setUncontrolledInputMode] = useState2(
44
+ options.defaultInputMode ?? "field"
45
+ );
46
+ const inputValue = options.inputValue ?? uncontrolledInputValue;
47
+ const value = options.value ?? uncontrolledValue;
48
+ const inputMode = options.inputMode ?? uncontrolledInputMode;
49
+ const fieldInputActive = inputMode === "field";
50
+ const queryValue = useDebouncedValue(inputValue);
51
+ const queryPending = inputValue !== queryValue;
52
+ const result = useMemo(
53
+ () => options.calchemy.parseDate(queryValue, options.parseContext),
54
+ [queryValue, options.calchemy, options.parseContext]
55
+ );
56
+ const expectedValue = options.expectedValue;
57
+ const expectedOptions = {
58
+ ...options.multipleRangeExpansionLimit === void 0 ? {} : { multipleRangeExpansionLimit: options.multipleRangeExpansionLimit }
59
+ };
60
+ const expectedResult = resolveExpectedDateValue(result, expectedValue, expectedOptions);
61
+ const valueKindMismatch = expectedResult.status === "invalid" && result.status === "valid";
62
+ const settledInlineCompletion = useMemo(
63
+ () => options.calchemy.getInlineCompletion(queryValue, options.parseContext),
64
+ [queryValue, options.calchemy, options.parseContext]
65
+ );
66
+ const inlineCompletion = queryPending ? null : settledInlineCompletion;
67
+ function updateValue(nextValue, nextResult = result) {
68
+ if (options.value === void 0) {
69
+ setUncontrolledValue(nextValue);
70
+ }
71
+ options.onValueChange?.(nextValue, nextResult);
72
+ }
73
+ useEffect2(() => {
74
+ const nextResult = options.calchemy.parseDate(queryValue, options.parseContext);
75
+ const nextExpectedResult = resolveExpectedDateValue(
76
+ nextResult,
77
+ expectedValue,
78
+ expectedOptions
79
+ );
80
+ if (nextExpectedResult.status === "valid") {
81
+ if (options.value === void 0) {
82
+ setUncontrolledValue(nextExpectedResult.value);
83
+ }
84
+ options.onValueChange?.(nextExpectedResult.value, nextExpectedResult);
85
+ }
86
+ }, [
87
+ queryValue,
88
+ expectedValue,
89
+ expectedOptions.multipleRangeExpansionLimit,
90
+ options.calchemy,
91
+ options.parseContext
92
+ ]);
93
+ function updateInputValue(nextValue) {
94
+ if (options.inputValue === void 0) {
95
+ setUncontrolledInputValue(nextValue);
96
+ }
97
+ options.onInputValueChange?.(nextValue);
98
+ }
99
+ function getActiveInlineCompletion() {
100
+ if (!fieldInputActive || queryPending) {
101
+ return null;
102
+ }
103
+ return settledInlineCompletion;
104
+ }
105
+ function setInputMode(nextMode) {
106
+ if (options.inputMode === void 0) {
107
+ setUncontrolledInputMode(nextMode);
108
+ }
109
+ options.onInputModeChange?.(nextMode);
110
+ }
111
+ function acceptCompletion() {
112
+ const activeCompletion = getActiveInlineCompletion();
113
+ if (!activeCompletion) {
114
+ return;
115
+ }
116
+ updateInputValue(composeInlineCompletion(inputValue, activeCompletion));
117
+ }
118
+ function selectCandidate(candidateId) {
119
+ if (!fieldInputActive || result.status !== "ambiguous") {
120
+ return;
121
+ }
122
+ const candidate = result.candidates.find((item) => item.id === candidateId);
123
+ if (!candidate) {
124
+ return;
125
+ }
126
+ const candidateResult = {
127
+ status: "valid",
128
+ input: inputValue,
129
+ value: candidate.value,
130
+ candidates: [candidate],
131
+ corrections: result.corrections,
132
+ warnings: result.warnings
133
+ };
134
+ const resolvedCandidateResult = resolveExpectedDateValue(candidateResult, expectedValue, expectedOptions);
135
+ if (resolvedCandidateResult.status !== "valid") {
136
+ return;
137
+ }
138
+ updateValue(resolvedCandidateResult.value, resolvedCandidateResult);
139
+ }
140
+ function selectDate(nextValue) {
141
+ const nextResult = {
142
+ status: "valid",
143
+ input: inputValue,
144
+ value: nextValue,
145
+ candidates: [],
146
+ corrections: [],
147
+ warnings: []
148
+ };
149
+ const resolvedResult = resolveExpectedDateValue(nextResult, expectedValue, expectedOptions);
150
+ if (resolvedResult.status !== "valid") {
151
+ return;
152
+ }
153
+ updateValue(resolvedResult.value, resolvedResult);
154
+ if (resolvedResult.value.kind === "single") {
155
+ updateInputValue(resolvedResult.value.date.toString());
156
+ }
157
+ }
158
+ return {
159
+ calchemy: options.calchemy,
160
+ parseContext: options.parseContext,
161
+ inputValue,
162
+ value,
163
+ result: expectedResult,
164
+ expectedValue,
165
+ inputMode,
166
+ valueKindMismatch,
167
+ inlineCompletion,
168
+ setInputValue: updateInputValue,
169
+ setInputMode,
170
+ acceptCompletion,
171
+ selectCandidate,
172
+ selectDate,
173
+ getInputProps() {
174
+ const activeCompletion = getActiveInlineCompletion();
175
+ return {
176
+ value: inputValue,
177
+ readOnly: !fieldInputActive,
178
+ onChange(event) {
179
+ if (!fieldInputActive) {
180
+ return;
181
+ }
182
+ updateInputValue(event.currentTarget.value);
183
+ },
184
+ onKeyDown(event) {
185
+ if (!fieldInputActive) {
186
+ return;
187
+ }
188
+ if (event.key === "Tab" && activeCompletion) {
189
+ event.preventDefault();
190
+ acceptCompletion();
191
+ }
192
+ },
193
+ "aria-invalid": expectedResult.status === "invalid",
194
+ ...fieldInputActive && inlineCompletion ? {
195
+ "aria-description": formatInlineCompletionDescription(
196
+ inputValue,
197
+ inlineCompletion
198
+ )
199
+ } : {},
200
+ "calchemy-status": valueKindMismatch ? "kind-mismatch" : expectedResult.status,
201
+ "calchemy-expected-value": expectedValue,
202
+ "calchemy-value-kind": expectedResult.status === "valid" ? expectedResult.value.kind : void 0
203
+ };
204
+ }
205
+ };
206
+ }
207
+
208
+ // src/components/calendar/Calendar.tsx
209
+ import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
210
+
211
+ // src/components/calendar/CalendarGrid.tsx
212
+ import { useMemo as useMemo3 } from "react";
213
+
214
+ // src/components/calendar/calendar-period-drag.tsx
215
+ import {
216
+ createContext,
217
+ useCallback,
218
+ useContext,
219
+ useMemo as useMemo2,
220
+ useRef,
221
+ useState as useState3
222
+ } from "react";
223
+ import { jsx } from "react/jsx-runtime";
224
+ var CalendarPeriodDragContext = createContext(null);
225
+ var multipleDragSurfaceStyle = {
226
+ position: "relative",
227
+ userSelect: "none",
228
+ WebkitUserSelect: "none",
229
+ touchAction: "none"
230
+ };
231
+ var multiplePeriodListDragSurfaceStyle = {
232
+ ...multipleDragSurfaceStyle,
233
+ position: "relative",
234
+ background: "transparent"
235
+ };
236
+ function mergeCalendarDragPointerProps(enabled, drag, handlers) {
237
+ if (!enabled || !drag) {
238
+ return handlers;
239
+ }
240
+ const { onPointerDownCapture, onPointerMove, onPointerUp, onPointerCancel, onDragStart } = handlers;
241
+ return {
242
+ onPointerDownCapture: (event) => {
243
+ drag.handlePointerDownCapture(event);
244
+ onPointerDownCapture?.(event);
245
+ },
246
+ onPointerMove: (event) => {
247
+ drag.handlePointerMove(event);
248
+ onPointerMove?.(event);
249
+ },
250
+ onPointerUp: (event) => {
251
+ drag.handlePointerUp(event);
252
+ onPointerUp?.(event);
253
+ },
254
+ onPointerCancel: (event) => {
255
+ drag.handlePointerCancel(event);
256
+ onPointerCancel?.(event);
257
+ },
258
+ onDragStart: (event) => {
259
+ event.preventDefault();
260
+ onDragStart?.(event);
261
+ }
262
+ };
263
+ }
264
+ function useOptionalCalendarPeriodDrag() {
265
+ return useContext(CalendarPeriodDragContext);
266
+ }
267
+ function useCalendarPeriodDragSurface(dragSelection = true, surfaceElementRef) {
268
+ const calendar = useCalchemyCalendar();
269
+ const multipleSelection = dragSelection && calendar.calchemy.expectedValue === "multiple";
270
+ const surfaceRef = useRef(null);
271
+ const dayCells = useRef(/* @__PURE__ */ new Map());
272
+ const suppressClickRef = useRef(false);
273
+ const dragStateRef = useRef(null);
274
+ const dragPreviewFrameRef = useRef(null);
275
+ const dragGestureCleanupRef = useRef(null);
276
+ const [dragState, setDragState] = useState3(null);
277
+ const [dragRectangle, setDragRectangle] = useState3(null);
278
+ const previewSelectedKeys = useMemo2(
279
+ () => new Set(dragState?.previewKeys ?? []),
280
+ [dragState]
281
+ );
282
+ const acquireDragGestureLock = useCallback(() => {
283
+ if (dragGestureCleanupRef.current || typeof document === "undefined") {
284
+ return;
285
+ }
286
+ const preventGestureDefault = (event) => {
287
+ event.preventDefault();
288
+ };
289
+ const clearDocumentSelection = () => {
290
+ document.getSelection()?.removeAllRanges();
291
+ };
292
+ const previousBodyUserSelect = document.body.style.userSelect;
293
+ const previousDocumentUserSelect = document.documentElement.style.userSelect;
294
+ document.body.style.userSelect = "none";
295
+ document.documentElement.style.userSelect = "none";
296
+ document.addEventListener("pointermove", preventGestureDefault, { capture: true, passive: false });
297
+ document.addEventListener("touchmove", preventGestureDefault, { capture: true, passive: false });
298
+ document.addEventListener("wheel", preventGestureDefault, { capture: true, passive: false });
299
+ document.addEventListener("selectstart", preventGestureDefault, { capture: true });
300
+ document.addEventListener("dragstart", preventGestureDefault, { capture: true });
301
+ clearDocumentSelection();
302
+ dragGestureCleanupRef.current = () => {
303
+ document.removeEventListener("pointermove", preventGestureDefault, { capture: true });
304
+ document.removeEventListener("touchmove", preventGestureDefault, { capture: true });
305
+ document.removeEventListener("wheel", preventGestureDefault, { capture: true });
306
+ document.removeEventListener("selectstart", preventGestureDefault, { capture: true });
307
+ document.removeEventListener("dragstart", preventGestureDefault, { capture: true });
308
+ document.body.style.userSelect = previousBodyUserSelect;
309
+ document.documentElement.style.userSelect = previousDocumentUserSelect;
310
+ clearDocumentSelection();
311
+ dragGestureCleanupRef.current = null;
312
+ };
313
+ }, []);
314
+ const releaseDragGestureLock = useCallback(() => {
315
+ dragGestureCleanupRef.current?.();
316
+ if (dragPreviewFrameRef.current !== null) {
317
+ cancelAnimationFrame(dragPreviewFrameRef.current);
318
+ dragPreviewFrameRef.current = null;
319
+ }
320
+ }, []);
321
+ const scheduleDragPreviewUpdate = useCallback((nextDragState) => {
322
+ dragStateRef.current = nextDragState;
323
+ if (dragPreviewFrameRef.current !== null) {
324
+ return;
325
+ }
326
+ dragPreviewFrameRef.current = requestAnimationFrame(() => {
327
+ setDragState(dragStateRef.current);
328
+ dragPreviewFrameRef.current = null;
329
+ });
330
+ }, []);
331
+ const registerDay = useCallback((element, date, disabled) => {
332
+ const key = date.toString();
333
+ if (element) {
334
+ dayCells.current.set(key, { date, disabled, element });
335
+ return;
336
+ }
337
+ dayCells.current.delete(key);
338
+ }, []);
339
+ const getDayCellFromTarget = useCallback((target) => {
340
+ if (!(target instanceof Element)) {
341
+ return null;
342
+ }
343
+ const button = target.closest("[calchemy-date]");
344
+ if (!(button instanceof HTMLButtonElement)) {
345
+ return null;
346
+ }
347
+ for (const cell of dayCells.current.values()) {
348
+ if (cell.element === button) {
349
+ return cell;
350
+ }
351
+ }
352
+ return null;
353
+ }, []);
354
+ const commitMultipleSelection = useCallback(
355
+ (keys, fallbackDates = []) => {
356
+ const dates = resolveDateKeys(keys, dayCells.current, fallbackDates);
357
+ calendar.selectValue({
358
+ kind: "multiple",
359
+ dates
360
+ });
361
+ },
362
+ [calendar]
363
+ );
364
+ const endDragGesture = useCallback(
365
+ (pointerId) => {
366
+ if (pointerId !== void 0) {
367
+ surfaceRef.current?.releasePointerCapture?.(pointerId);
368
+ }
369
+ releaseDragGestureLock();
370
+ dragStateRef.current = null;
371
+ setDragState(null);
372
+ setDragRectangle(null);
373
+ },
374
+ [releaseDragGestureLock]
375
+ );
376
+ const handlePointerMove = useCallback(
377
+ (event) => {
378
+ const activeDrag = dragStateRef.current;
379
+ if (!multipleSelection || !activeDrag || event.pointerId !== activeDrag.pointerId) {
380
+ return;
381
+ }
382
+ event.preventDefault();
383
+ const current = getPointerPoint(event);
384
+ setDragRectangle({ start: activeDrag.start, current });
385
+ const dragRect = getDragRect(activeDrag.start, current);
386
+ const clipRect = getDragClipRect(surfaceRef.current);
387
+ const gestureKeys = getIntersectingDateKeys(
388
+ dayCells.current,
389
+ dragRect,
390
+ activeDrag.cellBounds,
391
+ clipRect
392
+ );
393
+ const baseKeys = activeDrag.baseDates.map((date) => date.toString());
394
+ const previewKeys = toggleDateKeys(baseKeys, gestureKeys);
395
+ const hasMoved = activeDrag.hasMoved || hasPointerMoved(activeDrag.start, current);
396
+ scheduleDragPreviewUpdate({
397
+ ...activeDrag,
398
+ current,
399
+ previewKeys,
400
+ hasMoved
401
+ });
402
+ },
403
+ [multipleSelection, scheduleDragPreviewUpdate]
404
+ );
405
+ const handlePointerDownCapture = useCallback(
406
+ (event) => {
407
+ if (!multipleSelection || event.button !== 0 || !event.isPrimary) {
408
+ return;
409
+ }
410
+ const baseDates = getMultipleDates(calendar.selected);
411
+ event.preventDefault();
412
+ if (typeof document !== "undefined") {
413
+ document.getSelection()?.removeAllRanges();
414
+ }
415
+ surfaceRef.current = surfaceElementRef?.current ?? event.currentTarget;
416
+ surfaceRef.current?.setPointerCapture?.(event.pointerId);
417
+ acquireDragGestureLock();
418
+ const nextDragState = {
419
+ pointerId: event.pointerId,
420
+ start: getPointerPoint(event),
421
+ current: getPointerPoint(event),
422
+ baseDates,
423
+ previewKeys: baseDates.map((selectedDate) => selectedDate.toString()),
424
+ hasMoved: false,
425
+ startDayCell: getDayCellFromTarget(event.target),
426
+ cellBounds: snapshotCellBounds(dayCells.current)
427
+ };
428
+ dragStateRef.current = nextDragState;
429
+ setDragState(nextDragState);
430
+ setDragRectangle({
431
+ start: nextDragState.start,
432
+ current: nextDragState.current
433
+ });
434
+ },
435
+ [acquireDragGestureLock, calendar.selected, getDayCellFromTarget, multipleSelection, surfaceElementRef]
436
+ );
437
+ const handlePointerUp = useCallback(
438
+ (event) => {
439
+ const activeDrag = dragStateRef.current;
440
+ if (!multipleSelection || !activeDrag || event.pointerId !== activeDrag.pointerId) {
441
+ return;
442
+ }
443
+ if (dragPreviewFrameRef.current !== null) {
444
+ cancelAnimationFrame(dragPreviewFrameRef.current);
445
+ dragPreviewFrameRef.current = null;
446
+ setDragState(activeDrag);
447
+ }
448
+ event.preventDefault();
449
+ suppressClickRef.current = true;
450
+ setTimeout(() => {
451
+ suppressClickRef.current = false;
452
+ }, 0);
453
+ if (activeDrag.hasMoved) {
454
+ commitMultipleSelection(activeDrag.previewKeys, activeDrag.baseDates);
455
+ } else {
456
+ const dayCell = activeDrag.startDayCell;
457
+ if (dayCell && !dayCell.disabled) {
458
+ const selectedDates = getMultipleDates(calendar.selected);
459
+ const nextKeys = toggleDateKeys(
460
+ selectedDates.map((selectedDate) => selectedDate.toString()),
461
+ [dayCell.date.toString()]
462
+ );
463
+ commitMultipleSelection(nextKeys, selectedDates.concat(dayCell.date));
464
+ }
465
+ }
466
+ endDragGesture(event.pointerId);
467
+ const focused = document.activeElement;
468
+ if (focused instanceof HTMLElement && event.currentTarget.contains(focused)) {
469
+ focused.blur();
470
+ }
471
+ },
472
+ [calendar.selected, commitMultipleSelection, endDragGesture, getDayCellFromTarget, multipleSelection]
473
+ );
474
+ const handlePointerCancel = useCallback(
475
+ (event) => {
476
+ const activeDrag = dragStateRef.current;
477
+ if (!activeDrag || event.pointerId !== activeDrag.pointerId) {
478
+ return;
479
+ }
480
+ endDragGesture(event.pointerId);
481
+ },
482
+ [endDragGesture]
483
+ );
484
+ return useMemo2(() => {
485
+ if (!multipleSelection) {
486
+ return null;
487
+ }
488
+ return {
489
+ multipleSelection,
490
+ dragState,
491
+ dragRectangle,
492
+ previewSelectedKeys,
493
+ suppressClickRef,
494
+ surfaceRef,
495
+ registerDay,
496
+ handlePointerDownCapture,
497
+ handlePointerMove,
498
+ handlePointerUp,
499
+ handlePointerCancel
500
+ };
501
+ }, [
502
+ dragRectangle,
503
+ dragState,
504
+ handlePointerCancel,
505
+ handlePointerDownCapture,
506
+ handlePointerMove,
507
+ handlePointerUp,
508
+ multipleSelection,
509
+ previewSelectedKeys,
510
+ registerDay
511
+ ]);
512
+ }
513
+ function CalendarDragRectangleOverlay({
514
+ dragRectangle: dragRectangleProp,
515
+ surfaceRef: surfaceRefProp
516
+ } = {}) {
517
+ const drag = useOptionalCalendarPeriodDrag();
518
+ const dragRectangle = dragRectangleProp ?? drag?.dragRectangle ?? null;
519
+ const surfaceRef = surfaceRefProp ?? drag?.surfaceRef;
520
+ if (!dragRectangle) {
521
+ return null;
522
+ }
523
+ const surface = surfaceRef?.current;
524
+ if (!surface) {
525
+ return null;
526
+ }
527
+ const surfaceRect = surface.getBoundingClientRect();
528
+ const left = Math.min(dragRectangle.start.x, dragRectangle.current.x) - surfaceRect.left;
529
+ const top = Math.min(dragRectangle.start.y, dragRectangle.current.y) - surfaceRect.top;
530
+ const width = Math.abs(dragRectangle.current.x - dragRectangle.start.x);
531
+ const height = Math.abs(dragRectangle.current.y - dragRectangle.start.y);
532
+ return /* @__PURE__ */ jsx(
533
+ "div",
534
+ {
535
+ "aria-hidden": "true",
536
+ "calchemy-drag-rect": "",
537
+ style: {
538
+ position: "absolute",
539
+ left,
540
+ top,
541
+ width,
542
+ height,
543
+ pointerEvents: "none",
544
+ boxSizing: "border-box"
545
+ }
546
+ }
547
+ );
548
+ }
549
+ function CalendarPeriodDragProvider({ value, children }) {
550
+ return /* @__PURE__ */ jsx(CalendarPeriodDragContext.Provider, { value, children });
551
+ }
552
+ function getMultipleDates(value) {
553
+ return value?.kind === "multiple" ? value.dates : [];
554
+ }
555
+ function getSelectedDateKeys(value) {
556
+ return getMultipleDates(value).map((date) => date.toString());
557
+ }
558
+ function toggleDateKeys(baseKeys, toggledKeys) {
559
+ const next = new Set(baseKeys);
560
+ for (const key of toggledKeys) {
561
+ if (next.has(key)) {
562
+ next.delete(key);
563
+ } else {
564
+ next.add(key);
565
+ }
566
+ }
567
+ return Array.from(next).sort();
568
+ }
569
+ function getPointerPoint(event) {
570
+ return { x: event.clientX, y: event.clientY };
571
+ }
572
+ function hasPointerMoved(start, current) {
573
+ return Math.abs(start.x - current.x) > 2 || Math.abs(start.y - current.y) > 2;
574
+ }
575
+ function getDragRect(start, current) {
576
+ const left = Math.min(start.x, current.x);
577
+ const right = Math.max(start.x, current.x);
578
+ const top = Math.min(start.y, current.y);
579
+ const bottom = Math.max(start.y, current.y);
580
+ return {
581
+ left,
582
+ right,
583
+ top,
584
+ bottom,
585
+ x: left,
586
+ y: top,
587
+ width: right - left,
588
+ height: bottom - top,
589
+ toJSON: () => ({})
590
+ };
591
+ }
592
+ function snapshotCellBounds(cells) {
593
+ const bounds = /* @__PURE__ */ new Map();
594
+ for (const [key, cell] of cells) {
595
+ const rect = cell.element.getBoundingClientRect();
596
+ bounds.set(key, {
597
+ left: rect.left,
598
+ right: rect.right,
599
+ top: rect.top,
600
+ bottom: rect.bottom
601
+ });
602
+ }
603
+ return bounds;
604
+ }
605
+ function getDragClipRect(surface) {
606
+ if (!surface) {
607
+ return null;
608
+ }
609
+ const scrollContainer = surface.closest("[calchemy-scroll]");
610
+ if (!(scrollContainer instanceof HTMLElement)) {
611
+ return null;
612
+ }
613
+ const rect = scrollContainer.getBoundingClientRect();
614
+ return {
615
+ left: rect.left,
616
+ right: rect.right,
617
+ top: rect.top,
618
+ bottom: rect.bottom
619
+ };
620
+ }
621
+ function clipBoundsToRect(bounds, clip) {
622
+ const left = Math.max(bounds.left, clip.left);
623
+ const right = Math.min(bounds.right, clip.right);
624
+ const top = Math.max(bounds.top, clip.top);
625
+ const bottom = Math.min(bounds.bottom, clip.bottom);
626
+ if (left > right || top > bottom) {
627
+ return null;
628
+ }
629
+ return { left, right, top, bottom };
630
+ }
631
+ function getIntersectingDateKeys(cells, dragRect, cellBounds, clipRect = null) {
632
+ const clippedDragRect = clipRect ? clipBoundsToRect(dragRect, clipRect) : dragRect;
633
+ if (!clippedDragRect) {
634
+ return [];
635
+ }
636
+ return Array.from(cells.entries()).filter(([key, cell]) => {
637
+ const bounds = cellBounds.get(key);
638
+ if (!bounds || cell.disabled) {
639
+ return false;
640
+ }
641
+ const visibleBounds = clipRect ? clipBoundsToRect(bounds, clipRect) : bounds;
642
+ return visibleBounds && rectsIntersect(clippedDragRect, visibleBounds);
643
+ }).map(([key]) => key);
644
+ }
645
+ function rectsIntersect(left, right) {
646
+ return left.left <= right.right && left.right >= right.left && left.top <= right.bottom && left.bottom >= right.top;
647
+ }
648
+ function resolveDateKeys(keys, cells, fallbackDates) {
649
+ const fallbackByKey = new Map(fallbackDates.map((date) => [date.toString(), date]));
650
+ return Array.from(keys).sort().flatMap((key) => {
651
+ const date = cells.get(key)?.date ?? fallbackByKey.get(key);
652
+ return date ? [date] : [];
653
+ });
654
+ }
655
+
656
+ // src/components/calendar/date-model.ts
657
+ var defaultDateOrderPreference = ["DMY", "MDY", "YMD"];
658
+ function getFirstVisibleCalendarPeriod(calendar) {
659
+ const period = calendar.visiblePeriods[0];
660
+ if (!period) {
661
+ throw new Error("Calchemy.Calendar requires at least one visible generated period.");
662
+ }
663
+ return period;
664
+ }
665
+ function getCalendarDayState(calendar, period, date) {
666
+ const bounded = isDateWithinBounds(date, calendar.bounds);
667
+ const namedDates = getNamedDatesForDate(calendar, date);
668
+ const disabled = !bounded || (calendar.isDateDisabled?.(date, calendar) ?? false);
669
+ return {
670
+ outside: isBefore(date, period.start) || isAfter(date, period.end),
671
+ selected: isSelectedDate(calendar.selected, date),
672
+ today: calendar.today.equals(date),
673
+ weekend: isWeekend(date),
674
+ firstOfPeriod: date.equals(period.start),
675
+ lastOfPeriod: date.equals(period.end),
676
+ bounded,
677
+ disabled,
678
+ namedDates
679
+ };
680
+ }
681
+ function parseCalendarDuration(value, propName) {
682
+ const hasMonths = "months" in value;
683
+ const hasWeeks = "weeks" in value;
684
+ if (hasMonths === hasWeeks) {
685
+ throw new Error(`Calchemy.Calendar ${propName} must include exactly one of months or weeks.`);
686
+ }
687
+ const count = hasMonths ? value.months : value.weeks;
688
+ if (!Number.isInteger(count) || count < 1) {
689
+ throw new Error(`Calchemy.Calendar ${propName} must be a positive integer duration.`);
690
+ }
691
+ return {
692
+ unit: hasMonths ? "month" : "week",
693
+ count
694
+ };
695
+ }
696
+ function validateCalendarBounds(bounds) {
697
+ if (bounds?.start && bounds.end && isAfter(bounds.start, bounds.end)) {
698
+ throw new Error("Calchemy.Calendar bounds.start must be on or before bounds.end.");
699
+ }
700
+ }
701
+ function clampDateToBounds(date, bounds) {
702
+ if (bounds?.start && isBefore(date, bounds.start)) {
703
+ return bounds.start;
704
+ }
705
+ if (bounds?.end && isAfter(date, bounds.end)) {
706
+ return bounds.end;
707
+ }
708
+ return date;
709
+ }
710
+ function isDateWithinBounds(date, bounds) {
711
+ return (!bounds?.start || !isBefore(date, bounds.start)) && (!bounds?.end || !isAfter(date, bounds.end));
712
+ }
713
+ function periodIntersectsBounds(period, bounds) {
714
+ return (!bounds?.start || !isBefore(period.end, bounds.start)) && (!bounds?.end || !isAfter(period.start, bounds.end));
715
+ }
716
+ function getCalendarPeriodAtOffset(anchor, period, weekStartsOn, locale, offset) {
717
+ const firstStart = period.unit === "month" ? startOfMonth(anchor) : startOfWeek(anchor, weekStartsOn);
718
+ const start = addCalendarPeriod(firstStart, period.unit, offset);
719
+ const end = period.unit === "month" ? endOfMonth(start) : start.add({ days: 6 });
720
+ return {
721
+ id: `${period.unit}-${start.toString()}`,
722
+ unit: period.unit,
723
+ index: offset,
724
+ start,
725
+ end,
726
+ label: formatPeriodLabel(start, end, period.unit, locale)
727
+ };
728
+ }
729
+ function getInitialPeriodExtensions(period) {
730
+ return {
731
+ before: period.count * 6,
732
+ after: period.count * 3
733
+ };
734
+ }
735
+ function getSelectedValue(state) {
736
+ const resultValue = getResultValue(state);
737
+ return state.value ?? resultValue;
738
+ }
739
+ function getDateValueAnchor(value) {
740
+ if (!value) {
741
+ return null;
742
+ }
743
+ switch (value.kind) {
744
+ case "single":
745
+ return value.date;
746
+ case "range":
747
+ return value.start;
748
+ case "multiple":
749
+ return value.dates[0] ?? null;
750
+ }
751
+ }
752
+ function getDateValueKey(value) {
753
+ if (!value) {
754
+ return "";
755
+ }
756
+ switch (value.kind) {
757
+ case "single":
758
+ return `single:${value.date.toString()}`;
759
+ case "range":
760
+ return `range:${value.start.toString()}:${value.end.toString()}`;
761
+ case "multiple":
762
+ return `multiple:${value.dates.map((date) => date.toString()).join(",")}`;
763
+ }
764
+ }
765
+ function isDateInCalendarViewport(date, periods, visiblePeriodIndex, windowCount) {
766
+ return periods.filter(
767
+ (period) => period.index >= visiblePeriodIndex && period.index < visiblePeriodIndex + windowCount
768
+ ).some((period) => !isBefore(date, period.start) && !isAfter(date, period.end));
769
+ }
770
+ function getResultValue(state) {
771
+ return state.result.status === "valid" ? state.result.value : null;
772
+ }
773
+ function getToday(state) {
774
+ if (state.parseContext?.referenceDate) {
775
+ return state.parseContext.referenceDate;
776
+ }
777
+ const todayResult = state.calchemy.parseDate("today", state.parseContext);
778
+ if (todayResult.status === "valid" && todayResult.value.kind === "single") {
779
+ return todayResult.value.date;
780
+ }
781
+ return state.calchemy.Temporal.Now.plainDateISO(state.parseContext?.timeZone);
782
+ }
783
+ function buildCalendarPeriods(anchor, period, weekStartsOn, locale, extensions, bounds) {
784
+ const total = extensions.before + period.count + extensions.after;
785
+ return Array.from({ length: total }, (_, index) => {
786
+ const offset = index - extensions.before;
787
+ return getCalendarPeriodAtOffset(anchor, period, weekStartsOn, locale, offset);
788
+ }).filter((item) => periodIntersectsBounds(item, bounds));
789
+ }
790
+ function buildCalendarWeeks(period, weekStartsOn) {
791
+ const start = startOfWeek(period.start, weekStartsOn);
792
+ const end = endOfWeek(period.end, weekStartsOn);
793
+ const weeks = [];
794
+ let cursor = start;
795
+ while (!isAfter(cursor, end)) {
796
+ const week = Array.from({ length: 7 }, (_, index) => cursor.add({ days: index }));
797
+ weeks.push(week);
798
+ cursor = cursor.add({ days: 7 });
799
+ }
800
+ return weeks;
801
+ }
802
+ function buildWeekdays(calendar, weekdayFormat = "short") {
803
+ const sunday = startOfWeek(calendar.today, 0);
804
+ const first = startOfWeek(calendar.today, calendar.weekStartsOn);
805
+ return Array.from({ length: 7 }, (_, index) => {
806
+ const date = first.add({ days: index });
807
+ const weekdayIndex = sunday.until(date).days % 7;
808
+ return {
809
+ index: weekdayIndex,
810
+ label: date.toLocaleString(calendar.locale, { weekday: weekdayFormat }),
811
+ weekend: isWeekend(date)
812
+ };
813
+ });
814
+ }
815
+ function addCalendarPeriod(date, unit, count) {
816
+ return unit === "month" ? date.add({ months: count }) : date.add({ weeks: count });
817
+ }
818
+ function formatCalendarWindowLabel(periods, locale) {
819
+ const first = periods[0];
820
+ const last = periods.at(-1);
821
+ if (!first || !last) {
822
+ return "";
823
+ }
824
+ if (first.start.equals(last.start) && first.end.equals(last.end)) {
825
+ return first.label;
826
+ }
827
+ return `${formatDateLabel(first.start, locale)} - ${formatDateLabel(last.end, locale)}`;
828
+ }
829
+ function formatMonthLabel(date, locale) {
830
+ return date.toLocaleString(locale, { month: "long" });
831
+ }
832
+ function startOfMonth(date) {
833
+ return date.with({ day: 1 });
834
+ }
835
+ function endOfMonth(date) {
836
+ return startOfMonth(date).add({ months: 1 }).subtract({ days: 1 });
837
+ }
838
+ function startOfWeek(date, weekStartsOn) {
839
+ const temporalWeekday = weekStartsOn === 0 ? 7 : weekStartsOn;
840
+ let cursor = date;
841
+ while (cursor.dayOfWeek !== temporalWeekday) {
842
+ cursor = cursor.subtract({ days: 1 });
843
+ }
844
+ return cursor;
845
+ }
846
+ function endOfWeek(date, weekStartsOn) {
847
+ return startOfWeek(date, weekStartsOn).add({ days: 6 });
848
+ }
849
+ function isBefore(left, right) {
850
+ return left.toString() < right.toString();
851
+ }
852
+ function isAfter(left, right) {
853
+ return left.toString() > right.toString();
854
+ }
855
+ function isSelectedDate(value, date) {
856
+ if (!value) {
857
+ return false;
858
+ }
859
+ switch (value.kind) {
860
+ case "single":
861
+ return value.date.equals(date);
862
+ case "range":
863
+ return !isBefore(date, value.start) && !isAfter(date, value.end);
864
+ case "multiple":
865
+ return value.dates.some((selectedDate) => selectedDate.equals(date));
866
+ }
867
+ }
868
+ function isWeekend(date) {
869
+ return date.dayOfWeek === 6 || date.dayOfWeek === 7;
870
+ }
871
+ function getNamedDatesForDate(calendar, date) {
872
+ if (!calendar.namedDates) {
873
+ return [];
874
+ }
875
+ const context = getResolvedNamedDateContext(calendar);
876
+ return calendar.calchemy.calchemy.namedDatesVocabulary.filter((entry) => {
877
+ if (calendar.namedDates === "holidays" && !entry.isHoliday) {
878
+ return false;
879
+ }
880
+ return entry.resolveDate({ year: date.year, context })?.equals(date) ?? false;
881
+ });
882
+ }
883
+ function getResolvedNamedDateContext(calendar) {
884
+ const context = calendar.calchemy.parseContext;
885
+ return {
886
+ referenceDate: context?.referenceDate ?? calendar.calchemy.calchemy.Temporal.Now.plainDateISO(context?.timeZone),
887
+ locale: context?.locale ?? "en-US",
888
+ weekStartsOn: context?.weekStartsOn ?? 0,
889
+ dateOrderPreference: normalizeDateOrderPreference(context?.dateOrderPreference),
890
+ lastNDaysIncludesToday: context?.lastNDaysIncludesToday ?? true
891
+ };
892
+ }
893
+ function normalizeDateOrderPreference(value) {
894
+ if (!value || value.length === 0) {
895
+ return defaultDateOrderPreference;
896
+ }
897
+ return Array.from(new Set(value));
898
+ }
899
+ function formatPeriodLabel(start, end, unit, locale) {
900
+ if (unit === "month") {
901
+ return formatDateLabel(start, locale);
902
+ }
903
+ return `${start.toLocaleString(locale, { month: "short", day: "numeric" })} - ${end.toLocaleString(locale, {
904
+ month: "short",
905
+ day: "numeric",
906
+ year: "numeric"
907
+ })}`;
908
+ }
909
+ function formatDateLabel(date, locale) {
910
+ return date.toLocaleString(locale, { month: "long", year: "numeric" });
911
+ }
912
+
913
+ // src/components/calendar/CalendarGrid.tsx
914
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
915
+ var defaultCalendarWeekdayFormat = "short";
916
+ function CalendarWeekdays({
917
+ weekdayFormat = defaultCalendarWeekdayFormat,
918
+ ...props
919
+ }) {
920
+ const calendar = useCalchemyCalendar();
921
+ const weekdays = buildWeekdays(calendar, weekdayFormat);
922
+ return /* @__PURE__ */ jsx2("div", { ...props, "calchemy-days": "", children: weekdays.map((weekday) => /* @__PURE__ */ jsx2(
923
+ "div",
924
+ {
925
+ "calchemy-weekday": "",
926
+ "calchemy-weekend": weekday.weekend ? "" : void 0,
927
+ children: weekday.label
928
+ },
929
+ weekday.index
930
+ )) });
931
+ }
932
+ function CalendarGrid({
933
+ showBookends = false,
934
+ dragSelection = true,
935
+ style,
936
+ onPointerDownCapture,
937
+ onPointerMove,
938
+ onPointerUp,
939
+ onPointerCancel,
940
+ onDragStart,
941
+ ...props
942
+ }) {
943
+ const calendar = useCalchemyCalendar();
944
+ const period = useCalendarPeriod() ?? getFirstVisibleCalendarPeriod(calendar);
945
+ const weeks = buildCalendarWeeks(period, calendar.weekStartsOn);
946
+ const parentDrag = useOptionalCalendarPeriodDrag();
947
+ const localDrag = useCalendarPeriodDragSurface(
948
+ calendar.editable && dragSelection && parentDrag === null
949
+ );
950
+ const drag = parentDrag ?? localDrag;
951
+ const multipleSelection = Boolean(drag?.multipleSelection);
952
+ const useLocalDragHandlers = Boolean(drag && parentDrag === null);
953
+ const committedSelectedKeys = useMemo3(() => new Set(getSelectedDateKeys(calendar.selected)), [calendar.selected]);
954
+ const previewSelectedKeys = drag?.previewSelectedKeys ?? /* @__PURE__ */ new Set();
955
+ const dragState = drag?.dragState ?? null;
956
+ const dragPointerProps = mergeCalendarDragPointerProps(useLocalDragHandlers, drag, {
957
+ onPointerDownCapture,
958
+ onPointerMove,
959
+ onPointerUp,
960
+ onPointerCancel,
961
+ onDragStart
962
+ });
963
+ return /* @__PURE__ */ jsxs(
964
+ "div",
965
+ {
966
+ ...props,
967
+ "calchemy-grid": "",
968
+ "calchemy-multiple-drag": useLocalDragHandlers ? "" : void 0,
969
+ "calchemy-dragging": useLocalDragHandlers && dragState ? "" : void 0,
970
+ style: useLocalDragHandlers ? { ...multipleDragSurfaceStyle, ...style } : style,
971
+ ...dragPointerProps,
972
+ children: [
973
+ useLocalDragHandlers && drag?.dragRectangle ? /* @__PURE__ */ jsx2(CalendarDragRectangleOverlay, { dragRectangle: drag.dragRectangle, surfaceRef: drag.surfaceRef }) : null,
974
+ weeks.map((week) => /* @__PURE__ */ jsx2("div", { "calchemy-week": "", children: week.map((date) => {
975
+ const dayState = getCalendarDayState(calendar, period, date);
976
+ const namedDateLabels = dayState.namedDates.map((item) => item.value).join(", ");
977
+ const dateKey = date.toString();
978
+ const selected = multipleSelection ? dragState ? previewSelectedKeys.has(dateKey) : committedSelectedKeys.has(dateKey) : dayState.selected;
979
+ const dragPreview = Boolean(dragState && selected !== dayState.selected);
980
+ if (dayState.outside && !showBookends) {
981
+ return /* @__PURE__ */ jsx2(
982
+ "div",
983
+ {
984
+ "aria-hidden": "true",
985
+ "calchemy-cell": "",
986
+ "calchemy-blank": ""
987
+ },
988
+ date.toString()
989
+ );
990
+ }
991
+ return /* @__PURE__ */ jsx2(
992
+ "button",
993
+ {
994
+ type: "button",
995
+ ref: (element) => drag?.registerDay(element, date, dayState.disabled || !calendar.editable),
996
+ "calchemy-date": "",
997
+ "calchemy-selected": selected ? "" : void 0,
998
+ "calchemy-drag-preview": dragPreview ? "" : void 0,
999
+ "calchemy-drag-preview-selected": dragPreview && selected ? "" : void 0,
1000
+ "calchemy-drag-preview-deselected": dragPreview && !selected ? "" : void 0,
1001
+ "calchemy-today": dayState.today ? "" : void 0,
1002
+ "calchemy-weekend": dayState.weekend ? "" : void 0,
1003
+ "calchemy-outside": dayState.outside ? "" : void 0,
1004
+ "calchemy-first-of-period": dayState.firstOfPeriod ? "" : void 0,
1005
+ "calchemy-last-of-period": dayState.lastOfPeriod ? "" : void 0,
1006
+ "calchemy-disabled": dayState.disabled ? "" : void 0,
1007
+ "calchemy-out-of-bounds": !dayState.bounded ? "" : void 0,
1008
+ "calchemy-named-date": dayState.namedDates.length > 0 ? "" : void 0,
1009
+ "calchemy-holiday": dayState.namedDates.some((item) => item.isHoliday) ? "" : void 0,
1010
+ "calchemy-named-date-labels": namedDateLabels || void 0,
1011
+ "aria-disabled": !calendar.editable && !dayState.disabled ? true : void 0,
1012
+ disabled: dayState.disabled,
1013
+ onClick: () => {
1014
+ if (!calendar.editable) {
1015
+ return;
1016
+ }
1017
+ if (drag?.suppressClickRef.current) {
1018
+ drag.suppressClickRef.current = false;
1019
+ return;
1020
+ }
1021
+ if (!dayState.disabled) {
1022
+ if (multipleSelection) {
1023
+ const selectedDates = getMultipleDates(calendar.selected);
1024
+ const nextKeys = toggleDateKeys(
1025
+ selectedDates.map((selectedDate) => selectedDate.toString()),
1026
+ [dateKey]
1027
+ );
1028
+ const nextDates = nextKeys.flatMap((key) => {
1029
+ if (key === dateKey) {
1030
+ return [date];
1031
+ }
1032
+ const existing = selectedDates.find((selectedDate) => selectedDate.toString() === key);
1033
+ return existing ? [existing] : [];
1034
+ });
1035
+ calendar.selectValue({
1036
+ kind: "multiple",
1037
+ dates: nextDates
1038
+ });
1039
+ } else {
1040
+ calendar.selectDate(date);
1041
+ }
1042
+ }
1043
+ },
1044
+ children: date.day
1045
+ },
1046
+ date.toString()
1047
+ );
1048
+ }) }, week[0]?.toString()))
1049
+ ]
1050
+ }
1051
+ );
1052
+ }
1053
+
1054
+ // src/components/calendar/Calendar.tsx
1055
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1056
+ var defaultCalendarPeriod = { months: 1 };
1057
+ function Calendar({
1058
+ period = defaultCalendarPeriod,
1059
+ bounds,
1060
+ isDateDisabled,
1061
+ namedDates,
1062
+ children,
1063
+ ...divProps
1064
+ }) {
1065
+ const state = useCalchemyContext();
1066
+ const editable = state.inputMode === "calendar";
1067
+ validateCalendarBounds(bounds);
1068
+ const parsedPeriod = useMemo4(() => parseCalendarDuration(period, "period"), [period]);
1069
+ const selected = getSelectedValue(state);
1070
+ const today = getToday(state);
1071
+ const [periodAnchor, setPeriodAnchor] = useState4(
1072
+ () => clampDateToBounds(getDateValueAnchor(selected) ?? today, bounds)
1073
+ );
1074
+ const [navigationAnchor, setNavigationAnchor] = useState4(null);
1075
+ const [periodExtensions, setPeriodExtensions] = useState4(() => getInitialPeriodExtensions(parsedPeriod));
1076
+ const [visiblePeriodIndex, setScrolledVisiblePeriodIndex] = useState4(null);
1077
+ const prevInputValueRef = useRef2(state.inputValue);
1078
+ const prevSelectedKeyRef = useRef2(getDateValueKey(selected));
1079
+ const prevExpectedValueRef = useRef2(state.expectedValue);
1080
+ useEffect3(() => {
1081
+ if (prevExpectedValueRef.current === state.expectedValue) {
1082
+ return;
1083
+ }
1084
+ prevExpectedValueRef.current = state.expectedValue;
1085
+ setScrolledVisiblePeriodIndex(null);
1086
+ setNavigationAnchor(null);
1087
+ setPeriodAnchor(clampDateToBounds(getDateValueAnchor(selected) ?? today, bounds));
1088
+ prevInputValueRef.current = state.inputValue;
1089
+ prevSelectedKeyRef.current = getDateValueKey(selected);
1090
+ }, [bounds, selected, state.expectedValue, state.inputValue, today]);
1091
+ const periodAnchorKey = periodAnchor.toString();
1092
+ const activeVisiblePeriodIndex = visiblePeriodIndex?.anchor === periodAnchorKey && visiblePeriodIndex.inputValue === state.inputValue ? visiblePeriodIndex.index : 0;
1093
+ const weekStartsOn = state.parseContext?.weekStartsOn ?? 0;
1094
+ const locale = state.parseContext?.locale ?? "en-US";
1095
+ const periods = useMemo4(
1096
+ () => buildCalendarPeriods(periodAnchor, parsedPeriod, weekStartsOn, locale, periodExtensions, bounds),
1097
+ [periodAnchor, parsedPeriod.count, parsedPeriod.unit, weekStartsOn, locale, periodExtensions, bounds]
1098
+ );
1099
+ const visiblePeriods = useMemo4(
1100
+ () => {
1101
+ const visible = periods.filter(
1102
+ (item) => item.index >= activeVisiblePeriodIndex && item.index < activeVisiblePeriodIndex + parsedPeriod.count
1103
+ );
1104
+ return visible.length > 0 ? visible : periods.slice(0, parsedPeriod.count);
1105
+ },
1106
+ [periods, activeVisiblePeriodIndex, parsedPeriod.count]
1107
+ );
1108
+ const visiblePeriodAnchor = visiblePeriods[0]?.start ?? periodAnchor;
1109
+ useLayoutEffect(() => {
1110
+ const inputChanged = prevInputValueRef.current !== state.inputValue;
1111
+ prevInputValueRef.current = state.inputValue;
1112
+ const selectedKey = getDateValueKey(selected);
1113
+ const selectionChanged = prevSelectedKeyRef.current !== selectedKey;
1114
+ prevSelectedKeyRef.current = selectedKey;
1115
+ if (!inputChanged && !selectionChanged) {
1116
+ return;
1117
+ }
1118
+ if (selectionChanged && editable) {
1119
+ return;
1120
+ }
1121
+ const selectionAnchor = clampDateToBounds(getDateValueAnchor(selected) ?? today, bounds);
1122
+ const inputReflectsCalendarSelection = state.inputValue === selectionAnchor.toString();
1123
+ const shouldRevealSelection = inputChanged && !inputReflectsCalendarSelection;
1124
+ if (!shouldRevealSelection && isDateInCalendarViewport(
1125
+ selectionAnchor,
1126
+ periods,
1127
+ activeVisiblePeriodIndex,
1128
+ parsedPeriod.count
1129
+ )) {
1130
+ return;
1131
+ }
1132
+ setPeriodAnchor((current) => current.equals(selectionAnchor) ? current : selectionAnchor);
1133
+ setScrolledVisiblePeriodIndex(null);
1134
+ }, [
1135
+ activeVisiblePeriodIndex,
1136
+ bounds,
1137
+ editable,
1138
+ parsedPeriod.count,
1139
+ periods,
1140
+ selected,
1141
+ state.inputValue,
1142
+ today
1143
+ ]);
1144
+ function setCalendarPeriodAnchor(date) {
1145
+ const clamped = clampDateToBounds(date, bounds);
1146
+ setPeriodAnchor(clamped);
1147
+ setNavigationAnchor({ date: clamped, inputValue: state.inputValue });
1148
+ setPeriodExtensions(getInitialPeriodExtensions(parsedPeriod));
1149
+ setScrolledVisiblePeriodIndex(null);
1150
+ }
1151
+ function canMoveCalendar(unit, count) {
1152
+ const target = addCalendarPeriod(visiblePeriodAnchor, unit, count);
1153
+ return target.equals(clampDateToBounds(target, bounds));
1154
+ }
1155
+ function canExtendCalendarPeriods(direction, windows = 1) {
1156
+ if (!bounds) {
1157
+ return true;
1158
+ }
1159
+ const extendCount = parsedPeriod.count * windows;
1160
+ const firstIndex = periods[0]?.index ?? 0;
1161
+ const lastIndex = periods.at(-1)?.index ?? parsedPeriod.count - 1;
1162
+ const targetIndex = direction === "before" ? firstIndex - extendCount : lastIndex + 1;
1163
+ const targetPeriod = getCalendarPeriodAtOffset(periodAnchor, parsedPeriod, weekStartsOn, locale, targetIndex);
1164
+ return periodIntersectsBounds(targetPeriod, bounds);
1165
+ }
1166
+ const calendarState = useMemo4(
1167
+ () => ({
1168
+ calchemy: state,
1169
+ period: parsedPeriod,
1170
+ periodAnchor,
1171
+ visiblePeriodAnchor,
1172
+ today,
1173
+ selected,
1174
+ periods,
1175
+ visiblePeriods,
1176
+ weekStartsOn,
1177
+ locale,
1178
+ bounds,
1179
+ namedDates,
1180
+ editable,
1181
+ isDateDisabled,
1182
+ setPeriodAnchor: setCalendarPeriodAnchor,
1183
+ setVisiblePeriodIndex(index) {
1184
+ setScrolledVisiblePeriodIndex((current) => {
1185
+ if (current?.anchor === periodAnchorKey && current.inputValue === state.inputValue && current.index === index) {
1186
+ return current;
1187
+ }
1188
+ return { anchor: periodAnchorKey, inputValue: state.inputValue, index };
1189
+ });
1190
+ },
1191
+ canMove: canMoveCalendar,
1192
+ move(unit, count) {
1193
+ if (!canMoveCalendar(unit, count)) {
1194
+ return;
1195
+ }
1196
+ const clamped = clampDateToBounds(addCalendarPeriod(visiblePeriodAnchor, unit, count), bounds);
1197
+ setPeriodAnchor(clamped);
1198
+ setNavigationAnchor({
1199
+ date: clamped,
1200
+ inputValue: state.inputValue
1201
+ });
1202
+ setPeriodExtensions(getInitialPeriodExtensions(parsedPeriod));
1203
+ setScrolledVisiblePeriodIndex(null);
1204
+ },
1205
+ canExtendPeriods: canExtendCalendarPeriods,
1206
+ extendPeriods(direction, windows = 1) {
1207
+ if (!canExtendCalendarPeriods(direction, windows)) {
1208
+ return;
1209
+ }
1210
+ setPeriodExtensions((current) => ({
1211
+ ...current,
1212
+ [direction]: current[direction] + parsedPeriod.count * windows
1213
+ }));
1214
+ },
1215
+ selectDate(date) {
1216
+ if (!editable) {
1217
+ return;
1218
+ }
1219
+ if (state.expectedValue === "range") {
1220
+ const current = selected;
1221
+ if (current?.kind === "range" && current.start.equals(current.end)) {
1222
+ const start = isBefore(current.start, date) ? current.start : date;
1223
+ const end = isBefore(current.start, date) ? date : current.start;
1224
+ state.selectDate({ kind: "range", start, end });
1225
+ return;
1226
+ }
1227
+ state.selectDate({ kind: "range", start: date, end: date });
1228
+ return;
1229
+ }
1230
+ state.selectDate({ kind: "single", date });
1231
+ },
1232
+ selectValue(value) {
1233
+ if (!editable) {
1234
+ return;
1235
+ }
1236
+ state.selectDate(value);
1237
+ }
1238
+ }),
1239
+ [
1240
+ state,
1241
+ parsedPeriod,
1242
+ periodAnchor,
1243
+ periodAnchorKey,
1244
+ visiblePeriodAnchor,
1245
+ today,
1246
+ selected,
1247
+ periods,
1248
+ visiblePeriods,
1249
+ weekStartsOn,
1250
+ locale,
1251
+ bounds,
1252
+ namedDates,
1253
+ editable,
1254
+ isDateDisabled
1255
+ ]
1256
+ );
1257
+ const content = children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
1258
+ /* @__PURE__ */ jsxs2(CalendarHeader, { children: [
1259
+ /* @__PURE__ */ jsx3(CalendarPrevious, {}),
1260
+ /* @__PURE__ */ jsx3(CalendarHeading, {}),
1261
+ /* @__PURE__ */ jsx3(CalendarNext, {})
1262
+ ] }),
1263
+ /* @__PURE__ */ jsx3(CalendarWeekdays, {}),
1264
+ /* @__PURE__ */ jsx3(CalendarGrid, {})
1265
+ ] });
1266
+ return /* @__PURE__ */ jsx3(CalendarContext.Provider, { value: calendarState, children: /* @__PURE__ */ jsx3(
1267
+ "div",
1268
+ {
1269
+ ...divProps,
1270
+ "calchemy-calendar": "",
1271
+ "calchemy-editable": editable ? "" : void 0,
1272
+ style: {
1273
+ ...divProps.style,
1274
+ "--calchemy-calendar-period-count": parsedPeriod.count
1275
+ },
1276
+ children: content
1277
+ }
1278
+ ) });
1279
+ }
1280
+ function CalendarHeader(props) {
1281
+ return /* @__PURE__ */ jsx3("div", { ...props, "calchemy-header": "" });
1282
+ }
1283
+ function CalendarHeading(props) {
1284
+ const calendar = useCalchemyCalendar();
1285
+ return /* @__PURE__ */ jsx3("h2", { ...props, "calchemy-heading": "", children: props.children ?? formatCalendarWindowLabel(calendar.visiblePeriods, calendar.locale) });
1286
+ }
1287
+ function CalendarPrevious({
1288
+ onClick,
1289
+ children,
1290
+ ...props
1291
+ }) {
1292
+ return /* @__PURE__ */ jsx3(
1293
+ CalendarNavigationButton,
1294
+ {
1295
+ ...props,
1296
+ direction: -1,
1297
+ "calchemy-previous": "",
1298
+ onClick,
1299
+ children: children ?? "Previous"
1300
+ }
1301
+ );
1302
+ }
1303
+ function CalendarNext({
1304
+ onClick,
1305
+ children,
1306
+ ...props
1307
+ }) {
1308
+ return /* @__PURE__ */ jsx3(
1309
+ CalendarNavigationButton,
1310
+ {
1311
+ ...props,
1312
+ direction: 1,
1313
+ "calchemy-next": "",
1314
+ onClick,
1315
+ children: children ?? "Next"
1316
+ }
1317
+ );
1318
+ }
1319
+ function CalendarNavigationButton({
1320
+ direction,
1321
+ onClick,
1322
+ type = "button",
1323
+ ...props
1324
+ }) {
1325
+ const calendar = useCalchemyCalendar();
1326
+ const disabled = props.disabled ?? !calendar.canMove(calendar.period.unit, calendar.period.count * direction);
1327
+ function handleClick(event) {
1328
+ onClick?.(event);
1329
+ if (event.defaultPrevented || disabled) {
1330
+ return;
1331
+ }
1332
+ calendar.move(calendar.period.unit, calendar.period.count * direction);
1333
+ }
1334
+ return /* @__PURE__ */ jsx3("button", { ...props, type, disabled, onClick: handleClick });
1335
+ }
1336
+
1337
+ // src/components/calendar/CalendarPeriodHeading.tsx
1338
+ import { jsx as jsx4 } from "react/jsx-runtime";
1339
+ function CalendarPeriodHeading(props) {
1340
+ const calendar = useCalchemyCalendar();
1341
+ const period = useCalendarPeriod() ?? getFirstVisibleCalendarPeriod(calendar);
1342
+ return /* @__PURE__ */ jsx4("h3", { ...props, "calchemy-period-heading": "", children: props.children ?? period.label });
1343
+ }
1344
+
1345
+ // src/components/calendar/CalendarPeriod.tsx
1346
+ import { useContext as useContext2 } from "react";
1347
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1348
+ function CalendarPeriod({
1349
+ dragSelection = true,
1350
+ style,
1351
+ children,
1352
+ onPointerDownCapture,
1353
+ onPointerMove,
1354
+ onPointerUp,
1355
+ onPointerCancel,
1356
+ onDragStart,
1357
+ ...props
1358
+ }) {
1359
+ const period = useContext2(CalendarPeriodContext);
1360
+ const calendar = useCalchemyCalendar();
1361
+ const parentDrag = useOptionalCalendarPeriodDrag();
1362
+ const drag = useCalendarPeriodDragSurface(
1363
+ calendar.editable && dragSelection && parentDrag === null
1364
+ );
1365
+ const content = drag ? /* @__PURE__ */ jsxs3(CalendarPeriodDragProvider, { value: drag, children: [
1366
+ /* @__PURE__ */ jsx5(CalendarDragRectangleOverlay, {}),
1367
+ children
1368
+ ] }) : children;
1369
+ const dragPointerProps = mergeCalendarDragPointerProps(Boolean(drag), drag, {
1370
+ onPointerDownCapture,
1371
+ onPointerMove,
1372
+ onPointerUp,
1373
+ onPointerCancel,
1374
+ onDragStart
1375
+ });
1376
+ return /* @__PURE__ */ jsx5(
1377
+ "section",
1378
+ {
1379
+ ...props,
1380
+ "calchemy-period": "",
1381
+ "calchemy-period-id": period?.id,
1382
+ "calchemy-period-index": period?.index,
1383
+ "calchemy-multiple-drag": drag ? "" : void 0,
1384
+ "calchemy-dragging": drag?.dragState ? "" : void 0,
1385
+ style: drag ? { ...multipleDragSurfaceStyle, ...style } : parentDrag ? { touchAction: "none", ...style } : style,
1386
+ ...dragPointerProps,
1387
+ children: content
1388
+ }
1389
+ );
1390
+ }
1391
+
1392
+ // src/components/calendar/CalendarPeriodList.tsx
1393
+ import { useContext as useContext3, useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
1394
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1395
+ function CalendarPeriodList({
1396
+ children,
1397
+ dragSelection = true,
1398
+ style,
1399
+ onPointerDownCapture,
1400
+ onPointerMove,
1401
+ onPointerUp,
1402
+ onPointerCancel,
1403
+ onDragStart,
1404
+ ...props
1405
+ }) {
1406
+ const calendar = useCalchemyCalendar();
1407
+ const scrollContext = useContext3(CalendarScrollContext);
1408
+ const parentDrag = useOptionalCalendarPeriodDrag();
1409
+ const surfaceRef = useRef3(null);
1410
+ const hitCaptureRef = useRef3(null);
1411
+ const drag = useCalendarPeriodDragSurface(
1412
+ calendar.editable && dragSelection && parentDrag === null,
1413
+ surfaceRef
1414
+ );
1415
+ const periods = scrollContext ? calendar.periods : calendar.visiblePeriods;
1416
+ useLayoutEffect2(() => {
1417
+ if (!drag) {
1418
+ return;
1419
+ }
1420
+ const surface = surfaceRef.current;
1421
+ const hitCapture = hitCaptureRef.current;
1422
+ if (!surface || !hitCapture) {
1423
+ return;
1424
+ }
1425
+ const updateDragHitBounds = () => {
1426
+ const periodElements = surface.querySelectorAll("[calchemy-period]");
1427
+ if (periodElements.length === 0) {
1428
+ hitCapture.style.removeProperty("inline-size");
1429
+ hitCapture.style.removeProperty("block-size");
1430
+ return;
1431
+ }
1432
+ let maxInlineEnd = 0;
1433
+ let maxBlockEnd = 0;
1434
+ for (const periodElement of periodElements) {
1435
+ maxInlineEnd = Math.max(maxInlineEnd, periodElement.offsetLeft + periodElement.offsetWidth);
1436
+ maxBlockEnd = Math.max(maxBlockEnd, periodElement.offsetTop + periodElement.offsetHeight);
1437
+ }
1438
+ hitCapture.style.inlineSize = `${maxInlineEnd}px`;
1439
+ hitCapture.style.blockSize = `${maxBlockEnd}px`;
1440
+ };
1441
+ updateDragHitBounds();
1442
+ if (typeof ResizeObserver === "undefined") {
1443
+ return;
1444
+ }
1445
+ const resizeObserver = new ResizeObserver(updateDragHitBounds);
1446
+ resizeObserver.observe(surface);
1447
+ for (const periodElement of surface.querySelectorAll("[calchemy-period]")) {
1448
+ resizeObserver.observe(periodElement);
1449
+ }
1450
+ return () => {
1451
+ resizeObserver.disconnect();
1452
+ };
1453
+ }, [drag, periods]);
1454
+ const spacerStyle = scrollContext?.direction === "horizontal" ? { gridColumn: `span ${scrollContext.leadingSpacerPeriodCount}` } : { blockSize: `${scrollContext?.leadingSpacerPixelSize ?? 0}px` };
1455
+ const periodContent = /* @__PURE__ */ jsxs4(Fragment2, { children: [
1456
+ scrollContext && scrollContext.leadingSpacerPeriodCount > 0 ? /* @__PURE__ */ jsx6(
1457
+ "div",
1458
+ {
1459
+ "aria-hidden": "true",
1460
+ "calchemy-scroll-spacer": "",
1461
+ style: spacerStyle
1462
+ }
1463
+ ) : null,
1464
+ periods.map((period) => /* @__PURE__ */ jsx6(CalendarPeriodContext.Provider, { value: period, children: children ?? /* @__PURE__ */ jsxs4(CalendarPeriod, { children: [
1465
+ /* @__PURE__ */ jsx6(CalendarPeriodHeading, {}),
1466
+ /* @__PURE__ */ jsx6(CalendarWeekdays, {}),
1467
+ /* @__PURE__ */ jsx6(CalendarGrid, {})
1468
+ ] }) }, period.id))
1469
+ ] });
1470
+ const content = drag ? /* @__PURE__ */ jsxs4(CalendarPeriodDragProvider, { value: drag, children: [
1471
+ /* @__PURE__ */ jsx6(
1472
+ "div",
1473
+ {
1474
+ ref: hitCaptureRef,
1475
+ "aria-hidden": "true",
1476
+ style: {
1477
+ position: "absolute",
1478
+ top: 0,
1479
+ left: 0,
1480
+ zIndex: 0,
1481
+ pointerEvents: "auto",
1482
+ touchAction: "none"
1483
+ }
1484
+ }
1485
+ ),
1486
+ /* @__PURE__ */ jsx6(CalendarDragRectangleOverlay, {}),
1487
+ periodContent
1488
+ ] }) : periodContent;
1489
+ const dragPointerProps = mergeCalendarDragPointerProps(Boolean(drag), drag, {
1490
+ onPointerDownCapture,
1491
+ onPointerMove,
1492
+ onPointerUp,
1493
+ onPointerCancel,
1494
+ onDragStart
1495
+ });
1496
+ return /* @__PURE__ */ jsx6(
1497
+ "div",
1498
+ {
1499
+ ...props,
1500
+ ref: surfaceRef,
1501
+ "calchemy-period-list": "",
1502
+ "calchemy-multiple-drag": drag ? "" : void 0,
1503
+ "calchemy-dragging": drag?.dragState ? "" : void 0,
1504
+ style: drag ? { ...multiplePeriodListDragSurfaceStyle, ...style } : style,
1505
+ ...dragPointerProps,
1506
+ children: content
1507
+ }
1508
+ );
1509
+ }
1510
+
1511
+ // src/components/calendar/CalendarSelects.tsx
1512
+ import { jsx as jsx7 } from "react/jsx-runtime";
1513
+ function CalendarMonthSelect({ onChange, ...props }) {
1514
+ const calendar = useCalchemyCalendar();
1515
+ const months = Array.from({ length: 12 }, (_, index) => index + 1).filter(
1516
+ (month) => isMonthWithinBounds(calendar.visiblePeriodAnchor.with({ month, day: 1 }), calendar)
1517
+ );
1518
+ return /* @__PURE__ */ jsx7(
1519
+ "select",
1520
+ {
1521
+ ...props,
1522
+ "calchemy-month-select": "",
1523
+ value: String(calendar.visiblePeriodAnchor.month),
1524
+ onChange: (event) => handleCalendarSelectChange(
1525
+ event,
1526
+ onChange,
1527
+ (value) => calendar.setPeriodAnchor(calendar.visiblePeriodAnchor.with({ month: value, day: 1 }))
1528
+ ),
1529
+ children: months.map((month) => {
1530
+ const date = calendar.visiblePeriodAnchor.with({ month, day: 1 });
1531
+ return /* @__PURE__ */ jsx7("option", { value: String(month), children: formatMonthLabel(date, calendar.locale) }, month);
1532
+ })
1533
+ }
1534
+ );
1535
+ }
1536
+ function CalendarYearSelect({ startYear, endYear, onChange, ...props }) {
1537
+ const calendar = useCalchemyCalendar();
1538
+ const visibleYear = calendar.visiblePeriodAnchor.year;
1539
+ const firstYear = Math.min(calendar.bounds?.start?.year ?? startYear ?? visibleYear - 100, visibleYear);
1540
+ const lastYear = Math.max(calendar.bounds?.end?.year ?? endYear ?? visibleYear + 100, visibleYear);
1541
+ return /* @__PURE__ */ jsx7(
1542
+ "select",
1543
+ {
1544
+ ...props,
1545
+ "calchemy-year-select": "",
1546
+ value: String(visibleYear),
1547
+ onChange: (event) => handleCalendarSelectChange(
1548
+ event,
1549
+ onChange,
1550
+ (value) => calendar.setPeriodAnchor(calendar.visiblePeriodAnchor.with({ year: value, day: 1 }))
1551
+ ),
1552
+ children: Array.from({ length: lastYear - firstYear + 1 }, (_, index) => firstYear + index).map((year) => /* @__PURE__ */ jsx7("option", { value: String(year), children: year }, year))
1553
+ }
1554
+ );
1555
+ }
1556
+ function handleCalendarSelectChange(event, onChange, updatePeriodAnchor) {
1557
+ onChange?.(event);
1558
+ if (event.defaultPrevented) {
1559
+ return;
1560
+ }
1561
+ updatePeriodAnchor(Number(event.currentTarget.value));
1562
+ }
1563
+ function isMonthWithinBounds(monthStart, calendar) {
1564
+ const monthEnd = monthStart.add({ months: 1 }).subtract({ days: 1 });
1565
+ return (!calendar.bounds?.start || !isBefore(monthEnd, calendar.bounds.start)) && (!calendar.bounds?.end || !isAfter(monthStart, calendar.bounds.end));
1566
+ }
1567
+
1568
+ // src/components/Calchemy.tsx
1569
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1570
+ function Root(props) {
1571
+ const { children, ...options } = props;
1572
+ const state = useCalchemy(options);
1573
+ return /* @__PURE__ */ jsx8(CalchemyContext.Provider, { value: state, children });
1574
+ }
1575
+ function Field({
1576
+ renderInlineCompletion = true,
1577
+ readOnly,
1578
+ ...props
1579
+ }) {
1580
+ const state = useCalchemyContext();
1581
+ const inputProps = state.getInputProps();
1582
+ const completion = state.inputMode === "field" ? state.inlineCompletion : null;
1583
+ return /* @__PURE__ */ jsxs5(
1584
+ "span",
1585
+ {
1586
+ "calchemy-field": "",
1587
+ "calchemy-input-mode": state.inputMode,
1588
+ "calchemy-has-completion": completion ? "" : void 0,
1589
+ children: [
1590
+ renderInlineCompletion && completion ? /* @__PURE__ */ jsxs5("span", { "calchemy-field-backdrop": "", "aria-hidden": "true", children: [
1591
+ /* @__PURE__ */ jsx8("span", { "calchemy-field-typed": "", children: state.inputValue }),
1592
+ /* @__PURE__ */ jsx8("span", { "calchemy-completions": "", children: completion.suffix })
1593
+ ] }) : null,
1594
+ /* @__PURE__ */ jsx8("input", { ...props, ...inputProps, readOnly: readOnly ?? inputProps.readOnly })
1595
+ ]
1596
+ }
1597
+ );
1598
+ }
1599
+ function InputMode({
1600
+ fieldLabel = "Type",
1601
+ calendarLabel = "Pick",
1602
+ ...props
1603
+ }) {
1604
+ const state = useCalchemyContext();
1605
+ return /* @__PURE__ */ jsxs5("div", { ...props, "calchemy-mode": "", "calchemy-active": state.inputMode, role: "group", children: [
1606
+ /* @__PURE__ */ jsx8(
1607
+ "button",
1608
+ {
1609
+ type: "button",
1610
+ "calchemy-value": "field",
1611
+ "aria-pressed": state.inputMode === "field",
1612
+ onClick: () => state.setInputMode("field"),
1613
+ children: fieldLabel
1614
+ }
1615
+ ),
1616
+ /* @__PURE__ */ jsx8(
1617
+ "button",
1618
+ {
1619
+ type: "button",
1620
+ "calchemy-value": "calendar",
1621
+ "aria-pressed": state.inputMode === "calendar",
1622
+ onClick: () => state.setInputMode("calendar"),
1623
+ children: calendarLabel
1624
+ }
1625
+ )
1626
+ ] });
1627
+ }
1628
+ function Candidates(props) {
1629
+ const state = useCalchemyContext();
1630
+ if (state.inputMode !== "field" || state.result.status !== "ambiguous") {
1631
+ return null;
1632
+ }
1633
+ const candidates = state.expectedValue && state.expectedValue !== "multiple" ? state.result.candidates.filter(
1634
+ (candidate) => candidate.value.kind === state.expectedValue
1635
+ ) : state.result.candidates;
1636
+ if (candidates.length === 0) {
1637
+ return null;
1638
+ }
1639
+ return /* @__PURE__ */ jsx8("div", { ...props, "calchemy-candidates": "", children: candidates.map((candidate) => /* @__PURE__ */ jsx8(
1640
+ "button",
1641
+ {
1642
+ type: "button",
1643
+ "calchemy-candidate": "",
1644
+ onClick: () => state.selectCandidate(candidate.id),
1645
+ children: candidate.label
1646
+ },
1647
+ candidate.id
1648
+ )) });
1649
+ }
1650
+ var Calchemy = {
1651
+ Root,
1652
+ Field,
1653
+ InputMode,
1654
+ Candidates,
1655
+ Calendar,
1656
+ CalendarHeader,
1657
+ CalendarHeading,
1658
+ CalendarPrevious,
1659
+ CalendarNext,
1660
+ CalendarPeriodList,
1661
+ CalendarPeriod,
1662
+ CalendarPeriodHeading,
1663
+ CalendarWeekdays,
1664
+ CalendarGrid,
1665
+ CalendarMonthSelect,
1666
+ CalendarYearSelect
1667
+ };
1668
+ export {
1669
+ Calchemy,
1670
+ useCalchemy,
1671
+ useCalchemyCalendar,
1672
+ useCalchemyContext
1673
+ };
3
1674
  //# sourceMappingURL=index.js.map