calendarkit-pro 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/index.tsx
4
- import React18, { useId, useState as useState6, useMemo as useMemo4 } from "react";
4
+ import React18, { useCallback as useCallback5, useId, useState as useState6, useMemo as useMemo4 } from "react";
5
5
  import { DndContext, useSensor, useSensors, PointerSensor, DragOverlay } from "@dnd-kit/core";
6
6
  import { createSnapModifier, restrictToWindowEdges } from "@dnd-kit/modifiers";
7
7
  import { AnimatePresence as AnimatePresence3, motion as motion4 } from "framer-motion";
@@ -164,21 +164,26 @@ import { Plus, ChevronDown, Globe } from "lucide-react";
164
164
 
165
165
  // src/components/MiniCalendar.tsx
166
166
  import React3 from "react";
167
- import { format as format3, isSameMonth, isSameDay, isToday, addMonths, subMonths } from "date-fns";
167
+ import { format as format2, isSameMonth, isSameDay, isToday, addMonths, subMonths } from "date-fns";
168
168
 
169
- // src/lib/date.ts
170
- import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, format as format2 } from "date-fns";
171
- var getMonthGrid = (date, weekStartOn = 0) => {
169
+ // src/core/week.ts
170
+ import { eachDayOfInterval, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from "date-fns";
171
+ function resolveWeekStartsOn(locale, fallback) {
172
+ return locale?.options?.weekStartsOn ?? fallback;
173
+ }
174
+ function getWeekDays(date, weekStartsOn) {
175
+ return eachDayOfInterval({
176
+ start: startOfWeek(date, { weekStartsOn }),
177
+ end: endOfWeek(date, { weekStartsOn })
178
+ });
179
+ }
180
+ function getMonthGrid(date, weekStartsOn) {
172
181
  const monthStart = startOfMonth(date);
173
182
  return eachDayOfInterval({
174
- start: startOfWeek(monthStart, { weekStartsOn: weekStartOn }),
175
- end: endOfWeek(endOfMonth(monthStart), { weekStartsOn: weekStartOn })
183
+ start: startOfWeek(monthStart, { weekStartsOn }),
184
+ end: endOfWeek(endOfMonth(monthStart), { weekStartsOn })
176
185
  });
177
- };
178
- var uses24HourClock = (locale) => {
179
- if (!locale) return false;
180
- return !/[ap]\.?m/i.test(format2(new Date(2e3, 0, 1, 13, 0), "p", { locale }));
181
- };
186
+ }
182
187
 
183
188
  // src/components/MiniCalendar.tsx
184
189
  import { ChevronLeft as ChevronLeft2, ChevronRight as ChevronRight2 } from "lucide-react";
@@ -193,7 +198,7 @@ var MiniCalendar = ({
193
198
  React3.useEffect(() => {
194
199
  setViewDate(currentDate);
195
200
  }, [currentDate]);
196
- const weekStartsOn = locale?.options?.weekStartsOn ?? 0;
201
+ const weekStartsOn = resolveWeekStartsOn(locale, 0);
197
202
  const days = React3.useMemo(
198
203
  () => getMonthGrid(viewDate, weekStartsOn),
199
204
  [viewDate, weekStartsOn]
@@ -218,7 +223,7 @@ var MiniCalendar = ({
218
223
  onDateChange(day);
219
224
  if (onViewChange) onViewChange("day");
220
225
  };
221
- return /* @__PURE__ */ React3.createElement("div", { className: cn("px-4 w-[260px]", className) }, /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-between mb-4" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-semibold text-foreground capitalize" }, format3(viewDate, "MMMM yyyy", { locale })), /* @__PURE__ */ React3.createElement("div", { className: "flex items-center bg-muted/40 rounded-lg p-0.5" }, /* @__PURE__ */ React3.createElement(
226
+ return /* @__PURE__ */ React3.createElement("div", { className: cn("px-4 w-[260px]", className) }, /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-between mb-4" }, /* @__PURE__ */ React3.createElement("span", { className: "text-sm font-semibold text-foreground capitalize" }, format2(viewDate, "MMMM yyyy", { locale })), /* @__PURE__ */ React3.createElement("div", { className: "flex items-center bg-muted/40 rounded-lg p-0.5" }, /* @__PURE__ */ React3.createElement(
222
227
  Button,
223
228
  {
224
229
  variant: "ghost",
@@ -255,7 +260,7 @@ var MiniCalendar = ({
255
260
  !isSelected && isTodayDate && "bg-primary/10 text-primary ring-1 ring-primary/30"
256
261
  )
257
262
  },
258
- format3(day, "d")
263
+ format2(day, "d")
259
264
  );
260
265
  })));
261
266
  };
@@ -263,6 +268,194 @@ var MiniCalendar = ({
263
268
  // src/components/Sidebar.tsx
264
269
  import { toZonedTime } from "date-fns-tz";
265
270
  import { format as format4 } from "date-fns";
271
+
272
+ // src/core/format.ts
273
+ import { format as format3 } from "date-fns";
274
+
275
+ // src/locales/index.ts
276
+ var en = {
277
+ today: "Today",
278
+ month: "Month",
279
+ week: "Week",
280
+ day: "Day",
281
+ agenda: "Agenda",
282
+ resource: "Resources",
283
+ createEvent: "Create Event",
284
+ editEvent: "Edit Event",
285
+ delete: "Delete",
286
+ save: "Save",
287
+ cancel: "Cancel",
288
+ title: "Add title",
289
+ start: "Start",
290
+ end: "End",
291
+ allDay: "All Day",
292
+ description: "Description",
293
+ repeat: "Repeat",
294
+ noRepeat: "Does not repeat",
295
+ selectCalendar: "Select Calendar",
296
+ selectType: "Select Type",
297
+ daily: "Daily",
298
+ weekly: "Weekly",
299
+ monthly: "Monthly",
300
+ yearly: "Yearly",
301
+ search: "Search events...",
302
+ allTypes: "All Types",
303
+ timezone: "Time zone",
304
+ localTime: "Local Time",
305
+ create: "Create",
306
+ calendars: "Calendars",
307
+ event: "Event",
308
+ task: "Task",
309
+ appointmentSchedule: "Appointment schedule",
310
+ new: "New",
311
+ dateAndTime: "Date & Time",
312
+ whosJoining: "Who's joining?",
313
+ suggestedTimes: "Suggested times for guests",
314
+ viewSuggestions: "View suggestions",
315
+ whereWillItBe: "Where will it be?",
316
+ location: "Enter address",
317
+ descriptionAndAttachments: "Description",
318
+ dragAndDrop: "Drag & drop files or",
319
+ guests: "Guests",
320
+ addGuests: "Add guests",
321
+ addAttachment: "Upload files",
322
+ moreOptions: "More options",
323
+ doesNotRepeat: "Does not repeat",
324
+ locationHelpText: 'Enter a physical address (e.g. "123 Main St") to generate the map.',
325
+ notes: "Notes...",
326
+ guestCount: "guest",
327
+ guestsCount: "guests",
328
+ repeatFor: "Repeat for",
329
+ times: "times",
330
+ until: "Until",
331
+ endRepeat: "End repeat",
332
+ never: "Never",
333
+ afterOccurrences: "After occurrences",
334
+ onDate: "On date",
335
+ tomorrow: "Tomorrow",
336
+ edit: "Edit",
337
+ duplicate: "Duplicate",
338
+ eventSingular: "event",
339
+ eventPlural: "events",
340
+ allCaughtUp: "All caught up!",
341
+ noEventsScheduled: "No events scheduled",
342
+ noUpcoming: "Your schedule is clear",
343
+ noUpcomingHint: "No upcoming events to show. Create a new event to start planning."
344
+ };
345
+ var fr = {
346
+ today: "Aujourd'hui",
347
+ month: "Mois",
348
+ week: "Semaine",
349
+ day: "Jour",
350
+ agenda: "Agenda",
351
+ resource: "Ressources",
352
+ createEvent: "Cr\xE9er un \xE9v\xE9nement",
353
+ editEvent: "Modifier l'\xE9v\xE9nement",
354
+ delete: "Supprimer",
355
+ save: "Enregistrer",
356
+ cancel: "Annuler",
357
+ title: "Ajouter un titre",
358
+ start: "D\xE9but",
359
+ end: "Fin",
360
+ allDay: "Toute la journ\xE9e",
361
+ description: "Description",
362
+ repeat: "R\xE9p\xE9ter",
363
+ noRepeat: "Ne se r\xE9p\xE8te pas",
364
+ selectCalendar: "S\xE9lectionner un calendrier",
365
+ selectType: "S\xE9lectionner un type",
366
+ daily: "Quotidien",
367
+ weekly: "Hebdomadaire",
368
+ monthly: "Mensuel",
369
+ yearly: "Annuel",
370
+ search: "Rechercher...",
371
+ allTypes: "Tous les types",
372
+ timezone: "Fuseau horaire",
373
+ localTime: "Heure locale",
374
+ create: "Cr\xE9er",
375
+ calendars: "Calendriers",
376
+ event: "\xC9v\xE9nement",
377
+ task: "T\xE2che",
378
+ appointmentSchedule: "Planning de rendez-vous",
379
+ new: "Nouveau",
380
+ dateAndTime: "Date et heure",
381
+ whosJoining: "Qui participe ?",
382
+ suggestedTimes: "Horaires sugg\xE9r\xE9s",
383
+ viewSuggestions: "Voir les suggestions",
384
+ whereWillItBe: "O\xF9 aura lieu ?",
385
+ location: "Entrer une adresse",
386
+ descriptionAndAttachments: "Description",
387
+ dragAndDrop: "Glisser-d\xE9poser des fichiers ou",
388
+ guests: "Invit\xE9s",
389
+ addGuests: "Ajouter des invit\xE9s",
390
+ addAttachment: "T\xE9l\xE9charger des fichiers",
391
+ moreOptions: "Plus d'options",
392
+ doesNotRepeat: "Ne se r\xE9p\xE8te pas",
393
+ locationHelpText: 'Entrez une adresse physique (ex: "123 Rue Principale") pour g\xE9n\xE9rer la carte.',
394
+ notes: "Notes...",
395
+ guestCount: "invit\xE9",
396
+ guestsCount: "invit\xE9s",
397
+ repeatFor: "R\xE9p\xE9ter pendant",
398
+ times: "fois",
399
+ until: "Jusqu'au",
400
+ endRepeat: "Fin de r\xE9p\xE9tition",
401
+ never: "Jamais",
402
+ afterOccurrences: "Apr\xE8s occurrences",
403
+ onDate: "\xC0 la date",
404
+ tomorrow: "Demain",
405
+ edit: "Modifier",
406
+ duplicate: "Dupliquer",
407
+ eventSingular: "\xE9v\xE9nement",
408
+ eventPlural: "\xE9v\xE9nements",
409
+ allCaughtUp: "Tout est \xE0 jour !",
410
+ noEventsScheduled: "Aucun \xE9v\xE9nement pr\xE9vu",
411
+ noUpcoming: "Votre planning est libre",
412
+ noUpcomingHint: "Aucun \xE9v\xE9nement \xE0 venir. Cr\xE9ez un nouvel \xE9v\xE9nement pour commencer \xE0 planifier."
413
+ };
414
+
415
+ // src/core/format.ts
416
+ var clockCache = /* @__PURE__ */ new WeakMap();
417
+ function uses24HourClock(locale) {
418
+ if (!locale) return false;
419
+ let is24 = clockCache.get(locale);
420
+ if (is24 === void 0) {
421
+ is24 = !/[ap]\.?m/i.test(format3(new Date(2e3, 0, 1, 13, 0), "p", { locale }));
422
+ clockCache.set(locale, is24);
423
+ }
424
+ return is24;
425
+ }
426
+ var H24 = { hour: "H:mm", time: "H:mm", compact: "H:mm" };
427
+ var H12 = { hour: "h a", time: "h:mm a", compact: "h:mm" };
428
+ function timeFormats(locale) {
429
+ return uses24HourClock(locale) ? H24 : H12;
430
+ }
431
+ var MAX_FORMATTERS = 50;
432
+ var formatters = /* @__PURE__ */ new Map();
433
+ function getDateTimeFormat(locale, timeZone, options) {
434
+ const key = `${locale}|${timeZone ?? ""}|${JSON.stringify(options)}`;
435
+ let formatter = formatters.get(key);
436
+ if (formatter) {
437
+ formatters.delete(key);
438
+ } else {
439
+ formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone });
440
+ if (formatters.size >= MAX_FORMATTERS) {
441
+ formatters.delete(formatters.keys().next().value);
442
+ }
443
+ }
444
+ formatters.set(key, formatter);
445
+ return formatter;
446
+ }
447
+ function timeZoneAbbreviation(date, timeZone) {
448
+ try {
449
+ return getDateTimeFormat("en-US", timeZone, { timeZoneName: "short" }).formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "";
450
+ } catch {
451
+ return "";
452
+ }
453
+ }
454
+ function resolveTranslations(language, overrides) {
455
+ return { ...language === "fr" ? fr : en, ...overrides };
456
+ }
457
+
458
+ // src/components/Sidebar.tsx
266
459
  var Sidebar = ({
267
460
  currentDate,
268
461
  onDateChange,
@@ -293,14 +486,7 @@ var Sidebar = ({
293
486
  { id: "3", label: "Tasks", color: "#6366f1", active: true }
294
487
  ];
295
488
  const displayCalendars = calendars || defaultCalendars;
296
- const getAcronym = (tz) => {
297
- if (!tz || !now) return "LOC";
298
- try {
299
- return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(now).find((part) => part.type === "timeZoneName")?.value || "";
300
- } catch {
301
- return "";
302
- }
303
- };
489
+ const getAcronym = (tz) => !tz || !now ? "LOC" : timeZoneAbbreviation(now, tz);
304
490
  const timezones = [
305
491
  { value: "", label: translations?.localTime || "Local Time", acronym: "LOC" },
306
492
  { value: "UTC", label: "UTC", acronym: "UTC" },
@@ -325,10 +511,7 @@ var Sidebar = ({
325
511
  try {
326
512
  if (!tz.value) {
327
513
  time = format4(now, "HH:mm");
328
- try {
329
- acronym = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(now).find((part) => part.type === "timeZoneName")?.value || "LOC";
330
- } catch {
331
- }
514
+ acronym = timeZoneAbbreviation(now) || "LOC";
332
515
  } else {
333
516
  const zDate = toZonedTime(now, tz.value);
334
517
  time = format4(zDate, "HH:mm");
@@ -423,32 +606,198 @@ var Sidebar = ({
423
606
  };
424
607
 
425
608
  // src/views/MonthView.tsx
426
- import React7, { useMemo, useCallback } from "react";
427
- import { format as format5, isSameMonth as isSameMonth2, isToday as isToday2, startOfWeek as startOfWeek2, endOfWeek as endOfWeek2, eachDayOfInterval as eachDayOfInterval2 } from "date-fns";
428
- import { toZonedTime as toZonedTime2 } from "date-fns-tz";
609
+ import React7, { useMemo } from "react";
610
+ import { format as format7, isSameDay as isSameDay3, isSameMonth as isSameMonth2 } from "date-fns";
611
+
612
+ // src/core/events.ts
613
+ import { addDays, format as format6, startOfDay as startOfDay2 } from "date-fns";
614
+
615
+ // src/core/timezone.ts
616
+ import { format as format5, isSameDay as isSameDay2, startOfDay } from "date-fns";
617
+ import { fromZonedTime, toZonedTime as toZonedTime2 } from "date-fns-tz";
618
+
619
+ // src/core/warn.ts
620
+ var warned = /* @__PURE__ */ new Set();
621
+ function isProductionBuild() {
622
+ try {
623
+ return process.env.NODE_ENV === "production";
624
+ } catch {
625
+ return false;
626
+ }
627
+ }
628
+ function warnOnce(key, message) {
629
+ if (isProductionBuild() || warned.has(key)) return;
630
+ warned.add(key);
631
+ console.warn(`[calendarkit-pro] ${message}`);
632
+ }
633
+
634
+ // src/core/timezone.ts
635
+ var validity = /* @__PURE__ */ new Map();
636
+ function resolveTimeZone(tz) {
637
+ if (!tz) return void 0;
638
+ let valid = validity.get(tz);
639
+ if (valid === void 0) {
640
+ try {
641
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
642
+ valid = true;
643
+ } catch {
644
+ valid = false;
645
+ }
646
+ validity.set(tz, valid);
647
+ }
648
+ if (!valid) {
649
+ warnOnce(`invalid-timezone:${tz}`, `Unknown timezone "${tz}", falling back to local time.`);
650
+ return void 0;
651
+ }
652
+ return tz;
653
+ }
654
+ function toDisplay(date, tz) {
655
+ const zone = resolveTimeZone(tz);
656
+ return zone ? toZonedTime2(date, zone) : date;
657
+ }
658
+ function fromDisplay(wall, tz) {
659
+ const zone = resolveTimeZone(tz);
660
+ return zone ? fromZonedTime(wall, zone) : new Date(wall);
661
+ }
662
+ function minutesSinceDisplayMidnight(date, tz) {
663
+ const wall = toDisplay(date, tz);
664
+ return wall.getHours() * 60 + wall.getMinutes();
665
+ }
666
+ function isSameDisplayDay(date, day, tz) {
667
+ return isSameDay2(toDisplay(date, tz), day);
668
+ }
669
+ function displayToday(tz, now = /* @__PURE__ */ new Date()) {
670
+ return startOfDay(toDisplay(now, tz));
671
+ }
672
+
673
+ // src/core/events.ts
674
+ var toDate = (value) => value === void 0 || value === null ? void 0 : value instanceof Date ? value : new Date(value);
675
+ var isValidDate = (date) => !!date && !Number.isNaN(date.getTime());
676
+ var normalizedEvent = /* @__PURE__ */ new WeakMap();
677
+ var normalizedList = /* @__PURE__ */ new WeakMap();
678
+ function normalizeEvent(event) {
679
+ const cached = normalizedEvent.get(event);
680
+ if (cached !== void 0) return cached;
681
+ const start = toDate(event.start);
682
+ const end = toDate(event.end);
683
+ let result = null;
684
+ if (!isValidDate(start) || !isValidDate(end)) {
685
+ warnOnce(`invalid-date:${event.id}`, `Event "${event.id}" has an invalid start or end date and was skipped.`);
686
+ } else if (end.getTime() < start.getTime()) {
687
+ warnOnce(`end-before-start:${event.id}`, `Event "${event.id}" ends before it starts and was skipped.`);
688
+ } else {
689
+ const needsCopy = start !== event.start || end !== event.end || needsRecurrenceCopy(event);
690
+ result = needsCopy ? {
691
+ ...event,
692
+ start,
693
+ end,
694
+ recurrence: event.recurrence && {
695
+ ...event.recurrence,
696
+ until: toDate(event.recurrence.until),
697
+ exdates: event.recurrence.exdates?.map((d) => toDate(d))
698
+ }
699
+ } : event;
700
+ }
701
+ normalizedEvent.set(event, result);
702
+ return result;
703
+ }
704
+ function needsRecurrenceCopy(event) {
705
+ const r = event.recurrence;
706
+ if (!r) return false;
707
+ return r.until !== void 0 && !(r.until instanceof Date) || !!r.exdates?.some((d) => !(d instanceof Date));
708
+ }
709
+ function normalizeEvents(events) {
710
+ const cached = normalizedList.get(events);
711
+ if (cached) return cached;
712
+ const seen = /* @__PURE__ */ new Set();
713
+ const result = [];
714
+ for (const event of events) {
715
+ const normalized = normalizeEvent(event);
716
+ if (!normalized) continue;
717
+ if (seen.has(normalized.id)) {
718
+ warnOnce(`duplicate-id:${normalized.id}`, `Duplicate event id "${normalized.id}": ids must be unique.`);
719
+ }
720
+ seen.add(normalized.id);
721
+ result.push(normalized);
722
+ }
723
+ normalizedList.set(events, result);
724
+ return result;
725
+ }
726
+ var dayKey = (wall) => format6(wall, "yyyy-MM-dd");
727
+ function memoByZone(cache, events, tz, compute) {
728
+ let byZone = cache.get(events);
729
+ if (!byZone) {
730
+ byZone = /* @__PURE__ */ new Map();
731
+ cache.set(events, byZone);
732
+ }
733
+ const key = tz ?? "";
734
+ let value = byZone.get(key);
735
+ if (value === void 0) {
736
+ value = compute();
737
+ byZone.set(key, value);
738
+ }
739
+ return value;
740
+ }
741
+ var startDayCache = /* @__PURE__ */ new WeakMap();
742
+ function eventsByStartDay(events, tz) {
743
+ return memoByZone(startDayCache, events, tz, () => {
744
+ const map = /* @__PURE__ */ new Map();
745
+ for (const event of events) {
746
+ const key = dayKey(toDisplay(event.start, tz));
747
+ const bucket = map.get(key);
748
+ if (bucket) bucket.push(event);
749
+ else map.set(key, [event]);
750
+ }
751
+ return map;
752
+ });
753
+ }
754
+ var segmentCache = /* @__PURE__ */ new WeakMap();
755
+ function segmentsByDisplayDay(events, tz, range) {
756
+ const cacheKey = range ? `${tz ?? ""}|${range.start.getTime()}|${range.end.getTime()}` : tz;
757
+ return memoByZone(segmentCache, events, cacheKey, () => {
758
+ const map = /* @__PURE__ */ new Map();
759
+ for (const event of events) {
760
+ const wallStart = toDisplay(event.start, tz);
761
+ const rawEnd = toDisplay(event.end, tz);
762
+ const wallEnd = rawEnd < wallStart ? wallStart : rawEnd;
763
+ if (range && (wallEnd < range.start || wallStart >= range.end)) continue;
764
+ let day = startOfDay2(range && wallStart < range.start ? range.start : wallStart);
765
+ const stop = range && wallEnd > range.end ? range.end : wallEnd;
766
+ do {
767
+ const next = addDays(day, 1);
768
+ const start = wallStart > day ? wallStart : day;
769
+ const end = wallEnd < next ? wallEnd : next;
770
+ if (end > start || wallStart.getTime() === wallEnd.getTime()) {
771
+ const key = dayKey(day);
772
+ const bucket = map.get(key);
773
+ const segment = { event, start, end };
774
+ if (bucket) bucket.push(segment);
775
+ else map.set(key, [segment]);
776
+ }
777
+ day = next;
778
+ } while (day < stop);
779
+ }
780
+ return map;
781
+ });
782
+ }
429
783
 
430
784
  // src/components/dnd/DraggableEvent.tsx
431
785
  import React5 from "react";
432
786
  import { useDraggable } from "@dnd-kit/core";
433
787
  import { CSS } from "@dnd-kit/utilities";
434
- var DraggableEvent = ({ event, children, className, style: propStyle, ...props }) => {
788
+ var DraggableEventImpl = ({ event, dragId, grabOffsetMs, children, className, style: propStyle, ...props }) => {
435
789
  const dragDisabled = isEventDragDisabled(event);
436
790
  const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
437
- id: event.id,
438
- data: { event },
791
+ id: dragId ?? event.id,
792
+ data: { event, grabOffsetMs },
439
793
  disabled: dragDisabled
440
794
  });
441
795
  const style = {
442
796
  ...propStyle,
443
797
  transform: CSS.Translate.toString(transform),
444
798
  zIndex: isDragging ? 100 : propStyle?.zIndex,
445
- // When dragging, we want the ORIGINAL element to be almost invisible (or fully invisible)
446
- // so that the DragOverlay (the "ghost") is the only thing the user focuses on.
447
- // If we set opacity: 0, it disappears. If 0.1, it's a faint placeholder.
448
- // The user asked for "Make the entire dragged item transparent".
449
- // Usually this means the original item should be faded out while the drag overlay is moving.
799
+ // Hide the original while dragging: the DragOverlay ghost is what the user follows.
450
800
  opacity: isDragging ? 0 : propStyle?.opacity
451
- // Hiding original element completely
452
801
  };
453
802
  return /* @__PURE__ */ React5.createElement(
454
803
  "div",
@@ -463,33 +812,56 @@ var DraggableEvent = ({ event, children, className, style: propStyle, ...props }
463
812
  children
464
813
  );
465
814
  };
815
+ var DraggableEvent = React5.memo(DraggableEventImpl);
466
816
 
467
817
  // src/components/dnd/DroppableCell.tsx
468
818
  import React6 from "react";
469
819
  import { useDroppable } from "@dnd-kit/core";
470
- var DroppableCell = ({ id, date, resourceId, children, className, style, onClick }) => {
820
+ var QUARTER_HOVER = [
821
+ "hover:bg-blue-50/50 dark:hover:bg-blue-900/10",
822
+ "hover:bg-blue-50/80 dark:hover:bg-blue-900/20",
823
+ "hover:bg-blue-100/50 dark:hover:bg-blue-900/30",
824
+ "hover:bg-blue-100/80 dark:hover:bg-blue-900/40"
825
+ ];
826
+ var QUARTER_ACTIVE = [
827
+ "bg-blue-50/50 dark:bg-blue-900/10 ring-2 ring-primary ring-inset",
828
+ "bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-primary ring-inset",
829
+ "bg-blue-100/50 dark:bg-blue-900/30 ring-2 ring-primary ring-inset",
830
+ "bg-blue-100/80 dark:bg-blue-900/40 ring-2 ring-primary ring-inset"
831
+ ];
832
+ var quarterOf = (date) => Math.min(3, Math.floor(date.getMinutes() / 15));
833
+ var DroppableCellImpl = ({ id, date, resourceId, children, className, style, onClick, onSlotClick }) => {
471
834
  const { isOver, setNodeRef } = useDroppable({
472
835
  id,
473
836
  data: { date, resourceId }
474
837
  });
475
- const minutes = date.getMinutes();
476
- const quarterClass = minutes === 0 ? "hover:bg-blue-50/50 dark:hover:bg-blue-900/10" : minutes === 15 ? "hover:bg-blue-50/80 dark:hover:bg-blue-900/20" : minutes === 30 ? "hover:bg-blue-100/50 dark:hover:bg-blue-900/30" : "hover:bg-blue-100/80 dark:hover:bg-blue-900/40";
477
- const activeQuarterClass = minutes === 0 ? "bg-blue-50/50 dark:bg-blue-900/10 ring-2 ring-primary ring-inset" : minutes === 15 ? "bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-primary ring-inset" : minutes === 30 ? "bg-blue-100/50 dark:bg-blue-900/30 ring-2 ring-primary ring-inset" : "bg-blue-100/80 dark:bg-blue-900/40 ring-2 ring-primary ring-inset";
838
+ const quarter = quarterOf(date);
478
839
  return /* @__PURE__ */ React6.createElement(
479
840
  "div",
480
841
  {
481
842
  ref: setNodeRef,
482
843
  className: cn(
483
844
  className,
484
- quarterClass,
485
- isOver && activeQuarterClass
845
+ QUARTER_HOVER[quarter],
846
+ isOver && QUARTER_ACTIVE[quarter],
847
+ onSlotClick && "cursor-pointer"
486
848
  ),
487
849
  style,
488
- onClick
850
+ onClick: onSlotClick ? () => onSlotClick(date, resourceId) : onClick
489
851
  },
490
852
  children
491
853
  );
492
854
  };
855
+ var shallowEqualStyle = (a, b) => {
856
+ if (a === b) return true;
857
+ if (!a || !b) return false;
858
+ const keys = Object.keys(a);
859
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
860
+ };
861
+ var DroppableCell = React6.memo(
862
+ DroppableCellImpl,
863
+ (prev, next) => prev.id === next.id && prev.date.getTime() === next.date.getTime() && prev.resourceId === next.resourceId && prev.children === next.children && prev.className === next.className && prev.onClick === next.onClick && prev.onSlotClick === next.onSlotClick && shallowEqualStyle(prev.style, next.style)
864
+ );
493
865
 
494
866
  // src/views/MonthView.tsx
495
867
  var EventItem = React7.memo(({ event, onEventClick }) => /* @__PURE__ */ React7.createElement(DraggableEvent, { event }, /* @__PURE__ */ React7.createElement(
@@ -521,41 +893,21 @@ var MonthView = ({
521
893
  timezone,
522
894
  locale
523
895
  }) => {
524
- const weekStartsOn = locale?.options?.weekStartsOn ?? 1;
525
- const days = useMemo(
526
- () => getMonthGrid(currentDate, weekStartsOn),
527
- [currentDate, weekStartsOn]
528
- );
529
- const weekDays = useMemo(() => {
530
- const start = startOfWeek2(currentDate, { weekStartsOn });
531
- const end = endOfWeek2(currentDate, { weekStartsOn });
532
- return eachDayOfInterval2({ start, end });
533
- }, [currentDate, weekStartsOn]);
534
- const getZonedDate = useCallback((date) => {
535
- return timezone ? toZonedTime2(date, timezone) : date;
536
- }, [timezone]);
537
- const eventsByDay = useMemo(() => {
538
- const map = /* @__PURE__ */ new Map();
539
- events.forEach((event) => {
540
- const zonedStart = getZonedDate(event.start);
541
- const key = format5(zonedStart, "yyyy-MM-dd");
542
- if (!map.has(key)) {
543
- map.set(key, []);
544
- }
545
- map.get(key).push(event);
546
- });
547
- return map;
548
- }, [events, getZonedDate]);
896
+ const weekStartsOn = resolveWeekStartsOn(locale, 1);
897
+ const days = useMemo(() => getMonthGrid(currentDate, weekStartsOn), [currentDate, weekStartsOn]);
898
+ const weekDays = useMemo(() => getWeekDays(currentDate, weekStartsOn), [currentDate, weekStartsOn]);
899
+ const eventsByDay = eventsByStartDay(events, timezone);
900
+ const today = displayToday(timezone);
549
901
  return /* @__PURE__ */ React7.createElement("div", { className: "flex flex-col h-full bg-background border-[0.5px] scrollbar-hide border-border/50 rounded-2xl overflow-hidden min-w-[800px] md:min-w-0 shadow-sm" }, /* @__PURE__ */ React7.createElement("div", { className: "overflow-y-auto flex-1 relative" }, /* @__PURE__ */ React7.createElement("div", { className: "grid grid-cols-7 border-b-[0.5px] border-border/50 bg-gradient-to-r from-muted/30 via-muted/40 to-muted/30 sticky top-0 z-20 backdrop-blur-sm" }, weekDays.map((day) => /* @__PURE__ */ React7.createElement(
550
902
  "div",
551
903
  {
552
904
  key: day.toISOString(),
553
905
  className: "py-3 text-center text-xs font-semibold text-muted-foreground border-r-[0.5px] border-border/30 last:border-r-0 uppercase tracking-wider"
554
906
  },
555
- format5(day, "EEE", { locale })
907
+ format7(day, "EEE", { locale })
556
908
  ))), /* @__PURE__ */ React7.createElement("div", { className: "grid grid-cols-7", style: { gridAutoRows: "130px" } }, days.map((day) => {
557
- const dayKey = format5(day, "yyyy-MM-dd");
558
- const dayEvents = eventsByDay.get(dayKey) || [];
909
+ const dayKey2 = format7(day, "yyyy-MM-dd");
910
+ const dayEvents = eventsByDay.get(dayKey2) || [];
559
911
  const isCurrentMonth = isSameMonth2(day, currentDate);
560
912
  const cellId = day.toISOString();
561
913
  return /* @__PURE__ */ React7.createElement(
@@ -567,26 +919,87 @@ var MonthView = ({
567
919
  className: cn(
568
920
  "h-[130px] p-2 border-b-[0.5px] border-r-[0.5px] border-border/30 last:border-r-0 relative transition-all duration-200 hover:bg-accent/5 flex flex-col gap-1.5 overflow-hidden group",
569
921
  !isCurrentMonth && "bg-muted/5 text-muted-foreground/60",
570
- isToday2(day) && "bg-primary/5 ring-1 ring-inset ring-primary/20"
922
+ isSameDay3(day, today) && "bg-primary/5 ring-1 ring-inset ring-primary/20"
571
923
  ),
572
924
  onClick: () => onDateClick?.(day)
573
925
  },
574
926
  /* @__PURE__ */ React7.createElement("div", { className: "flex justify-between items-start" }, /* @__PURE__ */ React7.createElement("div", { className: cn(
575
927
  "text-sm font-semibold w-7 h-7 flex items-center justify-center rounded-full transition-all duration-200",
576
- isToday2(day) ? "bg-primary text-primary-foreground shadow-md shadow-primary/30" : "group-hover:bg-accent"
577
- ) }, format5(day, "d", { locale })), dayEvents.length > 0 && /* @__PURE__ */ React7.createElement("div", { className: "text-[10px] font-medium text-muted-foreground/60 bg-muted/50 px-1.5 py-0.5 rounded-full" }, dayEvents.length)),
578
- /* @__PURE__ */ React7.createElement("div", { className: "flex-1 flex flex-col gap-1 scrollbar-hide overflow-y-auto overflow-x-hidden" }, dayEvents.slice(0, 4).map((event) => /* @__PURE__ */ React7.createElement(EventItem, { key: `${event.id}-${dayKey}`, event, onEventClick })), dayEvents.length > 4 && /* @__PURE__ */ React7.createElement("div", { className: "text-[10px] text-primary font-semibold text-center py-1 px-2 rounded-md bg-primary/5 hover:bg-primary/10 cursor-pointer transition-colors" }, "+", dayEvents.length - 4, " more"))
928
+ isSameDay3(day, today) ? "bg-primary text-primary-foreground shadow-md shadow-primary/30" : "group-hover:bg-accent"
929
+ ) }, format7(day, "d", { locale })), dayEvents.length > 0 && /* @__PURE__ */ React7.createElement("div", { className: "text-[10px] font-medium text-muted-foreground/60 bg-muted/50 px-1.5 py-0.5 rounded-full" }, dayEvents.length)),
930
+ /* @__PURE__ */ React7.createElement("div", { className: "flex-1 flex flex-col gap-1 scrollbar-hide overflow-y-auto overflow-x-hidden" }, dayEvents.slice(0, 4).map((event) => /* @__PURE__ */ React7.createElement(EventItem, { key: `${event.id}-${dayKey2}`, event, onEventClick })), dayEvents.length > 4 && /* @__PURE__ */ React7.createElement("div", { className: "text-[10px] text-primary font-semibold text-center py-1 px-2 rounded-md bg-primary/5 hover:bg-primary/10 cursor-pointer transition-colors" }, "+", dayEvents.length - 4, " more"))
579
931
  );
580
932
  }))));
581
933
  };
582
934
 
583
935
  // src/views/WeekView.tsx
584
936
  import React9 from "react";
585
- import { format as format6, isSameDay as isSameDay2, differenceInMinutes, isToday as isToday3, startOfWeek as startOfWeek3, endOfWeek as endOfWeek3, eachDayOfInterval as eachDayOfInterval3 } from "date-fns";
586
- import { toZonedTime as toZonedTime3 } from "date-fns-tz";
937
+ import { addDays as addDays2, format as format8, differenceInMinutes, isSameDay as isSameDay4 } from "date-fns";
938
+
939
+ // src/core/layout.ts
940
+ var layoutCache = /* @__PURE__ */ new WeakMap();
941
+ function layoutDayEvents(segments) {
942
+ const cached = layoutCache.get(segments);
943
+ if (cached) return cached;
944
+ const sorted = [...segments].sort(
945
+ (a, b) => a.start.getTime() - b.start.getTime() || b.end.getTime() - a.end.getTime() || (a.event.id < b.event.id ? -1 : a.event.id > b.event.id ? 1 : 0)
946
+ );
947
+ const result = [];
948
+ let cluster = [];
949
+ let columnEnds = [];
950
+ let clusterEnd = -Infinity;
951
+ const closeCluster = () => {
952
+ for (const placed of cluster) placed.columns = columnEnds.length;
953
+ cluster = [];
954
+ columnEnds = [];
955
+ };
956
+ for (const segment of sorted) {
957
+ const start = segment.start.getTime();
958
+ const end = Math.max(segment.end.getTime(), start + 1);
959
+ if (start >= clusterEnd) closeCluster();
960
+ let column = columnEnds.findIndex((columnEnd) => columnEnd <= start);
961
+ if (column === -1) {
962
+ column = columnEnds.length;
963
+ columnEnds.push(end);
964
+ } else {
965
+ columnEnds[column] = end;
966
+ }
967
+ const placed = { ...segment, column, columns: 1 };
968
+ cluster.push(placed);
969
+ result.push(placed);
970
+ clusterEnd = Math.max(clusterEnd, end);
971
+ }
972
+ closeCluster();
973
+ layoutCache.set(segments, result);
974
+ return result;
975
+ }
976
+
977
+ // src/components/dnd/ResizableEvent.tsx
978
+ import React8, { useState as useState2, useRef, useCallback } from "react";
979
+
980
+ // src/core/dnd.ts
981
+ function computeDrop({ event, overDate, overResourceId, grabOffsetMs = 0, view, tz }) {
982
+ let wallStart;
983
+ if (view === "month") {
984
+ const original = toDisplay(event.start, tz);
985
+ wallStart = new Date(overDate);
986
+ wallStart.setHours(original.getHours(), original.getMinutes(), original.getSeconds(), original.getMilliseconds());
987
+ } else {
988
+ wallStart = new Date(overDate.getTime() - grabOffsetMs);
989
+ wallStart.setSeconds(0, 0);
990
+ }
991
+ const start = fromDisplay(wallStart, tz);
992
+ const end = new Date(start.getTime() + (event.end.getTime() - event.start.getTime()));
993
+ const resourceId = overResourceId ?? event.resourceId;
994
+ if (start.getTime() === event.start.getTime() && resourceId === event.resourceId) return null;
995
+ return { start, end, resourceId };
996
+ }
997
+ function computeResizeEnd(event, deltaMinutes, minDuration = 15) {
998
+ const minEnd = event.start.getTime() + minDuration * 6e4;
999
+ return new Date(Math.max(event.end.getTime() + deltaMinutes * 6e4, minEnd));
1000
+ }
587
1001
 
588
1002
  // src/components/dnd/ResizableEvent.tsx
589
- import React8, { useState as useState2, useRef, useCallback as useCallback2 } from "react";
590
1003
  var ResizableEvent = ({
591
1004
  event,
592
1005
  hourHeight,
@@ -604,7 +1017,7 @@ var ResizableEvent = ({
604
1017
  const startYRef = useRef(0);
605
1018
  const startHeightRef = useRef(0);
606
1019
  const resizeHeightRef = useRef(null);
607
- const handleResizeStart = useCallback2((e) => {
1020
+ const handleResizeStart = useCallback((e) => {
608
1021
  e.preventDefault();
609
1022
  e.stopPropagation();
610
1023
  const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
@@ -628,14 +1041,8 @@ var ResizableEvent = ({
628
1041
  setIsResizing(false);
629
1042
  const finalHeight = resizeHeightRef.current;
630
1043
  if (finalHeight !== null && containerRef.current && onResize) {
631
- const originalHeight = startHeightRef.current;
632
- const heightDiff = finalHeight - originalHeight;
633
- const minutesDiff = heightDiff / hourHeight * 60;
634
- const newEnd = new Date(event.end);
635
- newEnd.setMinutes(newEnd.getMinutes() + minutesDiff);
636
- if (newEnd > event.start) {
637
- onResize(event, newEnd);
638
- }
1044
+ const minutesDiff = (finalHeight - startHeightRef.current) / hourHeight * 60;
1045
+ onResize(event, computeResizeEnd(event, minutesDiff, minDuration));
639
1046
  }
640
1047
  resizeHeightRef.current = null;
641
1048
  setResizeHeight(null);
@@ -649,7 +1056,7 @@ var ResizableEvent = ({
649
1056
  document.addEventListener("touchmove", handleMove, { passive: false });
650
1057
  document.addEventListener("touchend", handleEnd);
651
1058
  }, [event, hourHeight, minDuration, onResize]);
652
- const handlePointerDown = useCallback2((e) => {
1059
+ const handlePointerDown = useCallback((e) => {
653
1060
  e.stopPropagation();
654
1061
  }, []);
655
1062
  return /* @__PURE__ */ React8.createElement(
@@ -686,7 +1093,16 @@ var ResizableEvent = ({
686
1093
  };
687
1094
 
688
1095
  // src/views/WeekView.tsx
689
- var WeekView = ({
1096
+ var HOUR_HEIGHT = 60;
1097
+ var HOURS = Array.from({ length: 24 }, (_, i) => i);
1098
+ var QUARTERS = [0, 15, 30, 45];
1099
+ var NO_SEGMENTS = [];
1100
+ var slotDate = (day, hour, minute) => {
1101
+ const date = new Date(day);
1102
+ date.setHours(hour, minute, 0, 0);
1103
+ return date;
1104
+ };
1105
+ var WeekViewImpl = ({
690
1106
  currentDate,
691
1107
  events,
692
1108
  onEventClick,
@@ -696,14 +1112,16 @@ var WeekView = ({
696
1112
  locale,
697
1113
  readonly
698
1114
  }) => {
699
- const weekStartsOn = locale?.options?.weekStartsOn ?? 1;
700
- const start = startOfWeek3(currentDate, { weekStartsOn });
701
- const end = endOfWeek3(currentDate, { weekStartsOn });
702
- const weekDays = eachDayOfInterval3({ start, end });
703
- const hours = Array.from({ length: 24 }, (_, i) => i);
704
- const hourHeight = 60;
1115
+ const weekDays = React9.useMemo(
1116
+ () => getWeekDays(currentDate, resolveWeekStartsOn(locale, 1)),
1117
+ [currentDate, locale]
1118
+ );
1119
+ const visibleRange = React9.useMemo(
1120
+ () => ({ start: weekDays[0], end: addDays2(weekDays[weekDays.length - 1], 1) }),
1121
+ [weekDays]
1122
+ );
705
1123
  const scrollContainerRef = React9.useRef(null);
706
- const [now, setNow] = React9.useState(/* @__PURE__ */ new Date());
1124
+ const [now, setNow] = React9.useState(() => /* @__PURE__ */ new Date());
707
1125
  React9.useEffect(() => {
708
1126
  const interval = setInterval(() => setNow(/* @__PURE__ */ new Date()), 6e4);
709
1127
  return () => clearInterval(interval);
@@ -711,132 +1129,93 @@ var WeekView = ({
711
1129
  React9.useEffect(() => {
712
1130
  if (scrollContainerRef.current) {
713
1131
  const offsetHour = 4;
714
- scrollContainerRef.current.scrollTop = ((/* @__PURE__ */ new Date()).getHours() - offsetHour) * hourHeight;
715
- }
716
- }, []);
717
- const getZonedDate = (date) => {
718
- return timezone ? toZonedTime3(date, timezone) : date;
719
- };
720
- const zonedNow = getZonedDate(/* @__PURE__ */ new Date());
721
- const getTimezoneDisplay = (tz) => {
722
- const date = now;
723
- let displayTime = "";
724
- let acronym = "";
725
- if (!tz) {
726
- displayTime = format6(date, "HH:mm");
727
- try {
728
- acronym = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(date).find((part) => part.type === "timeZoneName")?.value || "";
729
- } catch {
730
- acronym = "LOC";
731
- }
732
- } else {
733
- try {
734
- const zDate = toZonedTime3(date, tz);
735
- displayTime = format6(zDate, "HH:mm");
736
- acronym = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(date).find((part) => part.type === "timeZoneName")?.value || "";
737
- } catch {
738
- displayTime = format6(date, "HH:mm");
739
- acronym = tz;
740
- }
1132
+ scrollContainerRef.current.scrollTop = (toDisplay(/* @__PURE__ */ new Date(), timezone).getHours() - offsetHour) * HOUR_HEIGHT;
741
1133
  }
742
- return /* @__PURE__ */ React9.createElement("div", { className: "flex flex-col items-center justify-center leading-tight" }, /* @__PURE__ */ React9.createElement("span", null, displayTime), /* @__PURE__ */ React9.createElement("span", { className: "text-[10px] opacity-75" }, "(", acronym, ")"));
743
- };
744
- const is24h = uses24HourClock(locale);
745
- const timeFormat = is24h ? "H:mm" : "h a";
746
- const eventTimeFormat = is24h ? "H:mm" : "h:mm a";
747
- const nowFormat = is24h ? "H:mm" : "h:mm";
1134
+ }, [timezone]);
1135
+ const segmentsByDay = segmentsByDisplayDay(events, timezone, visibleRange);
1136
+ const today = displayToday(timezone, now);
1137
+ const zonedNow = toDisplay(now, timezone);
1138
+ const nowTop = minutesSinceDisplayMidnight(now, timezone) / 60 * HOUR_HEIGHT;
1139
+ const showsToday = weekDays.some((day) => isSameDay4(day, today));
1140
+ const zone = resolveTimeZone(timezone);
1141
+ const zoneLabel = timeZoneAbbreviation(now, zone) || zone || "LOC";
1142
+ const formats = timeFormats(locale);
748
1143
  return /* @__PURE__ */ React9.createElement("div", { className: "flex flex-col h-full bg-background border-[0.5px] border-border/50 rounded-2xl overflow-hidden min-w-[800px] md:min-w-0 shadow-sm" }, /* @__PURE__ */ React9.createElement(
749
1144
  "div",
750
1145
  {
751
1146
  ref: scrollContainerRef,
752
1147
  className: "flex-1 overflow-y-auto scrollbar-hide relative bg-background scroll-smooth"
753
1148
  },
754
- /* @__PURE__ */ React9.createElement("div", { className: "flex border-b-[0.5px] border-border/50 bg-gradient-to-r from-muted/20 via-background to-muted/20 z-20 sticky top-0 backdrop-blur-sm" }, /* @__PURE__ */ React9.createElement("div", { className: "flex-none p-3 text-center text-xs font-semibold text-muted-foreground w-16 flex items-center justify-center border-r-[0.5px] border-border/30 bg-muted/10" }, getTimezoneDisplay(timezone)), /* @__PURE__ */ React9.createElement("div", { className: "flex-1 grid grid-cols-7" }, weekDays.map((day, index) => /* @__PURE__ */ React9.createElement("div", { key: day.toISOString(), className: cn("py-3 px-2 text-center", index > 0 && "border-l-[0.5px] border-border/30") }, /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-1.5" }, format6(day, "EEE", { locale })), /* @__PURE__ */ React9.createElement("div", { className: cn(
1149
+ /* @__PURE__ */ React9.createElement("div", { className: "flex border-b-[0.5px] border-border/50 bg-gradient-to-r from-muted/20 via-background to-muted/20 z-20 sticky top-0 backdrop-blur-sm" }, /* @__PURE__ */ React9.createElement("div", { className: "flex-none p-3 text-center text-xs font-semibold text-muted-foreground w-16 flex items-center justify-center border-r-[0.5px] border-border/30 bg-muted/10" }, /* @__PURE__ */ React9.createElement("div", { className: "flex flex-col items-center justify-center leading-tight" }, /* @__PURE__ */ React9.createElement("span", null, format8(zonedNow, "HH:mm")), /* @__PURE__ */ React9.createElement("span", { className: "text-[10px] opacity-75" }, "(", zoneLabel, ")"))), /* @__PURE__ */ React9.createElement("div", { className: "flex-1 grid grid-cols-7" }, weekDays.map((day, index) => /* @__PURE__ */ React9.createElement("div", { key: day.toISOString(), className: cn("py-3 px-2 text-center", index > 0 && "border-l-[0.5px] border-border/30") }, /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-1.5" }, format8(day, "EEE", { locale })), /* @__PURE__ */ React9.createElement("div", { className: cn(
755
1150
  "w-9 h-9 flex items-center justify-center rounded-xl mx-auto text-sm font-semibold transition-all duration-200",
756
- isToday3(day) ? "bg-primary text-primary-foreground shadow-lg shadow-primary/30 scale-110" : "text-foreground hover:bg-accent/80"
757
- ) }, format6(day, "d", { locale })))))),
1151
+ isSameDay4(day, today) ? "bg-primary text-primary-foreground shadow-lg shadow-primary/30 scale-110" : "text-foreground hover:bg-accent/80"
1152
+ ) }, format8(day, "d", { locale })))))),
758
1153
  /* @__PURE__ */ React9.createElement(
759
1154
  "div",
760
1155
  {
761
1156
  className: "flex min-w-full relative",
762
- style: { height: hours.length * hourHeight }
1157
+ style: { height: HOURS.length * HOUR_HEIGHT }
763
1158
  },
764
- /* @__PURE__ */ React9.createElement("div", { className: "flex-none w-16 border-r-[0.5px] border-border/30 relative bg-muted/5" }, hours.map((hour) => /* @__PURE__ */ React9.createElement(
1159
+ /* @__PURE__ */ React9.createElement("div", { className: "flex-none w-16 border-r-[0.5px] border-border/30 relative bg-muted/5" }, HOURS.map((hour) => /* @__PURE__ */ React9.createElement(
765
1160
  "div",
766
1161
  {
767
1162
  key: hour,
768
1163
  className: "relative w-full text-[11px] text-muted-foreground/80 text-right pr-3 font-medium tabular-nums box-border",
769
- style: { height: hourHeight }
1164
+ style: { height: HOUR_HEIGHT }
770
1165
  },
771
- /* @__PURE__ */ React9.createElement("span", { className: "block -translate-y-1/2" }, hour !== 0 && format6((/* @__PURE__ */ new Date()).setHours(hour, 0, 0, 0), timeFormat, { locale }))
772
- ))),
1166
+ /* @__PURE__ */ React9.createElement("span", { className: "block -translate-y-1/2" }, hour !== 0 && format8(slotDate(currentDate, hour, 0), formats.hour, { locale }))
1167
+ )), showsToday && /* @__PURE__ */ React9.createElement(
1168
+ "div",
1169
+ {
1170
+ className: "absolute left-0 w-full pointer-events-none z-30 flex justify-end pr-2",
1171
+ style: { top: `${nowTop}px` }
1172
+ },
1173
+ /* @__PURE__ */ React9.createElement("span", { className: "text-[10px] font-bold text-primary-foreground bg-primary px-1.5 py-0.5 rounded-md shadow-md -translate-y-1/2 backdrop-blur-none" }, format8(zonedNow, formats.compact, { locale }))
1174
+ )),
773
1175
  /* @__PURE__ */ React9.createElement("div", { className: "flex-1 grid grid-cols-7 relative" }, weekDays.map((day, dayIndex) => {
774
- const dayEvents = events.filter((e) => {
775
- const zonedStart = getZonedDate(e.start);
776
- return isSameDay2(zonedStart, day);
777
- });
778
- return /* @__PURE__ */ React9.createElement("div", { key: day.toISOString(), className: cn("relative h-full", dayIndex > 0 && "border-l-[0.5px] border-border/30") }, hours.map((hour) => {
779
- return /* @__PURE__ */ React9.createElement(
780
- "div",
781
- {
782
- key: hour,
783
- className: "w-full border-b-[0.5px] border-dashed border-border/20 box-border relative hover:bg-accent/5 transition-colors",
784
- style: { height: hourHeight }
785
- },
786
- [0, 15, 30, 45].map((minute) => {
787
- const cellDate = new Date(day);
788
- cellDate.setHours(hour, minute, 0, 0);
789
- const cellId = cellDate.toISOString();
790
- return /* @__PURE__ */ React9.createElement(
791
- DroppableCell,
792
- {
793
- key: minute,
794
- id: cellId,
795
- date: cellDate,
796
- className: "w-full absolute left-0 right-0 z-0 transition-colors",
797
- style: {
798
- height: "25%",
799
- top: `${minute / 60 * 100}%`
800
- }
1176
+ const dayKey2 = format8(day, "yyyy-MM-dd");
1177
+ const positioned = layoutDayEvents(segmentsByDay.get(dayKey2) ?? NO_SEGMENTS);
1178
+ return /* @__PURE__ */ React9.createElement("div", { key: dayKey2, className: cn("relative h-full", dayIndex > 0 && "border-l-[0.5px] border-border/30") }, HOURS.map((hour) => /* @__PURE__ */ React9.createElement(
1179
+ "div",
1180
+ {
1181
+ key: hour,
1182
+ className: "w-full border-b-[0.5px] border-dashed border-border/20 box-border relative hover:bg-accent/5 transition-colors",
1183
+ style: { height: HOUR_HEIGHT }
1184
+ },
1185
+ QUARTERS.map((minute) => {
1186
+ const cellDate = slotDate(day, hour, minute);
1187
+ return /* @__PURE__ */ React9.createElement(
1188
+ DroppableCell,
1189
+ {
1190
+ key: minute,
1191
+ id: cellDate.toISOString(),
1192
+ date: cellDate,
1193
+ className: "w-full absolute left-0 right-0 z-0 transition-colors",
1194
+ style: {
1195
+ height: "25%",
1196
+ top: `${minute / 60 * 100}%`
801
1197
  },
802
- /* @__PURE__ */ React9.createElement(
803
- "div",
804
- {
805
- className: "w-full h-full bg-transparent cursor-pointer",
806
- onClick: () => onTimeSlotClick?.(cellDate)
807
- }
808
- )
809
- );
810
- })
811
- );
812
- }), dayEvents.map((event) => {
813
- const overlappingEvents = dayEvents.filter((e) => {
814
- if (e.id === event.id) return false;
815
- const s1 = getZonedDate(event.start).getTime();
816
- const e1 = getZonedDate(event.end).getTime();
817
- const s2 = getZonedDate(e.start).getTime();
818
- const e2 = getZonedDate(e.end).getTime();
819
- return s1 < e2 && e1 > s2;
820
- });
821
- const group = [event, ...overlappingEvents].sort(
822
- (a, b) => getZonedDate(a.start).getTime() - getZonedDate(b.start).getTime() || (a.id > b.id ? 1 : -1)
823
- // stable sort
824
- );
825
- const index = group.findIndex((e) => e.id === event.id);
826
- const count = group.length;
827
- const widthPercent = 100 / count;
828
- const leftPercent = index * widthPercent;
829
- const zonedEventStart = getZonedDate(event.start);
830
- const zonedEventEnd = getZonedDate(event.end);
831
- const startMinutes = zonedEventStart.getHours() * 60 + zonedEventStart.getMinutes();
832
- const durationMinutes = differenceInMinutes(zonedEventEnd, zonedEventStart);
833
- const top = startMinutes / 60 * hourHeight;
834
- const height = durationMinutes / 60 * hourHeight;
1198
+ onSlotClick: onTimeSlotClick
1199
+ }
1200
+ );
1201
+ })
1202
+ )), positioned.map(({ event, start, end, column, columns }) => {
1203
+ const widthPercent = 100 / columns;
1204
+ const leftPercent = column * widthPercent;
1205
+ const zonedEventStart = toDisplay(event.start, timezone);
1206
+ const zonedEventEnd = toDisplay(event.end, timezone);
1207
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
1208
+ const durationMinutes = differenceInMinutes(end, start);
1209
+ const top = startMinutes / 60 * HOUR_HEIGHT;
1210
+ const height = durationMinutes / 60 * HOUR_HEIGHT;
835
1211
  const isShortEvent = durationMinutes < 60;
1212
+ const isLastSegment = end.getTime() === zonedEventEnd.getTime();
836
1213
  return /* @__PURE__ */ React9.createElement(
837
1214
  DraggableEvent,
838
1215
  {
839
- key: `${event.id}-${day.toISOString()}`,
1216
+ key: `${event.id}-${dayKey2}`,
1217
+ dragId: `${event.id}@${dayKey2}`,
1218
+ grabOffsetMs: start.getTime() - zonedEventStart.getTime(),
840
1219
  event,
841
1220
  className: `absolute z-10 transition-all ${readonly ? "cursor-default" : ""}`,
842
1221
  style: {
@@ -845,15 +1224,15 @@ var WeekView = ({
845
1224
  left: `${leftPercent}%`,
846
1225
  width: `${widthPercent}%`,
847
1226
  // Add minimal spacing between overlapping events
848
- paddingRight: count > 1 ? "2px" : "0"
1227
+ paddingRight: columns > 1 ? "2px" : "0"
849
1228
  }
850
1229
  },
851
1230
  /* @__PURE__ */ React9.createElement(
852
1231
  ResizableEvent,
853
1232
  {
854
- readonly,
1233
+ readonly: readonly || !isLastSegment,
855
1234
  event,
856
- hourHeight,
1235
+ hourHeight: HOUR_HEIGHT,
857
1236
  onResize: onEventResize,
858
1237
  className: "h-full",
859
1238
  style: { height: "100%" }
@@ -868,7 +1247,7 @@ var WeekView = ({
868
1247
  !event.color && "border-primary/20 bg-primary/10",
869
1248
  isShortEvent ? "px-1 flex items-center justify-center" : "p-2",
870
1249
  // Add active border for overlapped events to distinguish them
871
- count > 1 && "border-l-4 border-l-primary/50"
1250
+ columns > 1 && "border-l-4 border-l-primary/50"
872
1251
  ),
873
1252
  style: {
874
1253
  height: "100%",
@@ -881,46 +1260,43 @@ var WeekView = ({
881
1260
  e.stopPropagation();
882
1261
  onEventClick?.(event);
883
1262
  },
884
- title: count > 1 ? `${event.title} (${index + 1}/${count})` : void 0
1263
+ title: columns > 1 ? `${event.title} (${column + 1}/${columns})` : void 0
885
1264
  },
886
1265
  /* @__PURE__ */ React9.createElement("div", { className: "flex flex-col h-full overflow-hidden w-full" }, /* @__PURE__ */ React9.createElement("div", { className: cn(
887
1266
  "font-semibold truncate text-foreground/90 leading-tight",
888
1267
  isShortEvent ? "text-xs text-center" : "text-xs"
889
- ) }, event.title), !isShortEvent && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] text-muted-foreground truncate mt-0.5 font-medium leading-tight" }, format6(zonedEventStart, eventTimeFormat, { locale }), " - ", format6(zonedEventEnd, eventTimeFormat, { locale })), event.description && height > 50 && /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] text-muted-foreground/80 truncate mt-1 leading-tight opacity-80" }, event.description)), count > 1 && !isShortEvent && /* @__PURE__ */ React9.createElement("div", { className: "absolute top-1 right-1 bg-background/80 backdrop-blur-sm rounded-full w-4 h-4 flex items-center justify-center text-[9px] font-bold text-muted-foreground border border-border shadow-sm" }, count))
1268
+ ) }, event.title), !isShortEvent && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] text-muted-foreground truncate mt-0.5 font-medium leading-tight" }, format8(zonedEventStart, formats.time, { locale }), " - ", format8(zonedEventEnd, formats.time, { locale })), event.description && height > 50 && /* @__PURE__ */ React9.createElement("div", { className: "text-[10px] text-muted-foreground/80 truncate mt-1 leading-tight opacity-80" }, event.description)), columns > 1 && !isShortEvent && /* @__PURE__ */ React9.createElement("div", { className: "absolute top-1 right-1 bg-background/80 backdrop-blur-sm rounded-full w-4 h-4 flex items-center justify-center text-[9px] font-bold text-muted-foreground border border-border shadow-sm" }, columns))
890
1269
  )
891
1270
  )
892
1271
  );
893
- }), isToday3(day) && /* @__PURE__ */ React9.createElement(
1272
+ }), isSameDay4(day, today) && /* @__PURE__ */ React9.createElement(
894
1273
  "div",
895
1274
  {
896
1275
  className: "absolute left-0 right-0 z-20 pointer-events-none flex items-center",
897
- style: {
898
- top: `${(zonedNow.getHours() * 60 + zonedNow.getMinutes()) / 60 * hourHeight}px`
899
- }
1276
+ style: { top: `${nowTop}px` }
900
1277
  },
901
1278
  /* @__PURE__ */ React9.createElement("div", { className: "h-[2px] w-full bg-gradient-to-r from-primary via-primary to-primary/50" }),
902
1279
  /* @__PURE__ */ React9.createElement("div", { className: "absolute -left-1.5 w-3 h-3 bg-primary rounded-full shadow-lg shadow-primary/40 ring-2 ring-background animate-pulse" })
903
1280
  ));
904
1281
  }))
905
- ),
906
- /* @__PURE__ */ React9.createElement(
907
- "div",
908
- {
909
- className: "absolute left-0 w-16 pointer-events-none z-30 flex justify-end pr-2",
910
- style: {
911
- top: `${(zonedNow.getHours() * 60 + zonedNow.getMinutes()) / 60 * hourHeight + 80}px`
912
- }
913
- },
914
- /* @__PURE__ */ React9.createElement("span", { className: "text-[10px] font-bold text-primary-foreground bg-primary px-1.5 py-0.5 rounded-md shadow-md -translate-y-1/2 backdrop-blur-none" }, format6(zonedNow, nowFormat, { locale }))
915
1282
  )
916
1283
  ));
917
1284
  };
1285
+ var WeekView = React9.memo(WeekViewImpl);
918
1286
 
919
1287
  // src/views/DayView.tsx
920
1288
  import React10 from "react";
921
- import { format as format7, differenceInMinutes as differenceInMinutes2, isToday as isToday4, isSameDay as isSameDay3 } from "date-fns";
922
- import { toZonedTime as toZonedTime4 } from "date-fns-tz";
923
- var DayView = ({
1289
+ import { addDays as addDays3, format as format9, differenceInMinutes as differenceInMinutes2, isSameDay as isSameDay5, startOfDay as startOfDay3 } from "date-fns";
1290
+ var HOUR_HEIGHT2 = 80;
1291
+ var HOURS2 = Array.from({ length: 24 }, (_, i) => i);
1292
+ var QUARTERS2 = [0, 15, 30, 45];
1293
+ var NO_SEGMENTS2 = [];
1294
+ var slotDate2 = (day, hour, minute) => {
1295
+ const date = new Date(day);
1296
+ date.setHours(hour, minute, 0, 0);
1297
+ return date;
1298
+ };
1299
+ var DayViewImpl = ({
924
1300
  currentDate,
925
1301
  events,
926
1302
  onEventClick,
@@ -930,10 +1306,12 @@ var DayView = ({
930
1306
  locale,
931
1307
  readonly
932
1308
  }) => {
933
- const hours = Array.from({ length: 24 }, (_, i) => i);
934
- const hourHeight = 80;
1309
+ const visibleRange = React10.useMemo(() => {
1310
+ const start = startOfDay3(currentDate);
1311
+ return { start, end: addDays3(start, 1) };
1312
+ }, [currentDate]);
935
1313
  const scrollContainerRef = React10.useRef(null);
936
- const [now, setNow] = React10.useState(/* @__PURE__ */ new Date());
1314
+ const [now, setNow] = React10.useState(() => /* @__PURE__ */ new Date());
937
1315
  React10.useEffect(() => {
938
1316
  const interval = setInterval(() => setNow(/* @__PURE__ */ new Date()), 6e4);
939
1317
  return () => clearInterval(interval);
@@ -941,99 +1319,71 @@ var DayView = ({
941
1319
  React10.useEffect(() => {
942
1320
  if (scrollContainerRef.current) {
943
1321
  const offsetHour = 3;
944
- scrollContainerRef.current.scrollTop = ((/* @__PURE__ */ new Date()).getHours() - offsetHour) * hourHeight;
1322
+ scrollContainerRef.current.scrollTop = (toDisplay(/* @__PURE__ */ new Date(), timezone).getHours() - offsetHour) * HOUR_HEIGHT2;
945
1323
  }
946
- }, []);
947
- const getZonedDate = (date) => {
948
- return timezone ? toZonedTime4(date, timezone) : date;
949
- };
950
- const zonedNow = getZonedDate(now);
951
- const dayEvents = events.filter((e) => {
952
- const zonedStart = getZonedDate(e.start);
953
- return isSameDay3(zonedStart, currentDate);
954
- });
955
- const is24h = uses24HourClock(locale);
956
- const timeFormat = is24h ? "H:mm" : "h a";
957
- const eventTimeFormat = is24h ? "H:mm" : "h:mm a";
958
- const nowFormat = is24h ? "H:mm" : "h:mm a";
959
- return /* @__PURE__ */ React10.createElement("div", { className: "flex flex-col h-full bg-background border-[0.5px] border-border/50 rounded-2xl overflow-hidden shadow-sm" }, /* @__PURE__ */ React10.createElement("div", { className: "px-6 py-4 border-b-[0.5px] border-border/50 bg-gradient-to-r from-muted/20 via-background to-muted/20 text-center shrink-0" }, /* @__PURE__ */ React10.createElement("div", { className: "flex items-center justify-center gap-3" }, /* @__PURE__ */ React10.createElement("h2", { className: "text-xl font-semibold capitalize text-foreground" }, format7(currentDate, "EEEE, MMMM d, yyyy", { locale })), isToday4(currentDate) && /* @__PURE__ */ React10.createElement("span", { className: "text-xs font-semibold bg-primary text-primary-foreground px-3 py-1 rounded-full shadow-md shadow-primary/20" }, "Today"))), /* @__PURE__ */ React10.createElement("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto relative" }, /* @__PURE__ */ React10.createElement("div", { className: "flex relative", style: { height: hours.length * hourHeight } }, /* @__PURE__ */ React10.createElement("div", { className: "w-20 bg-muted/5 border-r-[0.5px] border-border/30 relative" }, hours.map((hour) => /* @__PURE__ */ React10.createElement(
1324
+ }, [timezone]);
1325
+ const dayKey2 = format9(currentDate, "yyyy-MM-dd");
1326
+ const positioned = layoutDayEvents(segmentsByDisplayDay(events, timezone, visibleRange).get(dayKey2) ?? NO_SEGMENTS2);
1327
+ const isToday2 = isSameDay5(currentDate, displayToday(timezone, now));
1328
+ const zonedNow = toDisplay(now, timezone);
1329
+ const nowTop = minutesSinceDisplayMidnight(now, timezone) / 60 * HOUR_HEIGHT2;
1330
+ const formats = timeFormats(locale);
1331
+ return /* @__PURE__ */ React10.createElement("div", { className: "flex flex-col h-full bg-background border-[0.5px] border-border/50 rounded-2xl overflow-hidden shadow-sm" }, /* @__PURE__ */ React10.createElement("div", { className: "px-6 py-4 border-b-[0.5px] border-border/50 bg-gradient-to-r from-muted/20 via-background to-muted/20 text-center shrink-0" }, /* @__PURE__ */ React10.createElement("div", { className: "flex items-center justify-center gap-3" }, /* @__PURE__ */ React10.createElement("h2", { className: "text-xl font-semibold capitalize text-foreground" }, format9(currentDate, "EEEE, MMMM d, yyyy", { locale })), isToday2 && /* @__PURE__ */ React10.createElement("span", { className: "text-xs font-semibold bg-primary text-primary-foreground px-3 py-1 rounded-full shadow-md shadow-primary/20" }, "Today"))), /* @__PURE__ */ React10.createElement("div", { ref: scrollContainerRef, className: "flex-1 overflow-y-auto relative" }, /* @__PURE__ */ React10.createElement("div", { className: "flex relative", style: { height: HOURS2.length * HOUR_HEIGHT2 } }, /* @__PURE__ */ React10.createElement("div", { className: "w-20 bg-muted/5 border-r-[0.5px] border-border/30 relative" }, HOURS2.map((hour) => /* @__PURE__ */ React10.createElement(
960
1332
  "div",
961
1333
  {
962
1334
  key: hour,
963
1335
  className: "relative w-full",
964
- style: { height: hourHeight }
1336
+ style: { height: HOUR_HEIGHT2 }
965
1337
  },
966
- hour !== 0 && /* @__PURE__ */ React10.createElement("span", { className: "absolute w-full text-center -top-3 left-1/2 -translate-x-1/2 text-[11px] text-muted-foreground/80 font-medium tabular-nums bg-background px-1.5 py-0.5 rounded-md" }, format7((/* @__PURE__ */ new Date()).setHours(hour, 0, 0, 0), timeFormat, { locale }))
967
- )), isToday4(currentDate) && /* @__PURE__ */ React10.createElement(
1338
+ hour !== 0 && /* @__PURE__ */ React10.createElement("span", { className: "absolute w-full text-center -top-3 left-1/2 -translate-x-1/2 text-[11px] text-muted-foreground/80 font-medium tabular-nums bg-background px-1.5 py-0.5 rounded-md" }, format9(slotDate2(currentDate, hour, 0), formats.hour, { locale }))
1339
+ )), isToday2 && /* @__PURE__ */ React10.createElement(
968
1340
  "div",
969
1341
  {
970
1342
  className: "absolute left-0 w-full pointer-events-none z-30 flex justify-end pr-2",
971
- style: {
972
- top: `${(zonedNow.getHours() * 60 + zonedNow.getMinutes()) / 60 * hourHeight}px`
973
- }
1343
+ style: { top: `${nowTop}px` }
974
1344
  },
975
- /* @__PURE__ */ React10.createElement("span", { className: "text-[10px] font-bold text-white bg-primary px-2 py-1 rounded-lg shadow-md shadow-primary/30 -translate-y-1/2" }, format7(zonedNow, nowFormat, { locale }))
976
- )), /* @__PURE__ */ React10.createElement("div", { className: "flex-1 relative" }, hours.map((hour) => {
977
- return /* @__PURE__ */ React10.createElement(
978
- "div",
979
- {
980
- key: hour,
981
- className: "border-b-[0.5px] border-dashed border-border/20 box-border relative hover:bg-accent/5 transition-colors",
982
- style: { height: hourHeight }
983
- },
984
- [0, 15, 30, 45].map((minute) => {
985
- const cellDate = new Date(currentDate);
986
- cellDate.setHours(hour, minute, 0, 0);
987
- const cellId = cellDate.toISOString();
988
- return /* @__PURE__ */ React10.createElement(
989
- DroppableCell,
990
- {
991
- key: minute,
992
- id: cellId,
993
- date: cellDate,
994
- className: "w-full absolute left-0 right-0 z-0 transition-colors",
995
- style: {
996
- height: "25%",
997
- top: `${minute / 60 * 100}%`
998
- }
1345
+ /* @__PURE__ */ React10.createElement("span", { className: "text-[10px] font-bold text-white bg-primary px-2 py-1 rounded-lg shadow-md shadow-primary/30 -translate-y-1/2" }, format9(zonedNow, formats.time, { locale }))
1346
+ )), /* @__PURE__ */ React10.createElement("div", { className: "flex-1 relative" }, HOURS2.map((hour) => /* @__PURE__ */ React10.createElement(
1347
+ "div",
1348
+ {
1349
+ key: hour,
1350
+ className: "border-b-[0.5px] border-dashed border-border/20 box-border relative hover:bg-accent/5 transition-colors",
1351
+ style: { height: HOUR_HEIGHT2 }
1352
+ },
1353
+ QUARTERS2.map((minute) => {
1354
+ const cellDate = slotDate2(currentDate, hour, minute);
1355
+ return /* @__PURE__ */ React10.createElement(
1356
+ DroppableCell,
1357
+ {
1358
+ key: minute,
1359
+ id: cellDate.toISOString(),
1360
+ date: cellDate,
1361
+ className: "w-full absolute left-0 right-0 z-0 transition-colors",
1362
+ style: {
1363
+ height: "25%",
1364
+ top: `${minute / 60 * 100}%`
999
1365
  },
1000
- /* @__PURE__ */ React10.createElement(
1001
- "div",
1002
- {
1003
- className: "h-full w-full bg-transparent cursor-pointer",
1004
- onClick: () => onTimeSlotClick?.(cellDate)
1005
- }
1006
- )
1007
- );
1008
- })
1009
- );
1010
- }), dayEvents.map((event) => {
1011
- const overlappingEvents = dayEvents.filter((e) => {
1012
- if (e.id === event.id) return false;
1013
- const s1 = getZonedDate(event.start).getTime();
1014
- const e1 = getZonedDate(event.end).getTime();
1015
- const s2 = getZonedDate(e.start).getTime();
1016
- const e2 = getZonedDate(e.end).getTime();
1017
- return s1 < e2 && e1 > s2;
1018
- });
1019
- const group = [event, ...overlappingEvents].sort(
1020
- (a, b) => getZonedDate(a.start).getTime() - getZonedDate(b.start).getTime() || (a.id > b.id ? 1 : -1)
1021
- );
1022
- const index = group.findIndex((e) => e.id === event.id);
1023
- const count = group.length;
1024
- const widthPercent = 100 / count;
1025
- const leftPercent = index * widthPercent;
1026
- const zonedStart = getZonedDate(event.start);
1027
- const zonedEnd = getZonedDate(event.end);
1028
- const startMinutes = zonedStart.getHours() * 60 + zonedStart.getMinutes();
1029
- const durationMinutes = differenceInMinutes2(zonedEnd, zonedStart);
1030
- const top = startMinutes / 60 * hourHeight;
1031
- const height = durationMinutes / 60 * hourHeight;
1366
+ onSlotClick: onTimeSlotClick
1367
+ }
1368
+ );
1369
+ })
1370
+ )), positioned.map(({ event, start, end, column, columns }) => {
1371
+ const widthPercent = 100 / columns;
1372
+ const leftPercent = column * widthPercent;
1373
+ const zonedStart = toDisplay(event.start, timezone);
1374
+ const zonedEnd = toDisplay(event.end, timezone);
1375
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
1376
+ const durationMinutes = differenceInMinutes2(end, start);
1377
+ const top = startMinutes / 60 * HOUR_HEIGHT2;
1378
+ const height = durationMinutes / 60 * HOUR_HEIGHT2;
1032
1379
  const isShortEvent = durationMinutes < 45;
1380
+ const isLastSegment = end.getTime() === zonedEnd.getTime();
1033
1381
  return /* @__PURE__ */ React10.createElement(
1034
1382
  DraggableEvent,
1035
1383
  {
1036
- key: `${event.id}-${currentDate.toISOString()}`,
1384
+ key: `${event.id}-${dayKey2}`,
1385
+ dragId: `${event.id}@${dayKey2}`,
1386
+ grabOffsetMs: start.getTime() - zonedStart.getTime(),
1037
1387
  event,
1038
1388
  className: `absolute z-10 transition-all ${readonly ? "cursor-default" : ""}`,
1039
1389
  style: {
@@ -1041,15 +1391,15 @@ var DayView = ({
1041
1391
  height: `${Math.max(height, 28)}px`,
1042
1392
  left: `calc(${leftPercent}% + 4px)`,
1043
1393
  width: `calc(${widthPercent}% - 8px)`,
1044
- paddingRight: count > 1 ? "2px" : "0"
1394
+ paddingRight: columns > 1 ? "2px" : "0"
1045
1395
  }
1046
1396
  },
1047
1397
  /* @__PURE__ */ React10.createElement(
1048
1398
  ResizableEvent,
1049
1399
  {
1050
- readonly,
1400
+ readonly: readonly || !isLastSegment,
1051
1401
  event,
1052
- hourHeight,
1402
+ hourHeight: HOUR_HEIGHT2,
1053
1403
  onResize: onEventResize,
1054
1404
  className: "h-full",
1055
1405
  style: { height: "100%" }
@@ -1063,44 +1413,43 @@ var DayView = ({
1063
1413
  readonly || isEventDragDisabled(event) ? "cursor-default" : "cursor-grab active:cursor-grabbing",
1064
1414
  !event.color && "bg-primary/10 border-primary/20",
1065
1415
  isShortEvent ? "px-2 flex items-center" : "px-3 py-2",
1066
- count > 1 && "border-l-4"
1416
+ columns > 1 && "border-l-4"
1067
1417
  ),
1068
1418
  style: {
1069
1419
  backgroundColor: event.color ? `${event.color}15` : void 0,
1070
1420
  borderColor: event.color ? `${event.color}30` : void 0,
1071
1421
  borderLeftColor: event.color || "var(--primary)",
1072
- borderLeftWidth: count > 1 ? "4px" : "3px"
1422
+ borderLeftWidth: columns > 1 ? "4px" : "3px"
1073
1423
  },
1074
1424
  onClick: (e) => {
1075
1425
  e.stopPropagation();
1076
1426
  onEventClick?.(event);
1077
1427
  },
1078
- title: count > 1 ? `${event.title} (${index + 1}/${count})` : void 0
1428
+ title: columns > 1 ? `${event.title} (${column + 1}/${columns})` : void 0
1079
1429
  },
1080
1430
  /* @__PURE__ */ React10.createElement("div", { className: "flex flex-col h-full overflow-hidden w-full" }, /* @__PURE__ */ React10.createElement("div", { className: cn(
1081
1431
  "font-semibold truncate leading-tight",
1082
1432
  isShortEvent ? "text-xs" : "text-sm",
1083
1433
  event.color ? "text-foreground" : "text-foreground/90"
1084
- ) }, event.title), !isShortEvent && /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement("div", { className: "text-xs text-muted-foreground mt-0.5 font-medium" }, format7(zonedStart, eventTimeFormat, { locale }), " - ", format7(zonedEnd, eventTimeFormat, { locale })), event.description && height > 60 && /* @__PURE__ */ React10.createElement("div", { className: "text-xs text-muted-foreground/80 mt-1 line-clamp-2" }, event.description)), count > 1 && !isShortEvent && /* @__PURE__ */ React10.createElement("div", { className: "absolute top-1.5 right-1.5 bg-background/90 backdrop-blur-sm rounded-full w-5 h-5 flex items-center justify-center text-[10px] font-bold text-muted-foreground border border-border shadow-sm" }, count))
1434
+ ) }, event.title), !isShortEvent && /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement("div", { className: "text-xs text-muted-foreground mt-0.5 font-medium" }, format9(zonedStart, formats.time, { locale }), " - ", format9(zonedEnd, formats.time, { locale })), event.description && height > 60 && /* @__PURE__ */ React10.createElement("div", { className: "text-xs text-muted-foreground/80 mt-1 line-clamp-2" }, event.description)), columns > 1 && !isShortEvent && /* @__PURE__ */ React10.createElement("div", { className: "absolute top-1.5 right-1.5 bg-background/90 backdrop-blur-sm rounded-full w-5 h-5 flex items-center justify-center text-[10px] font-bold text-muted-foreground border border-border shadow-sm" }, columns))
1085
1435
  )
1086
1436
  )
1087
1437
  );
1088
- }), isToday4(currentDate) && /* @__PURE__ */ React10.createElement(
1438
+ }), isToday2 && /* @__PURE__ */ React10.createElement(
1089
1439
  "div",
1090
1440
  {
1091
1441
  className: "absolute left-0 right-0 z-20 pointer-events-none flex items-center",
1092
- style: {
1093
- top: `${(zonedNow.getHours() * 60 + zonedNow.getMinutes()) / 60 * hourHeight}px`
1094
- }
1442
+ style: { top: `${nowTop}px` }
1095
1443
  },
1096
1444
  /* @__PURE__ */ React10.createElement("div", { className: "h-[2px] w-full bg-gradient-to-r from-primary via-primary to-primary/50" }),
1097
1445
  /* @__PURE__ */ React10.createElement("div", { className: "absolute -left-1.5 w-3 h-3 bg-primary rounded-full shadow-lg shadow-primary/40 ring-2 ring-background animate-pulse" })
1098
1446
  )))));
1099
1447
  };
1448
+ var DayView = React10.memo(DayViewImpl);
1100
1449
 
1101
1450
  // src/views/AgendaView.tsx
1102
1451
  import React12, { useMemo as useMemo2 } from "react";
1103
- import { format as format8, isSameDay as isSameDay4, isToday as isToday5, isTomorrow, addDays, startOfDay, differenceInMinutes as differenceInMinutes3 } from "date-fns";
1452
+ import { format as format10, isSameDay as isSameDay6, addDays as addDays4, startOfDay as startOfDay4, differenceInMinutes as differenceInMinutes3 } from "date-fns";
1104
1453
 
1105
1454
  // src/components/EmptyState.tsx
1106
1455
  import React11 from "react";
@@ -1126,10 +1475,10 @@ var formatDuration = (start, end) => {
1126
1475
  if (remainingMinutes === 0) return `${hours}h`;
1127
1476
  return `${hours}h ${remainingMinutes}m`;
1128
1477
  };
1129
- var getDateLabel = (date, locale, translations) => {
1130
- if (isToday5(date)) return translations?.today || "Today";
1131
- if (isTomorrow(date)) return translations?.tomorrow || "Tomorrow";
1132
- return format8(date, "EEEE", { locale });
1478
+ var getDateLabel = (date, today, locale, translations) => {
1479
+ if (isSameDay6(date, today)) return translations?.today || "Today";
1480
+ if (isSameDay6(date, addDays4(today, 1))) return translations?.tomorrow || "Tomorrow";
1481
+ return format10(date, "EEEE", { locale });
1133
1482
  };
1134
1483
  var AgendaView = ({
1135
1484
  currentDate,
@@ -1137,24 +1486,24 @@ var AgendaView = ({
1137
1486
  onEventClick,
1138
1487
  onCreateEvent,
1139
1488
  locale,
1140
- translations
1489
+ translations,
1490
+ timezone
1141
1491
  }) => {
1142
1492
  const is24h = uses24HourClock(locale);
1143
- const timePattern = is24h ? "H:mm" : "h:mm a";
1144
- const compactTimePattern = is24h ? "H:mm" : "h:mm";
1493
+ const { time: timePattern, compact: compactTimePattern } = timeFormats(locale);
1494
+ const today = displayToday(timezone);
1145
1495
  const groupedEvents = useMemo2(() => {
1146
- const startDate = startOfDay(currentDate);
1496
+ const byDay = eventsByStartDay(events, timezone);
1147
1497
  const groups = [];
1148
- const sortedEvents = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
1149
1498
  for (let i = 0; i < 30; i++) {
1150
- const day = addDays(startDate, i);
1151
- const dayEvents = sortedEvents.filter((event) => isSameDay4(event.start, day));
1152
- if (dayEvents.length > 0) {
1153
- groups.push({ date: day, events: dayEvents });
1499
+ const day = addDays4(startOfDay4(currentDate), i);
1500
+ const dayEvents = byDay.get(format10(day, "yyyy-MM-dd"));
1501
+ if (dayEvents?.length) {
1502
+ groups.push({ date: day, events: [...dayEvents].sort((a, b) => a.start.getTime() - b.start.getTime()) });
1154
1503
  }
1155
1504
  }
1156
1505
  return groups;
1157
- }, [currentDate, events]);
1506
+ }, [currentDate, events, timezone]);
1158
1507
  const container = {
1159
1508
  hidden: { opacity: 0 },
1160
1509
  show: {
@@ -1185,11 +1534,11 @@ var AgendaView = ({
1185
1534
  },
1186
1535
  /* @__PURE__ */ React12.createElement("div", { className: "sticky top-0 bg-background/95 backdrop-blur-md py-4 z-10 border-b border-border/50" }, /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-4" }, /* @__PURE__ */ React12.createElement("div", { className: cn(
1187
1536
  "flex flex-col items-center justify-center w-16 h-16 rounded-2xl transition-all",
1188
- isToday5(group.date) ? "bg-primary text-primary-foreground shadow-lg shadow-primary/30" : "bg-muted/50 text-foreground"
1189
- ) }, /* @__PURE__ */ React12.createElement("span", { className: "text-2xl font-bold leading-none" }, format8(group.date, "d")), /* @__PURE__ */ React12.createElement("span", { className: "text-xs font-medium uppercase tracking-wide opacity-80" }, format8(group.date, "MMM", { locale }))), /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React12.createElement("span", { className: cn(
1537
+ isSameDay6(group.date, today) ? "bg-primary text-primary-foreground shadow-lg shadow-primary/30" : "bg-muted/50 text-foreground"
1538
+ ) }, /* @__PURE__ */ React12.createElement("span", { className: "text-2xl font-bold leading-none" }, format10(group.date, "d")), /* @__PURE__ */ React12.createElement("span", { className: "text-xs font-medium uppercase tracking-wide opacity-80" }, format10(group.date, "MMM", { locale }))), /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React12.createElement("span", { className: cn(
1190
1539
  "text-lg font-semibold",
1191
- isToday5(group.date) && "text-primary"
1192
- ) }, getDateLabel(group.date, locale, translations)), /* @__PURE__ */ React12.createElement("span", { className: "text-sm text-muted-foreground" }, group.events.length, " ", group.events.length !== 1 ? translations?.eventPlural || "events" : translations?.eventSingular || "event")))),
1540
+ isSameDay6(group.date, today) && "text-primary"
1541
+ ) }, getDateLabel(group.date, today, locale, translations)), /* @__PURE__ */ React12.createElement("span", { className: "text-sm text-muted-foreground" }, group.events.length, " ", group.events.length !== 1 ? translations?.eventPlural || "events" : translations?.eventSingular || "event")))),
1193
1542
  /* @__PURE__ */ React12.createElement("div", { className: "py-4 space-y-3" }, group.events.map((event) => /* @__PURE__ */ React12.createElement(
1194
1543
  motion.div,
1195
1544
  {
@@ -1211,14 +1560,14 @@ var AgendaView = ({
1211
1560
  style: { backgroundColor: event.color || "var(--primary)" }
1212
1561
  }
1213
1562
  ),
1214
- /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center min-w-[70px] pl-2" }, event.allDay ? /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center" }, /* @__PURE__ */ React12.createElement("span", { className: "text-xs font-semibold text-muted-foreground bg-muted/80 px-2.5 py-1 rounded-full" }, translations?.allDay || "All Day")) : /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center" }, /* @__PURE__ */ React12.createElement("span", { className: "text-base font-semibold text-foreground" }, format8(event.start, compactTimePattern, { locale })), !is24h && /* @__PURE__ */ React12.createElement("span", { className: "text-xs text-muted-foreground uppercase" }, format8(event.start, "a")), /* @__PURE__ */ React12.createElement("div", { className: "w-px h-3 bg-border my-1" }), /* @__PURE__ */ React12.createElement("span", { className: "text-xs text-muted-foreground/70 font-medium" }, formatDuration(event.start, event.end)))),
1563
+ /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center min-w-[70px] pl-2" }, event.allDay ? /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center" }, /* @__PURE__ */ React12.createElement("span", { className: "text-xs font-semibold text-muted-foreground bg-muted/80 px-2.5 py-1 rounded-full" }, translations?.allDay || "All Day")) : /* @__PURE__ */ React12.createElement("div", { className: "flex flex-col items-center" }, /* @__PURE__ */ React12.createElement("span", { className: "text-base font-semibold text-foreground" }, format10(toDisplay(event.start, timezone), compactTimePattern, { locale })), !is24h && /* @__PURE__ */ React12.createElement("span", { className: "text-xs text-muted-foreground uppercase" }, format10(toDisplay(event.start, timezone), "a")), /* @__PURE__ */ React12.createElement("div", { className: "w-px h-3 bg-border my-1" }), /* @__PURE__ */ React12.createElement("span", { className: "text-xs text-muted-foreground/70 font-medium" }, formatDuration(event.start, event.end)))),
1215
1564
  /* @__PURE__ */ React12.createElement("div", { className: "flex-1 min-w-0 space-y-2" }, /* @__PURE__ */ React12.createElement("div", { className: "flex items-start justify-between gap-2" }, /* @__PURE__ */ React12.createElement("h4", { className: "text-base font-semibold text-foreground group-hover:text-primary transition-colors line-clamp-1" }, event.title), /* @__PURE__ */ React12.createElement(
1216
1565
  "div",
1217
1566
  {
1218
1567
  className: "w-3 h-3 rounded-full shrink-0 mt-1.5",
1219
1568
  style: { backgroundColor: event.color || "var(--primary)" }
1220
1569
  }
1221
- )), event.description && /* @__PURE__ */ React12.createElement("p", { className: "text-sm text-muted-foreground line-clamp-2 leading-relaxed" }, event.description), /* @__PURE__ */ React12.createElement("div", { className: "flex flex-wrap items-center gap-3 pt-1" }, !event.allDay && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Clock, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, format8(event.start, timePattern, { locale }), " - ", format8(event.end, timePattern, { locale }))), event.guests && event.guests.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Users, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.guests.length, " ", event.guests.length !== 1 ? translations?.guestsCount || "guests" : translations?.guestCount || "guest")), event.attachments && event.attachments.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Paperclip, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.attachments.length)), event.reminders && event.reminders.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Bell, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.reminders.length)))),
1570
+ )), event.description && /* @__PURE__ */ React12.createElement("p", { className: "text-sm text-muted-foreground line-clamp-2 leading-relaxed" }, event.description), /* @__PURE__ */ React12.createElement("div", { className: "flex flex-wrap items-center gap-3 pt-1" }, !event.allDay && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Clock, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, format10(toDisplay(event.start, timezone), timePattern, { locale }), " - ", format10(toDisplay(event.end, timezone), timePattern, { locale }))), event.guests && event.guests.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Users, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.guests.length, " ", event.guests.length !== 1 ? translations?.guestsCount || "guests" : translations?.guestCount || "guest")), event.attachments && event.attachments.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Paperclip, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.attachments.length)), event.reminders && event.reminders.length > 0 && /* @__PURE__ */ React12.createElement("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground" }, /* @__PURE__ */ React12.createElement(Bell, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ React12.createElement("span", null, event.reminders.length)))),
1222
1571
  /* @__PURE__ */ React12.createElement("div", { className: "absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" }, /* @__PURE__ */ React12.createElement("svg", { className: "w-5 h-5 text-muted-foreground", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React12.createElement("path", { d: "M9 18l6-6-6-6" })))
1223
1572
  )))
1224
1573
  ))
@@ -1227,23 +1576,24 @@ var AgendaView = ({
1227
1576
 
1228
1577
  // src/views/ResourceView.tsx
1229
1578
  import React13, { useRef as useRef2 } from "react";
1230
- import { format as format9, startOfDay as startOfDay2, differenceInMinutes as differenceInMinutes4, isSameDay as isSameDay5 } from "date-fns";
1579
+ import { format as format11, startOfDay as startOfDay5, differenceInMinutes as differenceInMinutes4 } from "date-fns";
1231
1580
  var ResourceView = ({
1232
1581
  currentDate,
1233
1582
  events,
1234
1583
  resources,
1235
1584
  onEventClick,
1236
1585
  onTimeSlotClick,
1237
- locale
1586
+ locale,
1587
+ timezone
1238
1588
  }) => {
1239
1589
  const containerRef = useRef2(null);
1240
1590
  const hours = Array.from({ length: 24 }, (_, i) => i);
1241
1591
  const hourWidth = 100;
1242
- const timeFormat = uses24HourClock(locale) ? "H:mm" : "h a";
1592
+ const timeFormat = timeFormats(locale).hour;
1243
1593
  const getEventStyle = (event) => {
1244
- const start = new Date(event.start);
1245
- const end = new Date(event.end);
1246
- const dayStart = startOfDay2(currentDate);
1594
+ const start = toDisplay(event.start, timezone);
1595
+ const end = toDisplay(event.end, timezone);
1596
+ const dayStart = startOfDay5(currentDate);
1247
1597
  const startMinutes = differenceInMinutes4(start, dayStart);
1248
1598
  const durationMinutes = differenceInMinutes4(end, start);
1249
1599
  const left = startMinutes / 60 * hourWidth;
@@ -1261,10 +1611,10 @@ var ResourceView = ({
1261
1611
  className: "border-r border-border/50 text-xs text-muted-foreground p-2 font-medium shrink-0",
1262
1612
  style: { width: hourWidth }
1263
1613
  },
1264
- format9((/* @__PURE__ */ new Date()).setHours(hour, 0, 0, 0), timeFormat, { locale })
1614
+ format11((/* @__PURE__ */ new Date()).setHours(hour, 0, 0, 0), timeFormat, { locale })
1265
1615
  ))))), /* @__PURE__ */ React13.createElement("div", { className: "flex-1 overflow-auto relative", ref: containerRef }, /* @__PURE__ */ React13.createElement("div", { className: "min-w-fit" }, resources.map((resource) => {
1266
1616
  const resourceEvents = events.filter(
1267
- (e) => e.resourceId === resource.id && isSameDay5(new Date(e.start), currentDate)
1617
+ (e) => e.resourceId === resource.id && isSameDisplayDay(e.start, currentDate, timezone)
1268
1618
  );
1269
1619
  return /* @__PURE__ */ React13.createElement("div", { key: resource.id, className: "flex border-b border-border min-h-[100px]" }, /* @__PURE__ */ React13.createElement("div", { className: "w-48 shrink-0 border-r border-border p-4 bg-background sticky left-0 z-10 flex items-center gap-3" }, resource.avatar ? /* @__PURE__ */ React13.createElement("img", { src: resource.avatar, alt: resource.label, className: "w-8 h-8 rounded-full object-cover" }) : /* @__PURE__ */ React13.createElement("div", { className: "w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold text-primary" }, resource.label.substring(0, 2).toUpperCase()), /* @__PURE__ */ React13.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React13.createElement("span", { className: "text-sm font-medium" }, resource.label), /* @__PURE__ */ React13.createElement("span", { className: "text-xs text-muted-foreground" }, "ID: ", resource.id))), /* @__PURE__ */ React13.createElement("div", { className: "relative flex", style: { width: hours.length * hourWidth } }, hours.map((hour) => /* @__PURE__ */ React13.createElement(
1270
1620
  "div",
@@ -1274,20 +1624,18 @@ var ResourceView = ({
1274
1624
  style: { width: hourWidth }
1275
1625
  },
1276
1626
  [0, 15, 30, 45].map((minute) => {
1277
- const slotDate = new Date(currentDate);
1278
- slotDate.setHours(hour, minute, 0, 0);
1279
- const slotId = `${resource.id}-${slotDate.toISOString()}`;
1627
+ const slotDate3 = new Date(currentDate);
1628
+ slotDate3.setHours(hour, minute, 0, 0);
1629
+ const slotId = `${resource.id}-${slotDate3.toISOString()}`;
1280
1630
  return /* @__PURE__ */ React13.createElement(
1281
1631
  DroppableCell,
1282
1632
  {
1283
1633
  key: minute,
1284
1634
  id: slotId,
1285
- date: slotDate,
1635
+ date: slotDate3,
1286
1636
  resourceId: resource.id,
1287
1637
  className: "h-full flex-1 border-r border-border/10 last:border-border/30 hover:bg-accent/10 transition-colors",
1288
- onClick: () => {
1289
- onTimeSlotClick?.(slotDate, resource.id);
1290
- }
1638
+ onSlotClick: onTimeSlotClick
1291
1639
  }
1292
1640
  );
1293
1641
  })
@@ -1373,7 +1721,7 @@ var Modal = ({
1373
1721
  };
1374
1722
 
1375
1723
  // src/components/EventModal.tsx
1376
- import { format as format10, differenceInMinutes as differenceInMinutes5 } from "date-fns";
1724
+ import { format as format12, differenceInMinutes as differenceInMinutes5 } from "date-fns";
1377
1725
  import { motion as motion2, AnimatePresence } from "framer-motion";
1378
1726
  import {
1379
1727
  Clock as Clock2,
@@ -1530,7 +1878,7 @@ var EventModal = ({
1530
1878
  };
1531
1879
  const formatDateForInput = (date) => {
1532
1880
  if (!date) return "";
1533
- return format10(date, "yyyy-MM-dd'T'HH:mm");
1881
+ return format12(date, "yyyy-MM-dd'T'HH:mm");
1534
1882
  };
1535
1883
  const handleDateChange = (field, value) => {
1536
1884
  const date = new Date(value);
@@ -1627,7 +1975,7 @@ var EventModal = ({
1627
1975
  /* @__PURE__ */ React15.createElement(X2, { className: "w-4 h-4" })
1628
1976
  ))),
1629
1977
  /* @__PURE__ */ React15.createElement("h1", { className: "text-2xl font-bold text-foreground mb-3 leading-tight" }, event?.title || "(No title)"),
1630
- /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-3 text-sm" }, /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-background/60 backdrop-blur-sm rounded-lg border border-border/30" }, /* @__PURE__ */ React15.createElement(Calendar2, { className: "w-4 h-4 text-muted-foreground" }), /* @__PURE__ */ React15.createElement("span", { className: "font-medium" }, event?.start && format10(new Date(event.start), "EEE, MMM d"))), /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-background/60 backdrop-blur-sm rounded-lg border border-border/30" }, /* @__PURE__ */ React15.createElement(Clock2, { className: "w-4 h-4 text-muted-foreground" }), /* @__PURE__ */ React15.createElement("span", { className: "font-medium" }, event?.start && format10(new Date(event.start), "h:mm a"), " \u2013 ", event?.end && format10(new Date(event.end), "h:mm a"))))
1978
+ /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-3 text-sm" }, /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-background/60 backdrop-blur-sm rounded-lg border border-border/30" }, /* @__PURE__ */ React15.createElement(Calendar2, { className: "w-4 h-4 text-muted-foreground" }), /* @__PURE__ */ React15.createElement("span", { className: "font-medium" }, event?.start && format12(new Date(event.start), "EEE, MMM d"))), /* @__PURE__ */ React15.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-background/60 backdrop-blur-sm rounded-lg border border-border/30" }, /* @__PURE__ */ React15.createElement(Clock2, { className: "w-4 h-4 text-muted-foreground" }), /* @__PURE__ */ React15.createElement("span", { className: "font-medium" }, event?.start && format12(new Date(event.start), "h:mm a"), " \u2013 ", event?.end && format12(new Date(event.end), "h:mm a"))))
1631
1979
  ), /* @__PURE__ */ React15.createElement("div", { className: "px-6 pb-6 space-y-4 overflow-y-auto flex-1" }, event?.location && /* @__PURE__ */ React15.createElement(
1632
1980
  motion2.div,
1633
1981
  {
@@ -2021,7 +2369,7 @@ var DayViewSkeleton = () => /* @__PURE__ */ React16.createElement("div", { class
2021
2369
  var AgendaViewSkeleton = () => /* @__PURE__ */ React16.createElement("div", { className: "h-full bg-background border border-border/50 rounded-2xl overflow-hidden p-6" }, /* @__PURE__ */ React16.createElement("div", { className: "space-y-6" }, Array.from({ length: 4 }).map((_, dayIdx) => /* @__PURE__ */ React16.createElement("div", { key: dayIdx }, /* @__PURE__ */ React16.createElement(Skeleton, { className: "h-5 w-40 mb-4" }), /* @__PURE__ */ React16.createElement("div", { className: "space-y-3" }, Array.from({ length: 2 + Math.floor(Math.random() * 3) }).map((_2, eventIdx) => /* @__PURE__ */ React16.createElement("div", { key: eventIdx, className: "flex items-center gap-4 p-3 rounded-xl bg-muted/10" }, /* @__PURE__ */ React16.createElement(Skeleton, { className: "h-10 w-10 rounded-lg" }), /* @__PURE__ */ React16.createElement("div", { className: "flex-1 space-y-2" }, /* @__PURE__ */ React16.createElement(Skeleton, { className: "h-4 w-3/4" }), /* @__PURE__ */ React16.createElement(Skeleton, { className: "h-3 w-1/2" })))))))));
2022
2370
 
2023
2371
  // src/components/ContextMenu.tsx
2024
- import React17, { useState as useState4, useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4 } from "react";
2372
+ import React17, { useState as useState4, useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4 } from "react";
2025
2373
  import { Edit3, Trash2 as Trash22, Copy } from "lucide-react";
2026
2374
  import { AnimatePresence as AnimatePresence2, motion as motion3 } from "framer-motion";
2027
2375
  var EventContextMenu = ({
@@ -2134,7 +2482,7 @@ var EventContextMenu = ({
2134
2482
  };
2135
2483
  var useEventContextMenu = () => {
2136
2484
  const [contextMenu, setContextMenu] = useState4({ event: null, position: null });
2137
- const openContextMenu = useCallback3((event, e) => {
2485
+ const openContextMenu = useCallback2((event, e) => {
2138
2486
  e.preventDefault();
2139
2487
  e.stopPropagation();
2140
2488
  setContextMenu({
@@ -2142,7 +2490,7 @@ var useEventContextMenu = () => {
2142
2490
  position: { x: e.clientX, y: e.clientY }
2143
2491
  });
2144
2492
  }, []);
2145
- const closeContextMenu = useCallback3(() => {
2493
+ const closeContextMenu = useCallback2(() => {
2146
2494
  setContextMenu({ event: null, position: null });
2147
2495
  }, []);
2148
2496
  return {
@@ -2153,146 +2501,6 @@ var useEventContextMenu = () => {
2153
2501
  };
2154
2502
  };
2155
2503
 
2156
- // src/locales/index.ts
2157
- var en = {
2158
- today: "Today",
2159
- month: "Month",
2160
- week: "Week",
2161
- day: "Day",
2162
- agenda: "Agenda",
2163
- resource: "Resources",
2164
- createEvent: "Create Event",
2165
- editEvent: "Edit Event",
2166
- delete: "Delete",
2167
- save: "Save",
2168
- cancel: "Cancel",
2169
- title: "Add title",
2170
- start: "Start",
2171
- end: "End",
2172
- allDay: "All Day",
2173
- description: "Description",
2174
- repeat: "Repeat",
2175
- noRepeat: "Does not repeat",
2176
- selectCalendar: "Select Calendar",
2177
- selectType: "Select Type",
2178
- daily: "Daily",
2179
- weekly: "Weekly",
2180
- monthly: "Monthly",
2181
- yearly: "Yearly",
2182
- search: "Search events...",
2183
- allTypes: "All Types",
2184
- timezone: "Time zone",
2185
- localTime: "Local Time",
2186
- create: "Create",
2187
- calendars: "Calendars",
2188
- event: "Event",
2189
- task: "Task",
2190
- appointmentSchedule: "Appointment schedule",
2191
- new: "New",
2192
- dateAndTime: "Date & Time",
2193
- whosJoining: "Who's joining?",
2194
- suggestedTimes: "Suggested times for guests",
2195
- viewSuggestions: "View suggestions",
2196
- whereWillItBe: "Where will it be?",
2197
- location: "Enter address",
2198
- descriptionAndAttachments: "Description",
2199
- dragAndDrop: "Drag & drop files or",
2200
- guests: "Guests",
2201
- addGuests: "Add guests",
2202
- addAttachment: "Upload files",
2203
- moreOptions: "More options",
2204
- doesNotRepeat: "Does not repeat",
2205
- locationHelpText: 'Enter a physical address (e.g. "123 Main St") to generate the map.',
2206
- notes: "Notes...",
2207
- guestCount: "guest",
2208
- guestsCount: "guests",
2209
- repeatFor: "Repeat for",
2210
- times: "times",
2211
- until: "Until",
2212
- endRepeat: "End repeat",
2213
- never: "Never",
2214
- afterOccurrences: "After occurrences",
2215
- onDate: "On date",
2216
- tomorrow: "Tomorrow",
2217
- edit: "Edit",
2218
- duplicate: "Duplicate",
2219
- eventSingular: "event",
2220
- eventPlural: "events",
2221
- allCaughtUp: "All caught up!",
2222
- noEventsScheduled: "No events scheduled",
2223
- noUpcoming: "Your schedule is clear",
2224
- noUpcomingHint: "No upcoming events to show. Create a new event to start planning."
2225
- };
2226
- var fr = {
2227
- today: "Aujourd'hui",
2228
- month: "Mois",
2229
- week: "Semaine",
2230
- day: "Jour",
2231
- agenda: "Agenda",
2232
- resource: "Ressources",
2233
- createEvent: "Cr\xE9er un \xE9v\xE9nement",
2234
- editEvent: "Modifier l'\xE9v\xE9nement",
2235
- delete: "Supprimer",
2236
- save: "Enregistrer",
2237
- cancel: "Annuler",
2238
- title: "Ajouter un titre",
2239
- start: "D\xE9but",
2240
- end: "Fin",
2241
- allDay: "Toute la journ\xE9e",
2242
- description: "Description",
2243
- repeat: "R\xE9p\xE9ter",
2244
- noRepeat: "Ne se r\xE9p\xE8te pas",
2245
- selectCalendar: "S\xE9lectionner un calendrier",
2246
- selectType: "S\xE9lectionner un type",
2247
- daily: "Quotidien",
2248
- weekly: "Hebdomadaire",
2249
- monthly: "Mensuel",
2250
- yearly: "Annuel",
2251
- search: "Rechercher...",
2252
- allTypes: "Tous les types",
2253
- timezone: "Fuseau horaire",
2254
- localTime: "Heure locale",
2255
- create: "Cr\xE9er",
2256
- calendars: "Calendriers",
2257
- event: "\xC9v\xE9nement",
2258
- task: "T\xE2che",
2259
- appointmentSchedule: "Planning de rendez-vous",
2260
- new: "Nouveau",
2261
- dateAndTime: "Date et heure",
2262
- whosJoining: "Qui participe ?",
2263
- suggestedTimes: "Horaires sugg\xE9r\xE9s",
2264
- viewSuggestions: "Voir les suggestions",
2265
- whereWillItBe: "O\xF9 aura lieu ?",
2266
- location: "Entrer une adresse",
2267
- descriptionAndAttachments: "Description",
2268
- dragAndDrop: "Glisser-d\xE9poser des fichiers ou",
2269
- guests: "Invit\xE9s",
2270
- addGuests: "Ajouter des invit\xE9s",
2271
- addAttachment: "T\xE9l\xE9charger des fichiers",
2272
- moreOptions: "Plus d'options",
2273
- doesNotRepeat: "Ne se r\xE9p\xE8te pas",
2274
- locationHelpText: 'Entrez une adresse physique (ex: "123 Rue Principale") pour g\xE9n\xE9rer la carte.',
2275
- notes: "Notes...",
2276
- guestCount: "invit\xE9",
2277
- guestsCount: "invit\xE9s",
2278
- repeatFor: "R\xE9p\xE9ter pendant",
2279
- times: "fois",
2280
- until: "Jusqu'au",
2281
- endRepeat: "Fin de r\xE9p\xE9tition",
2282
- never: "Jamais",
2283
- afterOccurrences: "Apr\xE8s occurrences",
2284
- onDate: "\xC0 la date",
2285
- tomorrow: "Demain",
2286
- edit: "Modifier",
2287
- duplicate: "Dupliquer",
2288
- eventSingular: "\xE9v\xE9nement",
2289
- eventPlural: "\xE9v\xE9nements",
2290
- allCaughtUp: "Tout est \xE0 jour !",
2291
- noEventsScheduled: "Aucun \xE9v\xE9nement pr\xE9vu",
2292
- noUpcoming: "Votre planning est libre",
2293
- noUpcomingHint: "Aucun \xE9v\xE9nement \xE0 venir. Cr\xE9ez un nouvel \xE9v\xE9nement pour commencer \xE0 planifier."
2294
- };
2295
-
2296
2504
  // src/lib/theme.ts
2297
2505
  function colorToHsl(color) {
2298
2506
  let r = 0, g = 0, b = 0;
@@ -2373,18 +2581,160 @@ function getThemeStyles(theme) {
2373
2581
  }
2374
2582
 
2375
2583
  // src/hooks/useCalendarLogic.ts
2376
- import { useState as useState5, useMemo as useMemo3 } from "react";
2584
+ import { useMemo as useMemo3, useState as useState5 } from "react";
2377
2585
  import {
2378
- addMonths as addMonths2,
2379
- subMonths as subMonths2,
2586
+ addDays as addDays5,
2587
+ addMonths as addMonths3,
2380
2588
  addWeeks,
2381
- subWeeks,
2382
- addDays as addDays2,
2589
+ endOfMonth as endOfMonth2,
2590
+ format as format13,
2591
+ startOfMonth as startOfMonth3,
2592
+ startOfWeek as startOfWeek2,
2383
2593
  subDays,
2384
- differenceInMilliseconds
2594
+ subMonths as subMonths2,
2595
+ subWeeks
2385
2596
  } from "date-fns";
2386
- import { toZonedTime as toZonedTime5, fromZonedTime } from "date-fns-tz";
2597
+
2598
+ // src/core/recurrence.ts
2599
+ import { addMonths as addMonths2, startOfMonth as startOfMonth2 } from "date-fns";
2387
2600
  import { RRule } from "rrule";
2601
+ var WEEKDAYS = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR, RRule.SA, RRule.SU];
2602
+ var toFloating = (instant, tz) => {
2603
+ const w = toDisplay(instant, tz);
2604
+ return new Date(Date.UTC(w.getFullYear(), w.getMonth(), w.getDate(), w.getHours(), w.getMinutes(), w.getSeconds(), w.getMilliseconds()));
2605
+ };
2606
+ var fromFloating = (floating, tz) => fromDisplay(
2607
+ new Date(
2608
+ floating.getUTCFullYear(),
2609
+ floating.getUTCMonth(),
2610
+ floating.getUTCDate(),
2611
+ floating.getUTCHours(),
2612
+ floating.getUTCMinutes(),
2613
+ floating.getUTCSeconds(),
2614
+ floating.getUTCMilliseconds()
2615
+ ),
2616
+ tz
2617
+ );
2618
+ var minuteKey = (date) => Math.floor(date.getTime() / 6e4);
2619
+ var FREQUENCIES = { DAILY: RRule.DAILY, WEEKLY: RRule.WEEKLY, MONTHLY: RRule.MONTHLY, YEARLY: RRule.YEARLY };
2620
+ function buildRule(event, tz) {
2621
+ const r = event.recurrence;
2622
+ const freq = FREQUENCIES[r.freq];
2623
+ if (freq === void 0) throw new Error(`unsupported frequency "${r.freq}"`);
2624
+ return new RRule({
2625
+ freq,
2626
+ interval: r.interval || 1,
2627
+ dtstart: toFloating(event.start, tz),
2628
+ until: r.until ? toFloating(r.until, tz) : void 0,
2629
+ count: r.count,
2630
+ byweekday: r.byweekday?.length ? r.byweekday.map((d) => WEEKDAYS[d]) : void 0
2631
+ });
2632
+ }
2633
+ function occurrencesInMonth(event, monthStart, tz) {
2634
+ const rule = buildRule(event, tz);
2635
+ const excluded = new Set((event.recurrence.exdates ?? []).map(minuteKey));
2636
+ const duration = event.end.getTime() - event.start.getTime();
2637
+ const from = toFloating(monthStart, tz);
2638
+ const to = toFloating(addMonths2(monthStart, 1), tz);
2639
+ const result = [];
2640
+ for (const floating of rule.between(from, to, true)) {
2641
+ if (floating.getTime() === to.getTime()) continue;
2642
+ const start = fromFloating(floating, tz);
2643
+ if (excluded.has(minuteKey(start))) continue;
2644
+ result.push({
2645
+ ...event,
2646
+ id: `${event.id}-${start.getTime()}`,
2647
+ originalEventId: event.id,
2648
+ occurrenceStart: start,
2649
+ start,
2650
+ end: new Date(start.getTime() + duration)
2651
+ });
2652
+ }
2653
+ return result;
2654
+ }
2655
+ var MAX_MONTHS_PER_EVENT = 12;
2656
+ var occurrenceCache = /* @__PURE__ */ new WeakMap();
2657
+ function cachedMonth(event, monthStart, tz) {
2658
+ let months = occurrenceCache.get(event);
2659
+ if (!months) {
2660
+ months = /* @__PURE__ */ new Map();
2661
+ occurrenceCache.set(event, months);
2662
+ }
2663
+ const key = `${monthStart.getFullYear()}-${monthStart.getMonth()}|${tz ?? ""}`;
2664
+ let occurrences = months.get(key);
2665
+ if (occurrences) {
2666
+ months.delete(key);
2667
+ } else {
2668
+ occurrences = occurrencesInMonth(event, monthStart, tz);
2669
+ if (months.size >= MAX_MONTHS_PER_EVENT) months.delete(months.keys().next().value);
2670
+ }
2671
+ months.set(key, occurrences);
2672
+ return occurrences;
2673
+ }
2674
+ function expandRecurrences(events, range, anchorTz) {
2675
+ const result = [];
2676
+ const firstMonth = startOfMonth2(toDisplay(range.start, anchorTz));
2677
+ const lastMonth = startOfMonth2(toDisplay(range.end, anchorTz));
2678
+ for (const event of events) {
2679
+ if (!event.recurrence) {
2680
+ result.push(event);
2681
+ continue;
2682
+ }
2683
+ try {
2684
+ for (let month = firstMonth; month <= lastMonth; month = addMonths2(month, 1)) {
2685
+ for (const occurrence of cachedMonth(event, fromDisplay(month, anchorTz), anchorTz)) {
2686
+ if (occurrence.end >= range.start && occurrence.start <= range.end) result.push(occurrence);
2687
+ }
2688
+ }
2689
+ } catch (error) {
2690
+ warnOnce(`invalid-recurrence:${event.id}`, `Event "${event.id}" has an invalid recurrence rule: ${String(error)}`);
2691
+ result.push(event);
2692
+ }
2693
+ }
2694
+ return result;
2695
+ }
2696
+ function stripOccurrenceFields(event) {
2697
+ const { originalEventId: _originalEventId, occurrenceStart: _occurrenceStart, ...rest } = event;
2698
+ return rest;
2699
+ }
2700
+ var shiftFloating = (instant, deltaMs, tz) => fromFloating(new Date(toFloating(instant, tz).getTime() + deltaMs), tz);
2701
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2702
+ var floatingDay = (floating) => Math.floor(floating.getTime() / DAY_MS);
2703
+ function shiftSeries(master, occurrence, newStart, newEnd, anchorTz) {
2704
+ const fromFloat = toFloating(occurrence.start, anchorTz);
2705
+ const toFloat = toFloating(newStart, anchorTz);
2706
+ const deltaMs = toFloat.getTime() - fromFloat.getTime();
2707
+ const dayDelta = floatingDay(toFloat) - floatingDay(fromFloat);
2708
+ const start = shiftFloating(master.start, deltaMs, anchorTz);
2709
+ const end = new Date(start.getTime() + (newEnd.getTime() - newStart.getTime()));
2710
+ const recurrence = master.recurrence && {
2711
+ ...master.recurrence,
2712
+ byweekday: master.recurrence.byweekday && dayDelta % 7 !== 0 ? master.recurrence.byweekday.map((d) => ((d + dayDelta) % 7 + 7) % 7) : master.recurrence.byweekday,
2713
+ exdates: master.recurrence.exdates?.map((d) => shiftFloating(d, deltaMs, anchorTz))
2714
+ };
2715
+ return stripOccurrenceFields({ ...master, start, end, recurrence });
2716
+ }
2717
+ function excludeOccurrence(master, occurrenceStart) {
2718
+ const recurrence = master.recurrence ?? { freq: "DAILY" };
2719
+ return { ...master, recurrence: { ...recurrence, exdates: [...recurrence.exdates ?? [], occurrenceStart] } };
2720
+ }
2721
+
2722
+ // src/hooks/useStableCallback.ts
2723
+ import { useCallback as useCallback3, useRef as useRef5 } from "react";
2724
+ function useStableCallback(fn) {
2725
+ const ref = useRef5(fn);
2726
+ ref.current = fn;
2727
+ return useCallback3((...args) => ref.current(...args), []);
2728
+ }
2729
+
2730
+ // src/hooks/useCalendarLogic.ts
2731
+ function expansionRange(monthStart) {
2732
+ return {
2733
+ start: startOfWeek2(startOfMonth3(subMonths2(monthStart, 1))),
2734
+ end: addDays5(endOfMonth2(addMonths3(monthStart, 1)), 37)
2735
+ };
2736
+ }
2737
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
2388
2738
  var useCalendarLogic = ({
2389
2739
  events,
2390
2740
  view: controlledView,
@@ -2395,69 +2745,44 @@ var useCalendarLogic = ({
2395
2745
  onEventUpdate,
2396
2746
  onEventCreate,
2397
2747
  onEventDelete,
2748
+ onEventDrop,
2749
+ onEventResize,
2750
+ onOccurrenceDelete,
2398
2751
  readOnly,
2399
2752
  timezone
2400
2753
  }) => {
2401
2754
  const [internalView, setInternalView] = useState5("week");
2402
- const [internalDate, setInternalDate] = useState5(/* @__PURE__ */ new Date());
2755
+ const [internalDate, setInternalDate] = useState5(() => /* @__PURE__ */ new Date());
2403
2756
  const [isSidebarOpen, setIsSidebarOpen] = useState5(true);
2404
2757
  const [isModalOpen, setIsModalOpen] = useState5(false);
2405
2758
  const [selectedEvent, setSelectedEvent] = useState5(null);
2406
2759
  const [modalInitialDate, setModalInitialDate] = useState5(void 0);
2760
+ const [modalInitialResourceId, setModalInitialResourceId] = useState5(void 0);
2407
2761
  const view = controlledView ?? internalView;
2408
2762
  const currentDate = controlledDate ?? internalDate;
2409
- const expandedEvents = useMemo3(() => {
2410
- const allEvents = [];
2411
- const rangeStart = subDays(currentDate, 365);
2412
- const rangeEnd = addDays2(currentDate, 365);
2413
- events.forEach((event) => {
2414
- if (event.recurrence) {
2415
- try {
2416
- const rule = new RRule({
2417
- freq: RRule[event.recurrence.freq],
2418
- interval: event.recurrence.interval || 1,
2419
- dtstart: new Date(event.start),
2420
- until: event.recurrence.until ? new Date(event.recurrence.until) : void 0,
2421
- count: event.recurrence.count
2422
- });
2423
- const dates = rule.between(rangeStart, rangeEnd);
2424
- dates.forEach((date) => {
2425
- const duration = differenceInMilliseconds(new Date(event.end), new Date(event.start));
2426
- allEvents.push({
2427
- ...event,
2428
- id: `${event.id}-${date.getTime()}`,
2429
- // Unique ID for each instance
2430
- originalEventId: event.id,
2431
- // Reference to original
2432
- start: date,
2433
- end: new Date(date.getTime() + duration)
2434
- });
2435
- });
2436
- } catch (e) {
2437
- console.error("Error parsing recurrence rule", e);
2438
- allEvents.push(event);
2439
- }
2440
- } else {
2441
- allEvents.push(event);
2442
- }
2443
- });
2444
- return allEvents;
2445
- }, [events, currentDate]);
2446
- const handleViewChange = (newView) => {
2763
+ const normalizedEvents = useMemo3(() => normalizeEvents(events), [events]);
2764
+ const monthKey = format13(currentDate, "yyyy-MM");
2765
+ const range = useMemo3(() => expansionRange(/* @__PURE__ */ new Date(`${monthKey}-01T00:00:00`)), [monthKey]);
2766
+ const expandedEvents = useMemo3(
2767
+ () => expandRecurrences(normalizedEvents, range, timezone),
2768
+ [normalizedEvents, range, timezone]
2769
+ );
2770
+ const masterOf = (occurrence) => normalizedEvents.find((e) => e.id === occurrence.originalEventId);
2771
+ const handleViewChange = useStableCallback((newView) => {
2447
2772
  if (controlledOnViewChange) {
2448
2773
  controlledOnViewChange(newView);
2449
2774
  } else {
2450
2775
  setInternalView(newView);
2451
2776
  }
2452
- };
2453
- const handleDateChange = (newDate) => {
2777
+ });
2778
+ const handleDateChange = useStableCallback((newDate) => {
2454
2779
  if (controlledOnDateChange) {
2455
2780
  controlledOnDateChange(newDate);
2456
2781
  } else {
2457
2782
  setInternalDate(newDate);
2458
2783
  }
2459
- };
2460
- const handlePrev = () => {
2784
+ });
2785
+ const handlePrev = useStableCallback(() => {
2461
2786
  switch (view) {
2462
2787
  case "month":
2463
2788
  handleDateChange(subMonths2(currentDate, 1));
@@ -2473,133 +2798,139 @@ var useCalendarLogic = ({
2473
2798
  handleDateChange(subDays(currentDate, 7));
2474
2799
  break;
2475
2800
  }
2476
- };
2477
- const handleNext = () => {
2801
+ });
2802
+ const handleNext = useStableCallback(() => {
2478
2803
  switch (view) {
2479
2804
  case "month":
2480
- handleDateChange(addMonths2(currentDate, 1));
2805
+ handleDateChange(addMonths3(currentDate, 1));
2481
2806
  break;
2482
2807
  case "week":
2483
2808
  handleDateChange(addWeeks(currentDate, 1));
2484
2809
  break;
2485
2810
  case "day":
2486
2811
  case "resource":
2487
- handleDateChange(addDays2(currentDate, 1));
2812
+ handleDateChange(addDays5(currentDate, 1));
2488
2813
  break;
2489
2814
  case "agenda":
2490
- handleDateChange(addDays2(currentDate, 7));
2815
+ handleDateChange(addDays5(currentDate, 7));
2491
2816
  break;
2492
2817
  }
2493
- };
2494
- const handleToday = () => {
2818
+ });
2819
+ const handleToday = useStableCallback(() => {
2495
2820
  handleDateChange(/* @__PURE__ */ new Date());
2496
- };
2497
- const handleDateClick = (date) => {
2821
+ });
2822
+ const handleDateClick = useStableCallback((date) => {
2498
2823
  handleDateChange(date);
2499
2824
  handleViewChange("day");
2500
- };
2501
- const handleTimeSlotClick = (date) => {
2825
+ });
2826
+ const handleTimeSlotClick = useStableCallback((date, resourceId) => {
2502
2827
  if (readOnly) return;
2503
2828
  setSelectedEvent(null);
2504
2829
  setModalInitialDate(date);
2830
+ setModalInitialResourceId(resourceId);
2505
2831
  setIsModalOpen(true);
2506
- };
2507
- const handleEventClickInternal = (event) => {
2508
- if (onEventClick) {
2509
- onEventClick(event);
2510
- }
2832
+ });
2833
+ const handleEventClickInternal = useStableCallback((event) => {
2834
+ onEventClick?.(event);
2511
2835
  if (!readOnly) {
2512
2836
  setSelectedEvent(event);
2513
2837
  setModalInitialDate(void 0);
2838
+ setModalInitialResourceId(void 0);
2514
2839
  setIsModalOpen(true);
2515
2840
  }
2516
- };
2517
- const handleCreateEvent = () => {
2841
+ });
2842
+ const handleCreateEvent = useStableCallback(() => {
2518
2843
  if (readOnly) return;
2519
2844
  setSelectedEvent(null);
2520
2845
  setModalInitialDate(/* @__PURE__ */ new Date());
2846
+ setModalInitialResourceId(void 0);
2521
2847
  setIsModalOpen(true);
2522
- };
2523
- const handleModalSave = (eventData) => {
2524
- let effectiveEventId = selectedEvent?.id;
2848
+ });
2849
+ const handleModalSave = useStableCallback((eventData) => {
2525
2850
  if (selectedEvent?.originalEventId) {
2526
- effectiveEventId = selectedEvent.originalEventId;
2527
- }
2528
- if (effectiveEventId) {
2529
- if (onEventUpdate) {
2530
- onEventUpdate({
2531
- ...eventData,
2532
- id: effectiveEventId
2533
- });
2534
- }
2851
+ const master = masterOf(selectedEvent);
2852
+ if (!master) return;
2853
+ const edited = {
2854
+ ...master,
2855
+ ...stripOccurrenceFields(eventData),
2856
+ id: master.id,
2857
+ start: master.start,
2858
+ end: master.end
2859
+ };
2860
+ onEventUpdate?.(
2861
+ shiftSeries(edited, selectedEvent, eventData.start ?? selectedEvent.start, eventData.end ?? selectedEvent.end, timezone)
2862
+ );
2863
+ } else if (selectedEvent) {
2864
+ onEventUpdate?.({ ...eventData, id: selectedEvent.id });
2535
2865
  } else {
2536
- if (onEventCreate) {
2537
- onEventCreate(eventData);
2538
- }
2539
- }
2540
- };
2541
- const handleModalDelete = (eventId) => {
2542
- let effectiveEventId = eventId;
2543
- if (selectedEvent?.originalEventId && selectedEvent.id === eventId) {
2544
- effectiveEventId = selectedEvent.originalEventId;
2866
+ onEventCreate?.(
2867
+ modalInitialResourceId && eventData.resourceId === void 0 ? { ...eventData, resourceId: modalInitialResourceId } : eventData
2868
+ );
2545
2869
  }
2546
- if (onEventDelete) {
2547
- onEventDelete(effectiveEventId);
2870
+ });
2871
+ const handleEventDelete = useStableCallback((event) => {
2872
+ if (!event.originalEventId) {
2873
+ onEventDelete?.(event.id);
2874
+ return;
2548
2875
  }
2549
- };
2550
- const handleDragEnd = (event) => {
2551
- const { active, over } = event;
2552
- if (!over) return;
2553
- const activeEvent = active.data.current?.event;
2554
- const overDate = over.data.current?.date;
2555
- if (!activeEvent || !overDate) return;
2556
- const originalStart = new Date(activeEvent.start);
2557
- const originalEnd = new Date(activeEvent.end);
2558
- const duration = differenceInMilliseconds(originalEnd, originalStart);
2559
- let newStart;
2560
- if (view === "month") {
2561
- if (timezone) {
2562
- const zonedOriginal = toZonedTime5(originalStart, timezone);
2563
- const zonedNew = new Date(overDate);
2564
- zonedNew.setHours(zonedOriginal.getHours());
2565
- zonedNew.setMinutes(zonedOriginal.getMinutes());
2566
- zonedNew.setSeconds(zonedOriginal.getSeconds());
2567
- zonedNew.setMilliseconds(zonedOriginal.getMilliseconds());
2568
- newStart = fromZonedTime(zonedNew, timezone);
2569
- } else {
2570
- newStart = new Date(overDate);
2571
- newStart.setHours(originalStart.getHours());
2572
- newStart.setMinutes(originalStart.getMinutes());
2573
- newStart.setSeconds(originalStart.getSeconds());
2574
- newStart.setMilliseconds(originalStart.getMilliseconds());
2575
- }
2876
+ const master = masterOf(event);
2877
+ if (onOccurrenceDelete && master) {
2878
+ onOccurrenceDelete(event, excludeOccurrence(master, event.occurrenceStart ?? event.start));
2576
2879
  } else {
2577
- if (timezone) {
2578
- newStart = fromZonedTime(overDate, timezone);
2579
- } else {
2580
- newStart = new Date(overDate);
2581
- }
2582
- newStart.setSeconds(0);
2583
- newStart.setMilliseconds(0);
2584
- }
2585
- const newEnd = new Date(newStart.getTime() + duration);
2586
- let newResourceId = activeEvent.resourceId;
2587
- const overResourceId = over.data.current?.resourceId;
2588
- if (overResourceId) {
2589
- newResourceId = overResourceId;
2590
- }
2591
- if (newStart.getTime() === originalStart.getTime() && newEnd.getTime() === originalEnd.getTime() && newResourceId === activeEvent.resourceId) {
2592
- return;
2880
+ onEventDelete?.(event.originalEventId);
2593
2881
  }
2594
- if (onEventUpdate) {
2595
- onEventUpdate({
2596
- ...activeEvent,
2597
- start: newStart,
2598
- end: newEnd,
2599
- resourceId: newResourceId
2600
- });
2882
+ });
2883
+ const handleModalDelete = useStableCallback((eventId) => {
2884
+ if (selectedEvent && selectedEvent.id === eventId) {
2885
+ handleEventDelete(selectedEvent);
2886
+ } else {
2887
+ onEventDelete?.(eventId);
2601
2888
  }
2889
+ });
2890
+ const handleEventDuplicate = useStableCallback((event) => {
2891
+ const { recurrence: _recurrence, ...copy } = stripOccurrenceFields(event);
2892
+ onEventCreate?.({
2893
+ ...copy,
2894
+ id: `${event.originalEventId ?? event.id}-copy-${Date.now()}`,
2895
+ title: `${event.title} (Copy)`,
2896
+ start: new Date(event.start.getTime() + DAY_MS2),
2897
+ end: new Date(event.end.getTime() + DAY_MS2)
2898
+ });
2899
+ });
2900
+ const applyChange = (event, start, end) => {
2901
+ if (!event.originalEventId) return { original: event, updated: { ...event, start, end } };
2902
+ const master = masterOf(event);
2903
+ if (!master) return null;
2904
+ return { original: master, updated: shiftSeries(master, event, start, end, timezone) };
2602
2905
  };
2906
+ const handleDragEnd = useStableCallback((dragEvent) => {
2907
+ const { active, over } = dragEvent;
2908
+ if (!over) return;
2909
+ const dragged = active.data.current?.event;
2910
+ const overDate = over.data.current?.date;
2911
+ if (!dragged || !overDate) return;
2912
+ const drop = computeDrop({
2913
+ event: dragged,
2914
+ overDate,
2915
+ overResourceId: over.data.current?.resourceId,
2916
+ grabOffsetMs: active.data.current?.grabOffsetMs,
2917
+ view,
2918
+ tz: timezone
2919
+ });
2920
+ if (!drop) return;
2921
+ const change = applyChange(dragged, drop.start, drop.end);
2922
+ if (!change) return;
2923
+ const updated = { ...change.updated, resourceId: drop.resourceId };
2924
+ onEventDrop?.(change.original, updated.start, updated.end);
2925
+ onEventUpdate?.(updated);
2926
+ });
2927
+ const handleEventResize = useStableCallback((event, newEnd) => {
2928
+ if (readOnly) return;
2929
+ const change = applyChange(event, event.start, newEnd);
2930
+ if (!change) return;
2931
+ onEventResize?.(change.original, change.updated.start, change.updated.end);
2932
+ onEventUpdate?.(change.updated);
2933
+ });
2603
2934
  return {
2604
2935
  view,
2605
2936
  currentDate,
@@ -2621,15 +2952,18 @@ var useCalendarLogic = ({
2621
2952
  handleCreateEvent,
2622
2953
  handleModalSave,
2623
2954
  handleModalDelete,
2955
+ handleEventDelete,
2956
+ handleEventDuplicate,
2957
+ handleEventResize,
2624
2958
  handleDragEnd
2625
2959
  };
2626
2960
  };
2627
2961
 
2628
2962
  // src/index.tsx
2629
- import { differenceInMinutes as differenceInMinutes6, format as format11 } from "date-fns";
2963
+ import { differenceInMinutes as differenceInMinutes6, format as format14 } from "date-fns";
2630
2964
 
2631
2965
  // src/hooks/useSwipeGesture.ts
2632
- import { useRef as useRef5, useCallback as useCallback4, useEffect as useEffect5 } from "react";
2966
+ import { useRef as useRef6, useCallback as useCallback4, useEffect as useEffect5 } from "react";
2633
2967
  var useSwipeGesture = (options) => {
2634
2968
  const {
2635
2969
  onSwipeLeft,
@@ -2641,8 +2975,8 @@ var useSwipeGesture = (options) => {
2641
2975
  allowedTime = 500,
2642
2976
  enabled = true
2643
2977
  } = options;
2644
- const touchInfoRef = useRef5(null);
2645
- const elementRef = useRef5(null);
2978
+ const touchInfoRef = useRef6(null);
2979
+ const elementRef = useRef6(null);
2646
2980
  const handleTouchStart = useCallback4((e) => {
2647
2981
  if (!enabled) return;
2648
2982
  const touch = e.touches[0];
@@ -2710,7 +3044,9 @@ var Scheduler = ({
2710
3044
  onEventCreate,
2711
3045
  onEventUpdate,
2712
3046
  onEventDelete,
3047
+ onEventDrop,
2713
3048
  onEventResize,
3049
+ onOccurrenceDelete,
2714
3050
  timezone,
2715
3051
  onTimezoneChange,
2716
3052
  className,
@@ -2758,9 +3094,11 @@ var Scheduler = ({
2758
3094
  handleCreateEvent,
2759
3095
  handleModalSave,
2760
3096
  handleModalDelete,
3097
+ handleEventDelete,
3098
+ handleEventDuplicate,
3099
+ handleEventResize,
2761
3100
  handleDragEnd,
2762
3101
  expandedEvents
2763
- // New export
2764
3102
  } = useCalendarLogic({
2765
3103
  events,
2766
3104
  view: controlledView,
@@ -2771,6 +3109,9 @@ var Scheduler = ({
2771
3109
  onEventUpdate,
2772
3110
  onEventCreate,
2773
3111
  onEventDelete,
3112
+ onEventDrop,
3113
+ onEventResize,
3114
+ onOccurrenceDelete,
2774
3115
  readOnly,
2775
3116
  timezone
2776
3117
  });
@@ -2785,37 +3126,20 @@ var Scheduler = ({
2785
3126
  const snapToGrid = createSnapModifier(gridSize);
2786
3127
  const modifiers = [snapToGrid, restrictToWindowEdges];
2787
3128
  const dndSensors = readOnly ? [] : sensors;
2788
- const handleEventResize = (event, newEnd) => {
2789
- if (readOnly) return;
2790
- if (onEventResize) {
2791
- onEventResize(event, event.start, newEnd);
2792
- }
2793
- if (onEventUpdate) {
2794
- onEventUpdate({
2795
- ...event,
2796
- end: newEnd
2797
- });
2798
- }
2799
- };
2800
3129
  const id = useId();
2801
3130
  const swipeRef = useViewSwipe(handlePrev, handleNext, true);
2802
- const languagePack = language === "fr" ? fr : en;
2803
- const t = {
2804
- ...languagePack,
2805
- ...translations
2806
- };
2807
- const overlayTimeFormat = uses24HourClock(locale) ? "H:mm" : "h:mm a";
2808
- const handleDragStart = (event) => {
2809
- const { active } = event;
2810
- const draggedEvent = expandedEvents.find((e) => e.id === active.id);
3131
+ const t = useMemo4(() => resolveTranslations(language, translations), [language, translations]);
3132
+ const overlayTimeFormat = timeFormats(locale).time;
3133
+ const handleDragStart = useCallback5((event) => {
3134
+ const draggedEvent = event.active.data.current?.event;
2811
3135
  if (draggedEvent) {
2812
3136
  setActiveDragEvent(draggedEvent);
2813
3137
  }
2814
- };
2815
- const onDragEndWrapper = (event) => {
3138
+ }, []);
3139
+ const onDragEndWrapper = useCallback5((event) => {
2816
3140
  setActiveDragEvent(null);
2817
3141
  handleDragEnd(event);
2818
- };
3142
+ }, [handleDragEnd]);
2819
3143
  const getDragHeight = () => {
2820
3144
  if (!activeDragEvent) return void 0;
2821
3145
  if (view === "resource") {
@@ -2980,7 +3304,8 @@ var Scheduler = ({
2980
3304
  onEventClick: handleEventClickInternal,
2981
3305
  onCreateEvent: handleCreateEvent,
2982
3306
  locale,
2983
- translations: t
3307
+ translations: t,
3308
+ timezone
2984
3309
  }
2985
3310
  ),
2986
3311
  view === "resource" && resources && /* @__PURE__ */ React18.createElement(
@@ -2990,11 +3315,9 @@ var Scheduler = ({
2990
3315
  events: filteredEvents,
2991
3316
  resources,
2992
3317
  onEventClick: handleEventClickInternal,
2993
- onTimeSlotClick: (date) => {
2994
- if (readOnly) return;
2995
- handleTimeSlotClick(date);
2996
- },
2997
- locale
3318
+ onTimeSlotClick: handleTimeSlotClick,
3319
+ locale,
3320
+ timezone
2998
3321
  }
2999
3322
  )
3000
3323
  )))), /* @__PURE__ */ React18.createElement("div", { className: "md:hidden absolute bottom-6 right-6 z-50" }, /* @__PURE__ */ React18.createElement(
@@ -3036,20 +3359,12 @@ var Scheduler = ({
3036
3359
  handleEventClickInternal(event);
3037
3360
  closeContextMenu();
3038
3361
  },
3039
- onDelete: (eventId) => {
3040
- onEventDelete?.(eventId);
3362
+ onDelete: () => {
3363
+ if (contextMenuEvent) handleEventDelete(contextMenuEvent);
3041
3364
  closeContextMenu();
3042
3365
  },
3043
3366
  onDuplicate: (event) => {
3044
- const duplicatedEvent = {
3045
- ...event,
3046
- id: `${event.id}-copy-${Date.now()}`,
3047
- title: `${event.title} (Copy)`,
3048
- start: new Date(event.start.getTime() + 24 * 60 * 60 * 1e3),
3049
- // +1 day
3050
- end: new Date(event.end.getTime() + 24 * 60 * 60 * 1e3)
3051
- };
3052
- onEventCreate?.(duplicatedEvent);
3367
+ handleEventDuplicate(event);
3053
3368
  closeContextMenu();
3054
3369
  },
3055
3370
  translations: {
@@ -3083,7 +3398,7 @@ var Scheduler = ({
3083
3398
  className: "absolute left-0 top-0 bottom-0 w-1 rounded-l-lg",
3084
3399
  style: { backgroundColor: activeDragEvent.color || "var(--primary)" }
3085
3400
  }
3086
- ), /* @__PURE__ */ React18.createElement("div", { className: "pl-2" }, /* @__PURE__ */ React18.createElement("div", { className: "font-semibold truncate text-sm" }, activeDragEvent.title), (view === "week" || view === "day") && getDragHeight() && getDragHeight() > 40 && /* @__PURE__ */ React18.createElement("div", { className: "text-xs opacity-80 mt-0.5 flex items-center gap-1" }, /* @__PURE__ */ React18.createElement("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React18.createElement("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ React18.createElement("path", { d: "M12 6v6l4 2" })), format11(activeDragEvent.start, overlayTimeFormat, { locale }), " - ", format11(activeDragEvent.end, overlayTimeFormat, { locale }))), /* @__PURE__ */ React18.createElement("div", { className: "absolute bottom-1.5 right-1.5 opacity-60" }, /* @__PURE__ */ React18.createElement("svg", { className: "w-4 h-4", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "5", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "5", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "12", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "12", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "19", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "19", r: "1.5" }))))
3401
+ ), /* @__PURE__ */ React18.createElement("div", { className: "pl-2" }, /* @__PURE__ */ React18.createElement("div", { className: "font-semibold truncate text-sm" }, activeDragEvent.title), (view === "week" || view === "day") && getDragHeight() && getDragHeight() > 40 && /* @__PURE__ */ React18.createElement("div", { className: "text-xs opacity-80 mt-0.5 flex items-center gap-1" }, /* @__PURE__ */ React18.createElement("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, /* @__PURE__ */ React18.createElement("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ React18.createElement("path", { d: "M12 6v6l4 2" })), format14(activeDragEvent.start, overlayTimeFormat, { locale }), " - ", format14(activeDragEvent.end, overlayTimeFormat, { locale }))), /* @__PURE__ */ React18.createElement("div", { className: "absolute bottom-1.5 right-1.5 opacity-60" }, /* @__PURE__ */ React18.createElement("svg", { className: "w-4 h-4", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "5", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "5", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "12", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "12", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "9", cy: "19", r: "1.5" }), /* @__PURE__ */ React18.createElement("circle", { cx: "15", cy: "19", r: "1.5" }))))
3087
3402
  ) : null)
3088
3403
  )
3089
3404
  );