@dxos/react-ui-calendar 0.9.0 → 0.9.1-staging.ee54ba693a

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