@dxos/react-ui-calendar 0.9.0 → 0.9.1-main.c7dcc2e112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/lib/browser/index.mjs +582 -107
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +582 -107
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Calendar/Calendar.d.ts +25 -35
  8. package/dist/types/src/components/Calendar/Calendar.d.ts.map +1 -1
  9. package/dist/types/src/components/Calendar/Calendar.stories.d.ts +2 -0
  10. package/dist/types/src/components/Calendar/Calendar.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Calendar/Week.d.ts +30 -0
  12. package/dist/types/src/components/Calendar/Week.d.ts.map +1 -0
  13. package/dist/types/src/components/Calendar/Weekdays.d.ts +18 -0
  14. package/dist/types/src/components/Calendar/Weekdays.d.ts.map +1 -0
  15. package/dist/types/src/components/Calendar/context.d.ts +40 -0
  16. package/dist/types/src/components/Calendar/context.d.ts.map +1 -0
  17. package/dist/types/src/components/Calendar/util.d.ts +36 -0
  18. package/dist/types/src/components/Calendar/util.d.ts.map +1 -1
  19. package/dist/types/src/components/Calendar/util.test.d.ts +2 -0
  20. package/dist/types/src/components/Calendar/util.test.d.ts.map +1 -0
  21. package/dist/types/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +12 -12
  23. package/src/components/Calendar/Calendar.stories.tsx +43 -3
  24. package/src/components/Calendar/Calendar.tsx +117 -96
  25. package/src/components/Calendar/Week.tsx +488 -0
  26. package/src/components/Calendar/Weekdays.tsx +57 -0
  27. package/src/components/Calendar/context.ts +60 -0
  28. package/src/components/Calendar/util.test.ts +90 -0
  29. package/src/components/Calendar/util.ts +92 -1
@@ -1,17 +1,21 @@
1
1
  // src/components/Calendar/Calendar.tsx
2
- import { createContext } from "@radix-ui/react-context";
3
- import { addDays, format, startOfDay, startOfWeek } from "date-fns";
4
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { addDays as addDays3, format as format2, startOfDay as startOfDay3 } from "date-fns";
3
+ import React3, { forwardRef, useCallback as useCallback2, useEffect as useEffect2, useImperativeHandle, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
5
4
  import { useResizeDetector } from "react-resize-detector";
6
5
  import { List } from "react-virtualized";
7
6
  import { Event } from "@dxos/async";
8
7
  import { IconButton, useTranslation } from "@dxos/react-ui";
9
- import { composable, composableProps } from "@dxos/react-ui";
10
- import { mx } from "@dxos/ui-theme";
8
+ import { composable as composable2, composableProps as composableProps2 } from "@dxos/react-ui";
9
+ import { mx as mx3 } from "@dxos/ui-theme";
11
10
  import { translationKey } from "#translations";
12
11
 
12
+ // src/components/Calendar/context.ts
13
+ import { createContext } from "@radix-ui/react-context";
14
+ var [CalendarContextProvider, useCalendarContext] = createContext("Calendar");
15
+
13
16
  // src/components/Calendar/util.ts
14
- import { differenceInCalendarDays } from "date-fns";
17
+ import { differenceInCalendarDays, startOfDay } from "date-fns";
18
+ var gridEpoch = /* @__PURE__ */ new Date("1970-01-01");
15
19
  var getDate = (start2, weekNumber, dayOfWeek, weekStartsOn) => {
16
20
  const result = new Date(start2);
17
21
  const startDayOfWeek = start2.getDay();
@@ -29,17 +33,473 @@ var getRowIndex = (start2, date, weekStartsOn) => {
29
33
  var isSameDay = (date1, date2) => {
30
34
  return !!date2 && date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
31
35
  };
36
+ var MINUTES_PER_DAY = 24 * 60;
37
+ var SNAP_MINUTES = 15;
38
+ var minutesOfDay = (date) => (date.getTime() - startOfDay(date).getTime()) / 6e4;
39
+ var setMinutesOfDay = (date, minutes) => new Date(startOfDay(date).getTime() + minutes * 6e4);
40
+ var yToMinutes = (y, hourHeight) => y / hourHeight * 60;
41
+ var minutesToY = (minutes, hourHeight) => minutes / 60 * hourHeight;
42
+ var snapMinutes = (minutes, step = SNAP_MINUTES) => {
43
+ const snapped = Math.round(minutes / step) * step;
44
+ return Math.max(0, Math.min(MINUTES_PER_DAY, snapped));
45
+ };
46
+ var layoutDayEvents = (events) => {
47
+ const layout = /* @__PURE__ */ new Map();
48
+ const ordered = events.map((event, index) => ({
49
+ index,
50
+ start: event.start.getTime(),
51
+ end: event.end.getTime()
52
+ })).sort((a, b) => a.start - b.start);
53
+ let cluster = [];
54
+ let clusterMax = -Infinity;
55
+ const flush = () => {
56
+ if (cluster.length === 0) {
57
+ return;
58
+ }
59
+ const columnEnds = [];
60
+ const assigned = cluster.map(({ index, start: start2, end }) => {
61
+ let column = columnEnds.findIndex((columnEnd) => columnEnd <= start2);
62
+ if (column === -1) {
63
+ column = columnEnds.length;
64
+ }
65
+ columnEnds[column] = end;
66
+ return {
67
+ index,
68
+ column
69
+ };
70
+ });
71
+ const columnCount = columnEnds.length;
72
+ for (const { index, column } of assigned) {
73
+ layout.set(index, {
74
+ columnIndex: column,
75
+ columnCount
76
+ });
77
+ }
78
+ cluster = [];
79
+ clusterMax = -Infinity;
80
+ };
81
+ for (const entry of ordered) {
82
+ if (cluster.length > 0 && entry.start >= clusterMax) {
83
+ flush();
84
+ }
85
+ cluster.push(entry);
86
+ clusterMax = Math.max(clusterMax, entry.end);
87
+ }
88
+ flush();
89
+ return layout;
90
+ };
91
+
92
+ // src/components/Calendar/Week.tsx
93
+ import { addDays as addDays2, startOfDay as startOfDay2, startOfWeek as startOfWeek2 } from "date-fns";
94
+ import React2, { useCallback, useEffect, useLayoutEffect, useMemo as useMemo2, useRef, useState } from "react";
95
+ import { composable, composableProps } from "@dxos/react-ui";
96
+ import { mx as mx2 } from "@dxos/ui-theme";
97
+
98
+ // src/components/Calendar/Weekdays.tsx
99
+ import { addDays, format, startOfWeek } from "date-fns";
100
+ import React, { useMemo } from "react";
101
+ import { mx } from "@dxos/ui-theme";
102
+ var Weekdays = ({ weekStartsOn, columnWidth, gutter, dates }) => {
103
+ const labels = useMemo(() => {
104
+ const weekStart = startOfWeek(/* @__PURE__ */ new Date(), {
105
+ weekStartsOn
106
+ });
107
+ return Array.from({
108
+ length: 7
109
+ }, (_, index) => format(addDays(weekStart, index), "EEE"));
110
+ }, [
111
+ weekStartsOn
112
+ ]);
113
+ const today = useMemo(() => /* @__PURE__ */ new Date(), []);
114
+ const columnTemplate = columnWidth ? `repeat(7, ${columnWidth}px)` : "repeat(7, 1fr)";
115
+ return /* @__PURE__ */ React.createElement("div", {
116
+ className: "grid w-full shrink-0",
117
+ style: {
118
+ gridTemplateColumns: gutter ? `${gutter}px ${columnTemplate}` : columnTemplate
119
+ }
120
+ }, gutter != null && /* @__PURE__ */ React.createElement("div", {
121
+ "aria-hidden": true
122
+ }), labels.map((label, index) => {
123
+ const date = dates?.[index];
124
+ const isToday = !!date && isSameDay(date, today);
125
+ return /* @__PURE__ */ React.createElement("div", {
126
+ key: index,
127
+ className: mx("flex flex-col items-center p-2 text-sm font-thin", isToday && "text-accent-text")
128
+ }, /* @__PURE__ */ React.createElement("span", null, label), date && /* @__PURE__ */ React.createElement("span", {
129
+ className: "text-lg font-normal tabular-nums"
130
+ }, date.getDate()));
131
+ }));
132
+ };
133
+
134
+ // src/components/Calendar/Week.tsx
135
+ var CALENDAR_WEEK_NAME = "CalendarWeek";
136
+ var HOUR_HEIGHT = 48;
137
+ var GUTTER_WIDTH = 56;
138
+ var RESIZE_HANDLE = 6;
139
+ var MIN_DURATION = SNAP_MINUTES;
140
+ var INITIAL_HOUR = 8;
141
+ var CalendarWeek = composable(({ classNames, date, events = [], onEventCreate, onEventUpdate, ...props }, forwardedRef) => {
142
+ const { weekStartsOn, event: scrollEvent, setIndex } = useCalendarContext(CALENDAR_WEEK_NAME);
143
+ const today = useMemo2(() => /* @__PURE__ */ new Date(), []);
144
+ const [viewDate, setViewDate] = useState(() => date ?? today);
145
+ useEffect(() => {
146
+ if (date) {
147
+ setViewDate(date);
148
+ }
149
+ }, [
150
+ date
151
+ ]);
152
+ useEffect(() => {
153
+ return scrollEvent.on(({ date: date2 }) => setViewDate(date2));
154
+ }, [
155
+ scrollEvent
156
+ ]);
157
+ const weekDays = useMemo2(() => {
158
+ const weekStart = startOfWeek2(viewDate, {
159
+ weekStartsOn
160
+ });
161
+ return Array.from({
162
+ length: 7
163
+ }, (_, index) => startOfDay2(addDays2(weekStart, index)));
164
+ }, [
165
+ viewDate,
166
+ weekStartsOn
167
+ ]);
168
+ useEffect(() => {
169
+ setIndex(getRowIndex(gridEpoch, weekDays[0], weekStartsOn));
170
+ }, [
171
+ weekDays,
172
+ weekStartsOn,
173
+ setIndex
174
+ ]);
175
+ const eventsByDay = useMemo2(() => {
176
+ const byDay = weekDays.map(() => []);
177
+ for (const event of events) {
178
+ const dayIndex = weekDays.findIndex((day) => isSameDay(day, event.start));
179
+ if (dayIndex >= 0) {
180
+ byDay[dayIndex].push(event);
181
+ }
182
+ }
183
+ return byDay;
184
+ }, [
185
+ events,
186
+ weekDays
187
+ ]);
188
+ const scrollRef = useRef(null);
189
+ useLayoutEffect(() => {
190
+ scrollRef.current?.scrollTo({
191
+ top: minutesToY(INITIAL_HOUR * 60, HOUR_HEIGHT)
192
+ });
193
+ }, []);
194
+ const gestureRef = useRef(void 0);
195
+ const columnsRef = useRef([]);
196
+ const [draft, setDraft] = useState(void 0);
197
+ const pointerMinutes = useCallback((clientY) => {
198
+ const rect = columnsRef.current.find(Boolean)?.getBoundingClientRect();
199
+ if (!rect) {
200
+ return 0;
201
+ }
202
+ return Math.max(0, Math.min(MINUTES_PER_DAY, yToMinutes(clientY - rect.top, HOUR_HEIGHT)));
203
+ }, []);
204
+ const dayFromX = useCallback((clientX) => {
205
+ const index = columnsRef.current.findIndex((node) => {
206
+ const rect = node?.getBoundingClientRect();
207
+ return rect && clientX >= rect.left && clientX < rect.right;
208
+ });
209
+ return index >= 0 ? weekDays[index] : void 0;
210
+ }, [
211
+ weekDays
212
+ ]);
213
+ const applyGesture = useCallback((clientX, clientY) => {
214
+ const gesture = gestureRef.current;
215
+ if (!gesture) {
216
+ return void 0;
217
+ }
218
+ const { kind, day, eventId, anchorMinutes, grabOffset, durationMinutes } = gesture;
219
+ const raw = pointerMinutes(clientY);
220
+ switch (kind) {
221
+ case "create": {
222
+ const focus = snapMinutes(raw);
223
+ const from = Math.min(anchorMinutes, focus);
224
+ const to = Math.max(anchorMinutes, focus);
225
+ const end = Math.max(to, from + MIN_DURATION);
226
+ return {
227
+ eventId,
228
+ start: setMinutesOfDay(day, from),
229
+ end: setMinutesOfDay(day, end)
230
+ };
231
+ }
232
+ case "move": {
233
+ const targetDay = dayFromX(clientX) ?? day;
234
+ let start2 = snapMinutes(raw - grabOffset);
235
+ start2 = Math.max(0, Math.min(MINUTES_PER_DAY - durationMinutes, start2));
236
+ return {
237
+ eventId,
238
+ start: setMinutesOfDay(targetDay, start2),
239
+ end: setMinutesOfDay(targetDay, start2 + durationMinutes)
240
+ };
241
+ }
242
+ case "resize-start": {
243
+ const start2 = Math.min(snapMinutes(raw), anchorMinutes - MIN_DURATION);
244
+ return {
245
+ eventId,
246
+ start: setMinutesOfDay(day, start2),
247
+ end: setMinutesOfDay(day, anchorMinutes)
248
+ };
249
+ }
250
+ case "resize-end": {
251
+ const end = Math.max(snapMinutes(raw), anchorMinutes + MIN_DURATION);
252
+ return {
253
+ eventId,
254
+ start: setMinutesOfDay(day, anchorMinutes),
255
+ end: setMinutesOfDay(day, end)
256
+ };
257
+ }
258
+ }
259
+ }, [
260
+ dayFromX,
261
+ pointerMinutes
262
+ ]);
263
+ const callbacksRef = useRef({
264
+ onEventCreate,
265
+ onEventUpdate
266
+ });
267
+ callbacksRef.current = {
268
+ onEventCreate,
269
+ onEventUpdate
270
+ };
271
+ const detachRef = useRef(() => {
272
+ });
273
+ const beginGesture = useCallback((gesture, ev) => {
274
+ ev.preventDefault();
275
+ ev.stopPropagation();
276
+ gestureRef.current = gesture;
277
+ setDraft(applyGesture(ev.clientX, ev.clientY));
278
+ const handleMove = (moveEv) => {
279
+ if (gestureRef.current) {
280
+ setDraft(applyGesture(moveEv.clientX, moveEv.clientY));
281
+ }
282
+ };
283
+ const handleUp = (upEv) => {
284
+ const active = gestureRef.current;
285
+ const result = applyGesture(upEv.clientX, upEv.clientY);
286
+ detachRef.current();
287
+ gestureRef.current = void 0;
288
+ setDraft(void 0);
289
+ if (active && result) {
290
+ if (active.kind === "create") {
291
+ callbacksRef.current.onEventCreate?.({
292
+ start: result.start,
293
+ end: result.end
294
+ });
295
+ } else if (result.eventId) {
296
+ callbacksRef.current.onEventUpdate?.({
297
+ id: result.eventId,
298
+ start: result.start,
299
+ end: result.end
300
+ });
301
+ }
302
+ }
303
+ };
304
+ detachRef.current = () => {
305
+ window.removeEventListener("pointermove", handleMove);
306
+ window.removeEventListener("pointerup", handleUp);
307
+ window.removeEventListener("pointercancel", handleUp);
308
+ };
309
+ window.addEventListener("pointermove", handleMove);
310
+ window.addEventListener("pointerup", handleUp);
311
+ window.addEventListener("pointercancel", handleUp);
312
+ }, [
313
+ applyGesture
314
+ ]);
315
+ useEffect(() => () => detachRef.current(), []);
316
+ const handleColumnPointerDown = useCallback((day, event) => {
317
+ const rect = event.currentTarget.getBoundingClientRect();
318
+ const anchor = snapMinutes(yToMinutes(event.clientY - rect.top, HOUR_HEIGHT));
319
+ beginGesture({
320
+ kind: "create",
321
+ day,
322
+ anchorMinutes: anchor,
323
+ grabOffset: 0,
324
+ durationMinutes: 0
325
+ }, event);
326
+ }, [
327
+ beginGesture
328
+ ]);
329
+ const renderEvent = useCallback((event, day, start2, end, columnIndex, columnCount) => /* @__PURE__ */ React2.createElement(EventBlock, {
330
+ key: event.id,
331
+ event,
332
+ start: start2,
333
+ end,
334
+ columnIndex,
335
+ columnCount,
336
+ onMoveStart: (ev) => beginGesture({
337
+ kind: "move",
338
+ day,
339
+ eventId: event.id,
340
+ anchorMinutes: 0,
341
+ grabOffset: pointerMinutes(ev.clientY) - minutesOfDay(event.start),
342
+ durationMinutes: minutesOfDay(event.end) - minutesOfDay(event.start)
343
+ }, ev),
344
+ onResizeStart: (ev) => beginGesture({
345
+ kind: "resize-start",
346
+ day,
347
+ eventId: event.id,
348
+ anchorMinutes: minutesOfDay(event.end),
349
+ grabOffset: 0,
350
+ durationMinutes: 0
351
+ }, ev),
352
+ onResizeEnd: (ev) => beginGesture({
353
+ kind: "resize-end",
354
+ day,
355
+ eventId: event.id,
356
+ anchorMinutes: minutesOfDay(event.start),
357
+ grabOffset: 0,
358
+ durationMinutes: 0
359
+ }, ev)
360
+ }), [
361
+ beginGesture,
362
+ pointerMinutes
363
+ ]);
364
+ const draggedEvent = draft?.eventId ? events.find((event) => event.id === draft.eventId) : void 0;
365
+ return /* @__PURE__ */ React2.createElement("div", {
366
+ ...composableProps(props, {
367
+ classNames: [
368
+ "flex flex-col h-full w-full overflow-hidden outline-hidden",
369
+ classNames
370
+ ]
371
+ }),
372
+ ref: forwardedRef
373
+ }, /* @__PURE__ */ React2.createElement(Weekdays, {
374
+ weekStartsOn,
375
+ gutter: GUTTER_WIDTH,
376
+ dates: weekDays
377
+ }), /* @__PURE__ */ React2.createElement("div", {
378
+ ref: scrollRef,
379
+ className: "flex-1 overflow-y-auto _scrollbar-thin"
380
+ }, /* @__PURE__ */ React2.createElement("div", {
381
+ className: "grid relative",
382
+ style: {
383
+ height: minutesToY(MINUTES_PER_DAY, HOUR_HEIGHT),
384
+ gridTemplateColumns: `${GUTTER_WIDTH}px repeat(7, 1fr)`
385
+ }
386
+ }, /* @__PURE__ */ React2.createElement("div", {
387
+ className: "relative"
388
+ }, Array.from({
389
+ length: 24
390
+ }, (_, hour) => /* @__PURE__ */ React2.createElement("div", {
391
+ key: hour,
392
+ className: "absolute right-1 -translate-y-1/2 text-xs text-description tabular-nums",
393
+ style: {
394
+ top: minutesToY(hour * 60, HOUR_HEIGHT)
395
+ }
396
+ }, hour === 0 ? "" : `${hour.toString().padStart(2, "0")}:00`))), weekDays.map((day, dayIndex) => {
397
+ const dayEvents = eventsByDay[dayIndex];
398
+ const layout = layoutDayEvents(dayEvents);
399
+ const isToday = isSameDay(day, today);
400
+ const draftHere = draft && isSameDay(day, draft.start) ? draft : void 0;
401
+ return /* @__PURE__ */ React2.createElement("div", {
402
+ key: day.toISOString(),
403
+ ref: (node) => {
404
+ columnsRef.current[dayIndex] = node;
405
+ },
406
+ "data-date": day.toISOString(),
407
+ className: mx2("relative border-l border-separator cursor-cell select-none", dayIndex === 6 && "border-r", isToday && "bg-primary-500/5"),
408
+ onPointerDown: (ev) => handleColumnPointerDown(day, ev)
409
+ }, Array.from({
410
+ length: 24
411
+ }, (_, hour) => /* @__PURE__ */ React2.createElement("div", {
412
+ key: hour,
413
+ className: "absolute inset-x-0 border-t border-separator/60",
414
+ style: {
415
+ top: minutesToY(hour * 60, HOUR_HEIGHT)
416
+ }
417
+ })), dayEvents.map((event, index) => {
418
+ const slot = layout.get(index) ?? {
419
+ columnIndex: 0,
420
+ columnCount: 1
421
+ };
422
+ const editing = draft && draft.eventId === event.id ? draft : void 0;
423
+ if (editing && !isSameDay(editing.start, day)) {
424
+ return null;
425
+ }
426
+ const start2 = editing ? editing.start : event.start;
427
+ const end = editing ? editing.end : event.end;
428
+ return renderEvent(event, day, start2, end, slot.columnIndex, slot.columnCount);
429
+ }), draftHere && draggedEvent && !dayEvents.some((event) => event.id === draggedEvent.id) && renderEvent(draggedEvent, day, draftHere.start, draftHere.end, 0, 1), draftHere && !draftHere.eventId && /* @__PURE__ */ React2.createElement(PendingBlock, {
430
+ start: draftHere.start,
431
+ end: draftHere.end
432
+ }));
433
+ }))));
434
+ });
435
+ CalendarWeek.displayName = CALENDAR_WEEK_NAME;
436
+ var formatTime = (date) => {
437
+ const minutes = minutesOfDay(date);
438
+ const hour = Math.floor(minutes / 60);
439
+ const minute = Math.round(minutes % 60);
440
+ return `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
441
+ };
442
+ var EventBlock = ({ event, start: start2, end, columnIndex, columnCount, onMoveStart, onResizeStart, onResizeEnd }) => {
443
+ const top = minutesToY(minutesOfDay(start2), HOUR_HEIGHT);
444
+ const height = Math.max(minutesToY(minutesOfDay(end) - minutesOfDay(start2), HOUR_HEIGHT), RESIZE_HANDLE * 2);
445
+ const widthPct = 100 / columnCount;
446
+ return /* @__PURE__ */ React2.createElement("div", {
447
+ className: "absolute rounded-sm bg-primary-500/80 text-inverse-fg overflow-hidden cursor-move shadow-sm",
448
+ style: {
449
+ top,
450
+ height,
451
+ left: `calc(${columnIndex * widthPct}% + 1px)`,
452
+ width: `calc(${widthPct}% - 2px)`
453
+ },
454
+ onPointerDown: onMoveStart
455
+ }, /* @__PURE__ */ React2.createElement("div", {
456
+ className: "absolute inset-x-0 top-0 cursor-ns-resize",
457
+ style: {
458
+ height: RESIZE_HANDLE
459
+ },
460
+ onPointerDown: onResizeStart
461
+ }), /* @__PURE__ */ React2.createElement("div", {
462
+ className: "px-1 py-0.5 text-xs leading-tight"
463
+ }, /* @__PURE__ */ React2.createElement("div", {
464
+ className: "font-medium truncate"
465
+ }, event.title ?? "(untitled)")), /* @__PURE__ */ React2.createElement("div", {
466
+ className: "absolute inset-x-0 bottom-0 cursor-ns-resize",
467
+ style: {
468
+ height: RESIZE_HANDLE
469
+ },
470
+ onPointerDown: onResizeEnd
471
+ }));
472
+ };
473
+ var PendingBlock = ({ start: start2, end }) => {
474
+ const top = minutesToY(minutesOfDay(start2), HOUR_HEIGHT);
475
+ const height = minutesToY(minutesOfDay(end) - minutesOfDay(start2), HOUR_HEIGHT);
476
+ return /* @__PURE__ */ React2.createElement("div", {
477
+ className: "absolute inset-x-0 rounded bg-primary-500/40 border border-primary-500 pointer-events-none",
478
+ style: {
479
+ top,
480
+ height
481
+ }
482
+ }, /* @__PURE__ */ React2.createElement("div", {
483
+ className: "px-1 py-0.5 text-xs tabular-nums text-inverse-fg"
484
+ }, formatTime(start2), "\u2013", formatTime(end)));
485
+ };
32
486
 
33
487
  // src/components/Calendar/Calendar.tsx
34
488
  var maxRows = 50 * 100;
35
- var start = /* @__PURE__ */ new Date("1970-01-01");
489
+ var start = gridEpoch;
36
490
  var size = 40;
37
491
  var defaultWidth = 7 * size;
38
492
  var EDGE_SCROLL_ZONE = 32;
39
493
  var EDGE_SCROLL_MAX_SPEED = 12;
494
+ var DATE_CLASS_NAMES = {
495
+ current: "ring-2 ring-primary-500",
496
+ today: "border-2 border-amber-500 bg-amber-500/50 text-inverse-fg",
497
+ busy: "border border-green-700",
498
+ starred: "border-2 border-dashed border-amber-500"
499
+ };
40
500
  var makeRange = (a, b) => {
41
- const dayA = startOfDay(a);
42
- const dayB = startOfDay(b);
501
+ const dayA = startOfDay3(a);
502
+ const dayB = startOfDay3(b);
43
503
  return dayA <= dayB ? {
44
504
  from: dayA,
45
505
  to: dayB
@@ -52,7 +512,7 @@ var isInRange = (date, range) => {
52
512
  if (!range) {
53
513
  return false;
54
514
  }
55
- const day = startOfDay(date).getTime();
515
+ const day = startOfDay3(date).getTime();
56
516
  return day >= range.from.getTime() && day <= range.to.getTime();
57
517
  };
58
518
  var cellDate = (el) => {
@@ -66,24 +526,29 @@ var cellDate = (el) => {
66
526
  }
67
527
  return void 0;
68
528
  };
69
- var [CalendarContextProvider, useCalendarContext] = createContext("Calendar");
70
529
  var CalendarRoot = /* @__PURE__ */ forwardRef(({ children, weekStartsOn = 1 }, forwardedRef) => {
71
- const event = useMemo(() => new Event(), []);
72
- const [selected, setSelected] = useState();
73
- const [index, setIndex] = useState();
74
- const [range, setRange] = useState();
75
- const [pendingRange, setPendingRange] = useState();
530
+ const event = useMemo3(() => new Event(), []);
531
+ const [selected, setSelected] = useState2();
532
+ const [index, setIndex] = useState2();
533
+ const [range, setRange] = useState2();
534
+ const [pendingRange, setPendingRange] = useState2();
76
535
  useImperativeHandle(forwardedRef, () => ({
77
536
  scrollTo: (date) => {
78
537
  event.emit({
79
538
  type: "scroll",
80
539
  date
81
540
  });
541
+ },
542
+ select: (date) => {
543
+ event.emit({
544
+ type: "select",
545
+ date
546
+ });
82
547
  }
83
548
  }), [
84
549
  event
85
550
  ]);
86
- return /* @__PURE__ */ React.createElement(CalendarContextProvider, {
551
+ return /* @__PURE__ */ React3.createElement(CalendarContextProvider, {
87
552
  weekStartsOn,
88
553
  event,
89
554
  index,
@@ -97,15 +562,15 @@ var CalendarRoot = /* @__PURE__ */ forwardRef(({ children, weekStartsOn = 1 }, f
97
562
  }, children);
98
563
  });
99
564
  var CALENDAR_TOOLBAR_NAME = "CalendarHeader";
100
- var CalendarToolbar = composable(({ classNames, ...props }, forwardedRef) => {
565
+ var CalendarToolbar = composable2(({ classNames, ...props }, forwardedRef) => {
101
566
  const { t } = useTranslation(translationKey);
102
567
  const { weekStartsOn, event, index, selected } = useCalendarContext(CALENDAR_TOOLBAR_NAME);
103
- const top = useMemo(() => getDate(start, index ?? 0, 6, weekStartsOn), [
568
+ const top = useMemo3(() => getDate(start, index ?? 0, 6, weekStartsOn), [
104
569
  index,
105
570
  weekStartsOn
106
571
  ]);
107
- const today = useMemo(() => /* @__PURE__ */ new Date(), []);
108
- const handleToday = useCallback(() => {
572
+ const today = useMemo3(() => /* @__PURE__ */ new Date(), []);
573
+ const handleToday = useCallback2(() => {
109
574
  event.emit({
110
575
  type: "scroll",
111
576
  date: today
@@ -115,91 +580,98 @@ var CalendarToolbar = composable(({ classNames, ...props }, forwardedRef) => {
115
580
  start,
116
581
  today
117
582
  ]);
118
- return /* @__PURE__ */ React.createElement("div", {
119
- ...composableProps(props, {
583
+ return /* @__PURE__ */ React3.createElement("div", {
584
+ ...composableProps2(props, {
120
585
  role: "none",
121
586
  classNames: [
122
587
  "shrink-0 grid! grid-cols-3 items-center bg-toolbar-surface",
123
588
  classNames
124
589
  ]
125
590
  }),
126
- ref: forwardedRef,
127
- style: {
128
- width: defaultWidth
129
- }
130
- }, /* @__PURE__ */ React.createElement("div", {
591
+ ref: forwardedRef
592
+ }, /* @__PURE__ */ React3.createElement("div", {
131
593
  className: "flex justify-start"
132
- }, /* @__PURE__ */ React.createElement(IconButton, {
594
+ }, /* @__PURE__ */ React3.createElement(IconButton, {
133
595
  variant: "ghost",
134
596
  icon: "ph--calendar--regular",
135
597
  iconOnly: true,
136
598
  classNames: "aspect-square",
137
599
  label: t("today.button"),
138
600
  onClick: handleToday
139
- })), /* @__PURE__ */ React.createElement("div", {
601
+ })), /* @__PURE__ */ React3.createElement("div", {
140
602
  className: "flex justify-center p-2 text-description"
141
- }, format(selected ?? top, "MMMM")), /* @__PURE__ */ React.createElement("div", {
603
+ }, format2(selected ?? top, "MMMM")), /* @__PURE__ */ React3.createElement("div", {
142
604
  className: "flex justify-end p-2 text-description"
143
605
  }, (selected ?? top).getFullYear()));
144
606
  });
145
607
  CalendarToolbar.displayName = CALENDAR_TOOLBAR_NAME;
146
608
  var CALENDAR_GRID_NAME = "CalendarGrid";
147
- var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSelect, onSelectRange, ...props }, forwardedRef) => {
609
+ var CalendarGrid = composable2(({ classNames, rows, dates = [], initialDate, scrollMargin = 2, onSelect, onSelectRange, ...props }, forwardedRef) => {
148
610
  const { weekStartsOn, event, setIndex, selected, setSelected, range, setRange, pendingRange, setPendingRange } = useCalendarContext(CALENDAR_GRID_NAME);
149
611
  const { ref: containerRef, width = 0, height = 0 } = useResizeDetector();
150
612
  const maxHeight = rows ? rows * size : void 0;
151
- const listRef = useRef(null);
152
- const gridRef = useRef(null);
153
- const today = useMemo(() => /* @__PURE__ */ new Date(), []);
154
- const dateSet = useMemo(() => new Set(dates.map((date) => startOfDay(date).toISOString())), [
613
+ const listRef = useRef2(null);
614
+ const gridRef = useRef2(null);
615
+ const today = useMemo3(() => /* @__PURE__ */ new Date(), []);
616
+ const dateMarkers = useMemo3(() => {
617
+ const markers = /* @__PURE__ */ new Map();
618
+ for (const { startDate, endDate, tag = "busy" } of dates) {
619
+ const end = endDate ? startOfDay3(endDate) : startOfDay3(startDate);
620
+ for (let date = startOfDay3(startDate); date <= end; date = addDays3(date, 1)) {
621
+ const iso = date.toISOString();
622
+ if (markers.get(iso) !== "star") {
623
+ markers.set(iso, tag);
624
+ }
625
+ }
626
+ }
627
+ return markers;
628
+ }, [
155
629
  dates
156
630
  ]);
157
- const hasDate = useCallback((date) => dateSet.has(startOfDay(date).toISOString()), [
158
- dateSet
631
+ const getMarker = useCallback2((date) => {
632
+ const iso = startOfDay3(date).toISOString();
633
+ const tag = dateMarkers.get(iso);
634
+ return tag ? {
635
+ tag
636
+ } : void 0;
637
+ }, [
638
+ dateMarkers
159
639
  ]);
160
- const [initialized, setInitialized] = useState(false);
161
- useEffect(() => {
640
+ const [initialized, setInitialized] = useState2(false);
641
+ useEffect2(() => {
162
642
  const index = getRowIndex(start, initialDate ?? today, weekStartsOn);
163
- listRef.current?.scrollToRow(index);
643
+ listRef.current?.scrollToRow(Math.max(0, index - scrollMargin));
164
644
  }, [
165
645
  initialized,
166
646
  start,
167
647
  today,
168
648
  initialDate,
169
- weekStartsOn
649
+ weekStartsOn,
650
+ scrollMargin
170
651
  ]);
171
- useEffect(() => {
652
+ useEffect2(() => {
172
653
  return event.on((event2) => {
173
- switch (event2.type) {
174
- case "scroll": {
175
- const index = getRowIndex(start, event2.date, weekStartsOn);
176
- listRef.current?.scrollToRow(index);
177
- break;
178
- }
654
+ if (event2.type === "select") {
655
+ setSelected(event2.date);
179
656
  }
657
+ const index = getRowIndex(start, event2.date, weekStartsOn);
658
+ listRef.current?.scrollToRow(Math.max(0, index - scrollMargin));
180
659
  });
181
660
  }, [
182
- event
661
+ event,
662
+ start,
663
+ weekStartsOn,
664
+ scrollMargin,
665
+ setSelected
183
666
  ]);
184
- const days = useMemo(() => {
185
- const weekStart = startOfWeek(/* @__PURE__ */ new Date(), {
186
- weekStartsOn
187
- });
188
- return Array.from({
189
- length: 7
190
- }, (_, i) => {
191
- const day = addDays(weekStart, i);
192
- return format(day, "EEE");
193
- });
194
- }, []);
195
- const anchorRef = useRef(void 0);
196
- const focusRef = useRef(void 0);
197
- const draggingRef = useRef(false);
198
- const pointerXRef = useRef(0);
199
- const pointerYRef = useRef(0);
200
- const scrollTopRef = useRef(0);
201
- const scrollRafRef = useRef(void 0);
202
- const scrollIntoView = useCallback((date) => {
667
+ const anchorRef = useRef2(void 0);
668
+ const focusRef = useRef2(void 0);
669
+ const draggingRef = useRef2(false);
670
+ const pointerXRef = useRef2(0);
671
+ const pointerYRef = useRef2(0);
672
+ const scrollTopRef = useRef2(0);
673
+ const scrollRafRef = useRef2(void 0);
674
+ const scrollIntoView = useCallback2((date) => {
203
675
  const targetRow = getRowIndex(start, date, weekStartsOn);
204
676
  const visibleHeight = maxHeight ?? height;
205
677
  if (!visibleHeight) {
@@ -217,7 +689,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
217
689
  maxHeight,
218
690
  weekStartsOn
219
691
  ]);
220
- const updateRangeFromAnchor = useCallback((focus, fireRange = false) => {
692
+ const updateRangeFromAnchor = useCallback2((focus, fireRange = false) => {
221
693
  const anchor = anchorRef.current;
222
694
  if (!anchor) {
223
695
  return;
@@ -241,8 +713,8 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
241
713
  setRange,
242
714
  setSelected
243
715
  ]);
244
- const prevSelectedRef = useRef(void 0);
245
- const handleDayPointerDown = useCallback((date, ev) => {
716
+ const prevSelectedRef = useRef2(void 0);
717
+ const handleDayPointerDown = useCallback2((date, ev) => {
246
718
  ev.preventDefault();
247
719
  prevSelectedRef.current = selected;
248
720
  anchorRef.current = date;
@@ -260,7 +732,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
260
732
  setRange,
261
733
  setSelected
262
734
  ]);
263
- const handleDayPointerEnter = useCallback((date) => {
735
+ const handleDayPointerEnter = useCallback2((date) => {
264
736
  if (!draggingRef.current) {
265
737
  return;
266
738
  }
@@ -275,7 +747,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
275
747
  setPendingRange,
276
748
  setSelected
277
749
  ]);
278
- const handleDayPointerUp = useCallback((date) => {
750
+ const handleDayPointerUp = useCallback2((date) => {
279
751
  const anchor = anchorRef.current;
280
752
  const wasDragging = draggingRef.current;
281
753
  draggingRef.current = false;
@@ -309,7 +781,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
309
781
  setRange,
310
782
  setSelected
311
783
  ]);
312
- useEffect(() => {
784
+ useEffect2(() => {
313
785
  const cancel = () => {
314
786
  if (draggingRef.current) {
315
787
  draggingRef.current = false;
@@ -325,7 +797,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
325
797
  }, [
326
798
  setPendingRange
327
799
  ]);
328
- const tickEdgeScroll = useCallback(() => {
800
+ const tickEdgeScroll = useCallback2(() => {
329
801
  scrollRafRef.current = void 0;
330
802
  if (!draggingRef.current) {
331
803
  return;
@@ -363,7 +835,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
363
835
  setPendingRange,
364
836
  setSelected
365
837
  ]);
366
- useEffect(() => {
838
+ useEffect2(() => {
367
839
  const handleMove = (ev) => {
368
840
  if (!draggingRef.current) {
369
841
  return;
@@ -385,7 +857,7 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
385
857
  }, [
386
858
  tickEdgeScroll
387
859
  ]);
388
- const handleKeyDown = useCallback((ev) => {
860
+ const handleKeyDown = useCallback2((ev) => {
389
861
  let dx = 0;
390
862
  switch (ev.key) {
391
863
  case "ArrowLeft":
@@ -409,24 +881,24 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
409
881
  let focus = focusRef.current;
410
882
  if (!anchor) {
411
883
  if (selected) {
412
- anchor = startOfDay(selected);
884
+ anchor = startOfDay3(selected);
413
885
  focus = anchor;
414
886
  } else if (range) {
415
887
  anchor = range.from;
416
888
  focus = range.to;
417
889
  } else {
418
- anchor = startOfDay(today);
890
+ anchor = startOfDay3(today);
419
891
  focus = anchor;
420
892
  }
421
893
  anchorRef.current = anchor;
422
894
  focusRef.current = focus;
423
895
  }
424
- const newFocus = addDays(focus ?? anchor, dx);
896
+ const newFocus = addDays3(focus ?? anchor, dx);
425
897
  updateRangeFromAnchor(newFocus, true);
426
898
  scrollIntoView(newFocus);
427
899
  } else {
428
900
  const current = selected ?? focusRef.current ?? anchorRef.current ?? today;
429
- const next = addDays(startOfDay(current), dx);
901
+ const next = addDays3(startOfDay3(current), dx);
430
902
  anchorRef.current = next;
431
903
  focusRef.current = next;
432
904
  setRange(void 0);
@@ -449,17 +921,17 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
449
921
  updateRangeFromAnchor
450
922
  ]);
451
923
  const activeRange = pendingRange ?? range;
452
- const handleScroll = useCallback((info) => {
924
+ const handleScroll = useCallback2((info) => {
453
925
  scrollTopRef.current = info.scrollTop;
454
926
  setIndex(Math.round(info.scrollTop / size));
455
927
  }, []);
456
- const rowRenderer = useCallback(({ key, index, style }) => {
928
+ const rowRenderer = useCallback2(({ key, index, style }) => {
457
929
  const getBgColor = (date) => date.getMonth() % 2 === 0 ? "bg-group-surface" : "bg-group-alt-surface";
458
- return /* @__PURE__ */ React.createElement("div", {
930
+ return /* @__PURE__ */ React3.createElement("div", {
459
931
  key,
460
932
  style,
461
933
  className: "grid"
462
- }, /* @__PURE__ */ React.createElement("div", {
934
+ }, /* @__PURE__ */ React3.createElement("div", {
463
935
  className: "grid grid-cols-7 bg-input-surface",
464
936
  style: {
465
937
  gridTemplateColumns: `repeat(7, ${size}px)`
@@ -468,23 +940,26 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
468
940
  length: 7
469
941
  }).map((_, i) => {
470
942
  const date = getDate(start, index, i, weekStartsOn);
943
+ const marker = getMarker(date);
944
+ const isToday = isSameDay(date, today);
945
+ const isCurrent = isSameDay(date, selected);
946
+ const dateClassNames = isToday ? DATE_CLASS_NAMES.today : marker?.tag === "star" ? DATE_CLASS_NAMES.starred : marker ? DATE_CLASS_NAMES.busy : void 0;
471
947
  const inRange = isInRange(date, activeRange);
472
- const border = isSameDay(date, selected) ? "border-primary-500" : isSameDay(date, today) ? "border-amber-500" : hasDate(date) ? "border-neutral-700 border-dashed" : void 0;
473
- return /* @__PURE__ */ React.createElement("div", {
948
+ return /* @__PURE__ */ React3.createElement("div", {
474
949
  key: i,
475
- "data-date": startOfDay(date).toISOString(),
476
- className: mx("relative flex justify-center items-center cursor-pointer select-none", getBgColor(date)),
950
+ "data-date": startOfDay3(date).toISOString(),
951
+ className: mx3("relative flex justify-center cursor-pointer select-none", getBgColor(date)),
477
952
  onPointerDown: (ev) => handleDayPointerDown(date, ev),
478
953
  onPointerEnter: () => handleDayPointerEnter(date),
479
954
  onPointerUp: () => handleDayPointerUp(date)
480
- }, inRange && /* @__PURE__ */ React.createElement("div", {
955
+ }, inRange && /* @__PURE__ */ React3.createElement("div", {
481
956
  className: "absolute inset-0 bg-primary-500/20"
482
- }), /* @__PURE__ */ React.createElement("span", {
483
- className: "relative text-description text-sm"
484
- }, date.getDate()), !border && date.getDate() === 1 && /* @__PURE__ */ React.createElement("span", {
957
+ }), !dateClassNames && date.getDate() === 1 && /* @__PURE__ */ React3.createElement("span", {
485
958
  className: "absolute top-0 text-xs text-description"
486
- }, format(date, "MMM")), border && /* @__PURE__ */ React.createElement("div", {
487
- className: mx("absolute inset-1 border-2 rounded-full", border)
959
+ }, format2(date, "MMM")), /* @__PURE__ */ React3.createElement("div", {
960
+ className: mx3("absolute inset-1 rounded-full flex justify-center items-center text-sm text-description", dateClassNames)
961
+ }, date.getDate()), isCurrent && /* @__PURE__ */ React3.createElement("div", {
962
+ className: mx3("absolute inset-0.5 rounded-full", DATE_CLASS_NAMES.current)
488
963
  }));
489
964
  })));
490
965
  }, [
@@ -492,12 +967,12 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
492
967
  handleDayPointerDown,
493
968
  handleDayPointerEnter,
494
969
  handleDayPointerUp,
495
- hasDate,
970
+ getMarker,
496
971
  selected,
497
972
  weekStartsOn
498
973
  ]);
499
- return /* @__PURE__ */ React.createElement("div", {
500
- ...composableProps(props, {
974
+ return /* @__PURE__ */ React3.createElement("div", {
975
+ ...composableProps2(props, {
501
976
  role: "none",
502
977
  classNames: [
503
978
  "flex flex-col h-full w-full justify-center overflow-hidden outline-hidden",
@@ -514,18 +989,17 @@ var CalendarGrid = composable(({ classNames, rows, dates = [], initialDate, onSe
514
989
  },
515
990
  tabIndex: 0,
516
991
  onKeyDown: handleKeyDown
517
- }, /* @__PURE__ */ React.createElement("div", {
518
- className: "grid w-full grid-cols-7",
992
+ }, /* @__PURE__ */ React3.createElement("div", {
519
993
  style: {
520
994
  width: defaultWidth
521
995
  }
522
- }, days.map((date, i) => /* @__PURE__ */ React.createElement("div", {
523
- key: i,
524
- className: "flex justify-center p-2 text-sm font-thin"
525
- }, date))), /* @__PURE__ */ React.createElement("div", {
996
+ }, /* @__PURE__ */ React3.createElement(Weekdays, {
997
+ weekStartsOn,
998
+ columnWidth: size
999
+ })), /* @__PURE__ */ React3.createElement("div", {
526
1000
  className: "flex flex-col h-full w-full justify-center overflow-hidden",
527
1001
  ref: containerRef
528
- }, /* @__PURE__ */ React.createElement(List, {
1002
+ }, /* @__PURE__ */ React3.createElement(List, {
529
1003
  ref: listRef,
530
1004
  role: "none",
531
1005
  className: "scrollbar-none outline-hidden",
@@ -543,7 +1017,8 @@ CalendarGrid.displayName = CALENDAR_GRID_NAME;
543
1017
  var Calendar = {
544
1018
  Root: CalendarRoot,
545
1019
  Toolbar: CalendarToolbar,
546
- Grid: CalendarGrid
1020
+ Grid: CalendarGrid,
1021
+ Week: CalendarWeek
547
1022
  };
548
1023
  export {
549
1024
  Calendar