kalendly 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +377 -662
  2. package/dist/core/index.js +3 -3
  3. package/dist/index.d.mts +61 -602
  4. package/dist/index.d.ts +61 -602
  5. package/dist/index.js +963 -2375
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +937 -2364
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{vanilla/index.umd.js → index.umd.js} +438 -244
  10. package/dist/index.umd.js.map +1 -0
  11. package/dist/styles/calendar.css +96 -0
  12. package/package.json +54 -118
  13. package/dist/react/index.d.mts +0 -251
  14. package/dist/react/index.d.ts +0 -251
  15. package/dist/react/index.js +0 -969
  16. package/dist/react/index.js.map +0 -1
  17. package/dist/react/index.mjs +0 -924
  18. package/dist/react/index.mjs.map +0 -1
  19. package/dist/react-native/index.d.mts +0 -642
  20. package/dist/react-native/index.d.ts +0 -642
  21. package/dist/react-native/index.js +0 -1649
  22. package/dist/react-native/index.js.map +0 -1
  23. package/dist/react-native/index.mjs +0 -1615
  24. package/dist/react-native/index.mjs.map +0 -1
  25. package/dist/vanilla/index.d.mts +0 -271
  26. package/dist/vanilla/index.d.ts +0 -271
  27. package/dist/vanilla/index.js +0 -1064
  28. package/dist/vanilla/index.js.map +0 -1
  29. package/dist/vanilla/index.mjs +0 -1013
  30. package/dist/vanilla/index.mjs.map +0 -1
  31. package/dist/vanilla/index.umd.js.map +0 -1
  32. package/dist/vue/components/Calendar.vue.d.ts +0 -43
  33. package/dist/vue/components/Calendar.vue.d.ts.map +0 -1
  34. package/dist/vue/index.d.ts +0 -134
  35. package/dist/vue/index.d.ts.map +0 -1
  36. package/dist/vue/index.js +0 -1
  37. package/dist/vue/index.mjs +0 -717
  38. package/dist/vue/types.d.ts +0 -21
  39. package/dist/vue/types.d.ts.map +0 -1
@@ -18,34 +18,11 @@ var Kalendly = (() => {
18
18
  };
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
- // src/vanilla/index.ts
22
- var index_exports = {};
23
- __export(index_exports, {
24
- CalendarEngine: () => CalendarEngine,
25
- DAYS: () => DAYS,
26
- DEFAULT_CATEGORY_COLORS: () => DEFAULT_CATEGORY_COLORS,
27
- MONTHS: () => MONTHS,
28
- MONTHS_FULL: () => MONTHS_FULL,
29
- VanillaCalendar: () => VanillaCalendar,
30
- createCalendar: () => createCalendar,
31
- formatAttendees: () => formatAttendees,
32
- formatDateForDisplay: () => formatDateForDisplay,
33
- formatTimeRange: () => formatTimeRange,
34
- generateCalendarDates: () => generateCalendarDates,
35
- generateYears: () => generateYears,
36
- getCategoryColor: () => getCategoryColor,
37
- getCellClasses: () => getCellClasses,
38
- getDefaultEventColor: () => getDefaultEventColor,
39
- getEventsForDate: () => getEventsForDate,
40
- getMonthYearText: () => getMonthYearText,
41
- getPopupPositionClass: () => getPopupPositionClass,
42
- hasEvents: () => hasEvents,
43
- isSameDay: () => isSameDay,
44
- isToday: () => isToday,
45
- isValidHexColor: () => isValidHexColor,
46
- mergeCategoryColors: () => mergeCategoryColors,
47
- normalizeDate: () => normalizeDate,
48
- sortEventsByTime: () => sortEventsByTime
21
+ // src/web-components/CalendarElement.ts
22
+ var CalendarElement_exports = {};
23
+ __export(CalendarElement_exports, {
24
+ CalendarElement: () => CalendarElement,
25
+ defineCalendarElement: () => defineCalendarElement
49
26
  });
50
27
 
51
28
  // src/core/utils.ts
@@ -108,9 +85,6 @@ var Kalendly = (() => {
108
85
  return eventDate.getTime() === normalizedTargetDate.getTime();
109
86
  });
110
87
  }
111
- function hasEvents(events, date) {
112
- return getEventsForDate(events, date).length > 0;
113
- }
114
88
  function generateCalendarDates(year, month, events = [], weekStartsOn = 0) {
115
89
  const firstDay = new Date(year, month, 1);
116
90
  const lastDay = new Date(year, month + 1, 0);
@@ -212,16 +186,6 @@ var Kalendly = (() => {
212
186
  if (attendees.length === 2) return attendees.join(" and ");
213
187
  return `${attendees.slice(0, -1).join(", ")}, and ${attendees[attendees.length - 1]}`;
214
188
  }
215
- function sortEventsByTime(events) {
216
- return [...events].sort((a, b) => {
217
- const aAllDay = a.allDay || !a.startTime && !a.endTime;
218
- const bAllDay = b.allDay || !b.startTime && !b.endTime;
219
- if (aAllDay && !bAllDay) return -1;
220
- if (!aAllDay && bAllDay) return 1;
221
- if (!a.startTime || !b.startTime) return 0;
222
- return a.startTime.localeCompare(b.startTime);
223
- });
224
- }
225
189
  var DEFAULT_CATEGORY_COLORS = {
226
190
  work: "#3b82f6",
227
191
  personal: "#8b5cf6",
@@ -230,13 +194,6 @@ var Kalendly = (() => {
230
194
  appointment: "#f59e0b",
231
195
  other: "#6b7280"
232
196
  };
233
- function getDefaultEventColor(category, customColors) {
234
- const colorMap = customColors || DEFAULT_CATEGORY_COLORS;
235
- if (category && colorMap[category]) {
236
- return colorMap[category];
237
- }
238
- return "#fc8917";
239
- }
240
197
  function mergeCategoryColors(customColors) {
241
198
  return {
242
199
  ...DEFAULT_CATEGORY_COLORS,
@@ -480,83 +437,204 @@ var Kalendly = (() => {
480
437
  }
481
438
  };
482
439
 
483
- // src/vanilla/Calendar.ts
484
- var VanillaCalendar = class {
485
- constructor(props) {
440
+ // src/web-components/CalendarElement.ts
441
+ var isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
442
+ var CalendarElement = class extends HTMLElement {
443
+ constructor() {
444
+ super(...arguments);
445
+ this.engine = null;
486
446
  this.unsubscribe = null;
447
+ this.actions = null;
487
448
  this.pickerOpen = false;
488
449
  this.yearInput = "";
489
450
  this.yearInputValid = true;
451
+ this.listenersAttached = false;
490
452
  this.clickOutsideHandler = null;
491
453
  this.delegatedClickHandler = null;
492
454
  this.delegatedInputHandler = null;
493
455
  this.delegatedBlurHandler = null;
494
456
  this.delegatedKeydownHandler = null;
495
- this.listenersAttached = false;
496
- this.props = props;
497
- if (typeof props.container === "string") {
498
- const element = document.querySelector(props.container);
499
- if (!element) {
500
- throw new Error(`Container element "${props.container}" not found`);
501
- }
502
- this.container = element;
503
- } else {
504
- this.container = props.container;
457
+ // Rich property backing fields
458
+ this._events = [];
459
+ this._theme = null;
460
+ this._categoryColors = null;
461
+ this._renderEvent = null;
462
+ this._renderNoEvents = null;
463
+ // Selection state (availability mode — range)
464
+ this._rangeStart = null;
465
+ this._rangeEnd = null;
466
+ this._timeRangeDate = null;
467
+ this._timeRangeStart = null;
468
+ this._timeRangeEnd = null;
469
+ this._timeRangeComplete = false;
470
+ }
471
+ get events() {
472
+ return this._events;
473
+ }
474
+ set events(val) {
475
+ this._events = val;
476
+ if (this.engine) {
477
+ this.engine.updateEvents(val);
505
478
  }
506
- this.engine = new CalendarEngine({
507
- events: props.events,
508
- initialDate: props.initialDate,
509
- minYear: props.minYear,
510
- maxYear: props.maxYear,
511
- weekStartsOn: props.weekStartsOn
512
- });
513
- this.actions = this.engine.getActions();
479
+ }
480
+ set theme(val) {
481
+ this._theme = val;
514
482
  this.applyTheme();
515
- this.init();
516
483
  }
517
- applyTheme() {
518
- if (this.props.theme) {
519
- const root = document.documentElement;
520
- const theme = this.props.theme;
521
- if (theme.primary)
522
- root.style.setProperty("--calendar-primary-color", theme.primary);
523
- if (theme.secondary)
524
- root.style.setProperty("--calendar-secondary-color", theme.secondary);
525
- if (theme.tertiary)
526
- root.style.setProperty("--calendar-tertiary-color", theme.tertiary);
527
- if (theme.textColor)
528
- root.style.setProperty("--calendar-text-color", theme.textColor);
529
- if (theme.textLight)
530
- root.style.setProperty("--calendar-text-light", theme.textLight);
531
- if (theme.background)
532
- root.style.setProperty("--calendar-background", theme.background);
533
- if (theme.cellHover)
534
- root.style.setProperty("--calendar-cell-hover", theme.cellHover);
535
- if (theme.borderColor)
536
- root.style.setProperty("--calendar-border-color", theme.borderColor);
537
- if (theme.todayOutline)
538
- root.style.setProperty("--calendar-today-outline", theme.todayOutline);
539
- if (theme.selectedBg)
540
- root.style.setProperty("--calendar-selected-bg", theme.selectedBg);
541
- if (theme.eventIndicator)
542
- root.style.setProperty(
543
- "--calendar-event-indicator",
544
- theme.eventIndicator
545
- );
484
+ set categoryColors(val) {
485
+ this._categoryColors = val;
486
+ if (this.engine) {
487
+ this.engine.updateCategoryColors(val);
546
488
  }
547
489
  }
548
- init() {
549
- this.container.classList.add("kalendly-calendar");
550
- if (this.props.className) {
551
- this.container.classList.add(this.props.className);
490
+ set renderEvent(val) {
491
+ this._renderEvent = val;
492
+ if (this.engine) this.render();
493
+ }
494
+ set renderNoEvents(val) {
495
+ this._renderNoEvents = val;
496
+ if (this.engine) this.render();
497
+ }
498
+ get loading() {
499
+ return this.hasAttribute("loading");
500
+ }
501
+ set loading(val) {
502
+ if (val) this.setAttribute("loading", "");
503
+ else this.removeAttribute("loading");
504
+ }
505
+ connectedCallback() {
506
+ this.classList.add("kalendly-calendar");
507
+ this.initEngine();
508
+ this.render();
509
+ }
510
+ disconnectedCallback() {
511
+ this.cleanup();
512
+ this.classList.remove("kalendly-calendar");
513
+ this.innerHTML = "";
514
+ }
515
+ attributeChangedCallback(name, oldVal, newVal) {
516
+ if (oldVal === newVal) return;
517
+ if (name === "loading") {
518
+ this.render();
519
+ return;
520
+ }
521
+ if (name === "selectable" && newVal === null) {
522
+ this._rangeStart = null;
523
+ this._rangeEnd = null;
524
+ this._timeRangeDate = null;
525
+ this._timeRangeStart = null;
526
+ this._timeRangeEnd = null;
527
+ this._timeRangeComplete = false;
552
528
  }
529
+ this.reinit();
530
+ }
531
+ get minYear() {
532
+ const val = this.getAttribute("min-year");
533
+ return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() - 30;
534
+ }
535
+ get maxYear() {
536
+ const val = this.getAttribute("max-year");
537
+ return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() + 10;
538
+ }
539
+ initEngine() {
540
+ const initialDateAttr = this.getAttribute("initial-date");
541
+ const initialDate = initialDateAttr ? new Date(initialDateAttr) : void 0;
542
+ const weekStartsOnAttr = this.getAttribute("week-starts-on");
543
+ const weekStartsOn = weekStartsOnAttr === "1" ? 1 : 0;
544
+ this.engine = new CalendarEngine({
545
+ events: this._events,
546
+ initialDate,
547
+ minYear: this.minYear,
548
+ maxYear: this.maxYear,
549
+ weekStartsOn,
550
+ categoryColors: this._categoryColors ?? void 0
551
+ });
552
+ this.actions = this.engine.getActions();
553
+ this.applyTheme();
553
554
  this.unsubscribe = this.engine.subscribe(() => {
554
555
  this.render();
555
556
  });
557
+ }
558
+ reinit() {
559
+ if (!this.engine) return;
560
+ this.cleanup();
561
+ this.initEngine();
556
562
  this.render();
557
563
  }
564
+ cleanup() {
565
+ if (this.unsubscribe) {
566
+ this.unsubscribe();
567
+ this.unsubscribe = null;
568
+ }
569
+ if (this.delegatedClickHandler) {
570
+ this.removeEventListener("click", this.delegatedClickHandler);
571
+ this.delegatedClickHandler = null;
572
+ }
573
+ if (this.delegatedInputHandler) {
574
+ this.removeEventListener("input", this.delegatedInputHandler);
575
+ this.delegatedInputHandler = null;
576
+ }
577
+ if (this.delegatedBlurHandler) {
578
+ this.removeEventListener("blur", this.delegatedBlurHandler, true);
579
+ this.delegatedBlurHandler = null;
580
+ }
581
+ if (this.delegatedKeydownHandler) {
582
+ this.removeEventListener("keydown", this.delegatedKeydownHandler);
583
+ this.delegatedKeydownHandler = null;
584
+ }
585
+ if (this.clickOutsideHandler) {
586
+ document.removeEventListener("click", this.clickOutsideHandler);
587
+ this.clickOutsideHandler = null;
588
+ }
589
+ this.listenersAttached = false;
590
+ if (this.engine) {
591
+ this.engine.destroy();
592
+ this.engine = null;
593
+ }
594
+ this.actions = null;
595
+ }
596
+ applyTheme() {
597
+ if (!this._theme) return;
598
+ const root = document.documentElement;
599
+ const t = this._theme;
600
+ if (t.primary)
601
+ root.style.setProperty("--calendar-primary-color", t.primary);
602
+ if (t.secondary)
603
+ root.style.setProperty("--calendar-secondary-color", t.secondary);
604
+ if (t.tertiary)
605
+ root.style.setProperty("--calendar-tertiary-color", t.tertiary);
606
+ if (t.textColor)
607
+ root.style.setProperty("--calendar-text-color", t.textColor);
608
+ if (t.textLight)
609
+ root.style.setProperty("--calendar-text-light", t.textLight);
610
+ if (t.background)
611
+ root.style.setProperty("--calendar-background", t.background);
612
+ if (t.cellHover)
613
+ root.style.setProperty("--calendar-cell-hover", t.cellHover);
614
+ if (t.borderColor)
615
+ root.style.setProperty("--calendar-border-color", t.borderColor);
616
+ if (t.todayOutline)
617
+ root.style.setProperty("--calendar-today-outline", t.todayOutline);
618
+ if (t.selectedBg)
619
+ root.style.setProperty("--calendar-selected-bg", t.selectedBg);
620
+ if (t.eventIndicator)
621
+ root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
622
+ }
558
623
  render() {
624
+ if (!this.engine) return;
559
625
  const viewModel = this.engine.getViewModel();
626
+ const useShortMonths = this.hasAttribute("use-short-month-names");
627
+ const title = this.getAttribute("title");
628
+ const minYear = this.minYear;
629
+ const maxYear = this.maxYear;
630
+ const availabilityMode = this.getAttribute("availability-mode");
631
+ const selectable = this.hasAttribute("selectable");
632
+ const isLoading = this.loading;
633
+ const today = /* @__PURE__ */ new Date();
634
+ const todayMonth = today.getMonth();
635
+ const todayYear = today.getFullYear();
636
+ const isCurrentMonth = viewModel.currentYear === todayYear && viewModel.currentMonth === todayMonth;
637
+ const showScrollHint = viewModel.tasks.length > 3;
560
638
  const defaultRenderEvent = (event) => {
561
639
  const timeRange = formatTimeRange(event);
562
640
  const attendeesList = formatAttendees(event.attendees);
@@ -602,46 +680,46 @@ var Kalendly = (() => {
602
680
  ${event.status && event.status !== "scheduled" ? `<span class="badge status-${event.status}">${getStatusLabel(event.status)}</span>` : ""}
603
681
  </div>
604
682
  </div>
605
-
683
+
606
684
  ${timeRange ? `
607
685
  <div class="event-time">
608
686
  <span class="event-time-label">Time:</span>
609
687
  <span class="event-time-value">${timeRange}</span>
610
688
  </div>
611
689
  ` : ""}
612
-
690
+
613
691
  ${event.description ? `
614
692
  <div class="event-description">${event.description}</div>
615
693
  ` : ""}
616
-
694
+
617
695
  ${event.location ? `
618
696
  <div class="event-time">
619
697
  <span class="event-time-label">Location:</span>
620
698
  <span class="event-time-value">${event.location}</span>
621
699
  </div>
622
700
  ` : ""}
623
-
701
+
624
702
  ${attendeesList ? `
625
703
  <div class="event-time">
626
704
  <span class="event-time-label">Attendees:</span>
627
705
  <span class="event-time-value">${attendeesList}</span>
628
706
  </div>
629
707
  ` : ""}
630
-
708
+
631
709
  ${event.organizer ? `
632
710
  <div class="event-time">
633
711
  <span class="event-time-label">Organizer:</span>
634
712
  <span class="event-time-value">${event.organizer}</span>
635
713
  </div>
636
714
  ` : ""}
637
-
715
+
638
716
  ${event.notes ? `
639
717
  <div class="event-time">
640
718
  <span class="event-time-label">Notes:</span>
641
719
  <span class="event-time-value">${event.notes}</span>
642
720
  </div>
643
721
  ` : ""}
644
-
722
+
645
723
  ${event.url ? `
646
724
  <div class="event-time">
647
725
  <a href="${event.url}" target="_blank" rel="noopener noreferrer" class="event-link">
@@ -649,7 +727,7 @@ var Kalendly = (() => {
649
727
  </a>
650
728
  </div>
651
729
  ` : ""}
652
-
730
+
653
731
  ${event.tags && event.tags.length > 0 ? `
654
732
  <div class="event-tags">
655
733
  ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
@@ -659,25 +737,48 @@ var Kalendly = (() => {
659
737
  `;
660
738
  };
661
739
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
662
- const renderEvent = this.props.renderEvent || defaultRenderEvent;
663
- const renderNoEvents = this.props.renderNoEvents || defaultRenderNoEvents;
664
- const showScrollHint = viewModel.tasks.length > 3;
665
- const today = /* @__PURE__ */ new Date();
666
- const todayMonth = today.getMonth();
667
- const todayYear = today.getFullYear();
668
- const minYear = this.props.minYear ?? todayYear - 30;
669
- const maxYear = this.props.maxYear ?? todayYear + 10;
670
- const isCurrentMonth = viewModel.currentYear === todayYear && viewModel.currentMonth === todayMonth;
740
+ const renderEvent = this._renderEvent || defaultRenderEvent;
741
+ const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
742
+ const renderTimeGrid = (events, date) => {
743
+ const isHourBooked = (hour) => events.some((event) => {
744
+ if (!event.startTime || !event.endTime) return true;
745
+ const [startH] = event.startTime.split(":").map(Number);
746
+ const [endH] = event.endTime.split(":").map(Number);
747
+ return hour >= startH && hour < endH;
748
+ });
749
+ const slots = Array.from({ length: 24 }, (_, hour) => {
750
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
751
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
752
+ const booked = isHourBooked(hour);
753
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
754
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
755
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
756
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
757
+ const slotClasses = [
758
+ "time-grid__slot",
759
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
760
+ isRangeStart ? "time-grid__slot--range-start" : "",
761
+ isRangeEnd ? "time-grid__slot--range-end" : "",
762
+ isInRange ? "time-grid__slot--in-range" : ""
763
+ ].filter(Boolean).join(" ");
764
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
765
+ return `
766
+ <div class="${slotClasses}" ${slotAttrs}>
767
+ <span class="time-grid__label">${startTime}</span>
768
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
769
+ </div>`;
770
+ });
771
+ return `<div class="time-grid">${slots.join("")}</div>`;
772
+ };
671
773
  const html = `
672
- ${this.props.title ? `
673
- <div class="page--title">
674
- <h1>${this.props.title}</h1>
675
- </div>
774
+ ${title ? `
775
+ <div class="page--title">
776
+ <h1>${title}</h1>
777
+ </div>
676
778
  ` : ""}
677
779
 
678
780
  <div class="calendar--content">
679
781
  <div class="calendar--card">
680
- <!-- Navigation Header -->
681
782
  <div class="calendar--nav-header">
682
783
  <button type="button" class="calendar--nav-arrow" data-action="previous" aria-label="Previous month">
683
784
  &#8249;
@@ -691,7 +792,7 @@ var Kalendly = (() => {
691
792
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
692
793
  aria-haspopup="true"
693
794
  >
694
- ${this.props.useShortMonthNames ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
795
+ ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
695
796
  <span class="calendar--picker-chevron">&#9662;</span>
696
797
  </button>
697
798
 
@@ -722,7 +823,7 @@ var Kalendly = (() => {
722
823
  </div>
723
824
 
724
825
  <div class="calendar--picker-months">
725
- ${(this.props.useShortMonthNames ? MONTHS : MONTHS_FULL).map((month, index) => {
826
+ ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
726
827
  const isSelected = index === viewModel.currentMonth;
727
828
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
728
829
  return `
@@ -758,30 +859,52 @@ var Kalendly = (() => {
758
859
  </tr>
759
860
  </thead>
760
861
  <tbody data-calendar-body>
761
- ${viewModel.calendarDates.map(
862
+ ${isLoading ? Array.from(
863
+ { length: 6 },
864
+ () => `<tr>${Array.from(
865
+ { length: 7 },
866
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
867
+ ).join("")}</tr>`
868
+ ).join("") : viewModel.calendarDates.map(
762
869
  (week) => `
763
- <tr>
764
- ${week.map((calendarDate, dayIndex) => {
870
+ <tr>
871
+ ${week.map((calendarDate, dayIndex) => {
765
872
  const classes = getCellClasses(calendarDate);
873
+ if (availabilityMode && calendarDate.isCurrentMonth) {
874
+ classes.push(
875
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
876
+ );
877
+ }
878
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
879
+ const d = calendarDate.date;
880
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
881
+ classes.push("availability--range-start");
882
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
883
+ classes.push("availability--range-end");
884
+ if (this._rangeStart && this._rangeEnd) {
885
+ if (d > this._rangeStart && d < this._rangeEnd)
886
+ classes.push("availability--in-range");
887
+ }
888
+ }
766
889
  const dateString = calendarDate.date.toISOString();
767
890
  return `
768
- <td
769
- class="${classes.join(" ")}"
770
- data-date="${dateString}"
771
- data-day-index="${dayIndex}"
772
- data-clickable="true"
773
- >
774
- ${calendarDate.date.getDate()}
775
- </td>
776
- `;
891
+ <td
892
+ class="${classes.join(" ")}"
893
+ data-date="${dateString}"
894
+ data-day-index="${dayIndex}"
895
+ data-clickable="true"
896
+ >
897
+ ${calendarDate.date.getDate()}
898
+ </td>
899
+ `;
777
900
  }).join("")}
778
- </tr>
779
- `
901
+ </tr>
902
+ `
780
903
  ).join("")}
781
904
  </tbody>
782
905
  </table>
783
906
 
784
- ${viewModel.selectedDate ? `
907
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
785
908
  <div class="date-popup ${viewModel.popupPositionClass}">
786
909
  <div class="popup-header">
787
910
  <h2>${viewModel.scheduleDay}</h2>
@@ -795,26 +918,21 @@ var Kalendly = (() => {
795
918
  ` : ""}
796
919
 
797
920
  <div class="events-container">
798
- ${viewModel.tasks.length > 0 ? `
799
- ${viewModel.tasks.map((event) => renderEvent(event)).join("")}
800
- ` : renderNoEvents()}
921
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
801
922
  </div>
802
923
  </div>
803
924
  ` : ""}
804
925
  </div>
805
926
  </div>
806
927
  `;
807
- this.container.innerHTML = html;
928
+ this.innerHTML = html;
808
929
  this.attachEventListeners();
809
930
  }
810
931
  attachEventListeners() {
811
932
  if (this.listenersAttached) return;
812
933
  this.listenersAttached = true;
813
- const today = /* @__PURE__ */ new Date();
814
- const todayYear = today.getFullYear();
815
- const todayMonth = today.getMonth();
816
- const minYear = this.props.minYear ?? todayYear - 30;
817
- const maxYear = this.props.maxYear ?? todayYear + 10;
934
+ const minYear = this.minYear;
935
+ const maxYear = this.maxYear;
818
936
  this.delegatedClickHandler = (e) => {
819
937
  const target = e.target;
820
938
  const cell = target.closest('td[data-clickable="true"]');
@@ -822,9 +940,66 @@ var Kalendly = (() => {
822
940
  e.stopPropagation();
823
941
  const date = new Date(cell.dataset.date);
824
942
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
943
+ const availMode = this.getAttribute("availability-mode");
944
+ const isSelectable = this.hasAttribute("selectable");
945
+ if (availMode === "day" && isSelectable) {
946
+ const isBooked = cell.classList.contains("availability--booked");
947
+ if (!isBooked) {
948
+ let startDate;
949
+ let endDate;
950
+ if (this._rangeEnd !== null) {
951
+ this._rangeStart = date;
952
+ this._rangeEnd = null;
953
+ startDate = date;
954
+ endDate = date;
955
+ } else if (this._rangeStart === null) {
956
+ this._rangeStart = date;
957
+ startDate = date;
958
+ endDate = date;
959
+ } else {
960
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
961
+ const cursor = new Date(s);
962
+ cursor.setDate(cursor.getDate() + 1);
963
+ let blocked = false;
964
+ while (cursor < e2) {
965
+ if (this.engine.getEventsForDate(cursor).length > 0) {
966
+ blocked = true;
967
+ break;
968
+ }
969
+ cursor.setDate(cursor.getDate() + 1);
970
+ }
971
+ if (blocked) {
972
+ this._rangeStart = date;
973
+ this._rangeEnd = null;
974
+ } else {
975
+ this._rangeStart = s;
976
+ this._rangeEnd = e2;
977
+ }
978
+ startDate = this._rangeStart;
979
+ endDate = this._rangeEnd ?? this._rangeStart;
980
+ }
981
+ this.dispatchEvent(
982
+ new CustomEvent("cal-availability-select", {
983
+ bubbles: true,
984
+ composed: true,
985
+ detail: { startDate, endDate }
986
+ })
987
+ );
988
+ }
989
+ }
990
+ if (availMode === "time") {
991
+ this._timeRangeDate = null;
992
+ this._timeRangeStart = null;
993
+ this._timeRangeEnd = null;
994
+ this._timeRangeComplete = false;
995
+ }
825
996
  this.engine.handleDateClick(date, dayIndex);
826
- this.container.dispatchEvent(
827
- new CustomEvent("dateSelect", { detail: { date, dayIndex } })
997
+ this.dispatchEvent(
998
+ new CustomEvent("cal-date-select", {
999
+ bubbles: true,
1000
+ composed: true,
1001
+ detail: { date, events: this.engine.getEventsForDate(date) }
1002
+ })
828
1003
  );
829
1004
  return;
830
1005
  }
@@ -833,23 +1008,29 @@ var Kalendly = (() => {
833
1008
  const action = actionEl.dataset.action;
834
1009
  e.stopPropagation();
835
1010
  switch (action) {
836
- case "previous":
1011
+ case "previous": {
1012
+ const vm = this.engine.getViewModel();
1013
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1014
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1015
+ this.dispatchMonthChange(tYear, tMonth);
837
1016
  this.actions.previous();
838
- this.dispatchMonthChange();
839
1017
  break;
840
- case "next":
1018
+ }
1019
+ case "next": {
1020
+ const vm = this.engine.getViewModel();
1021
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1022
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1023
+ this.dispatchMonthChange(tYear, tMonth);
841
1024
  this.actions.next();
842
- this.dispatchMonthChange();
843
1025
  break;
844
- case "today":
1026
+ }
1027
+ case "today": {
1028
+ const t = /* @__PURE__ */ new Date();
1029
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
845
1030
  this.actions.goToToday();
846
1031
  this.pickerOpen = false;
847
- this.container.dispatchEvent(
848
- new CustomEvent("monthChange", {
849
- detail: { year: todayYear, month: todayMonth }
850
- })
851
- );
852
1032
  break;
1033
+ }
853
1034
  case "toggle-picker":
854
1035
  this.pickerOpen = !this.pickerOpen;
855
1036
  if (this.pickerOpen) {
@@ -864,8 +1045,8 @@ var Kalendly = (() => {
864
1045
  const newYear = vm.currentYear - 1;
865
1046
  this.yearInput = String(newYear);
866
1047
  this.updatePickerYear(newYear);
1048
+ this.dispatchMonthChange(newYear, vm.currentMonth);
867
1049
  this.actions.jump(newYear, vm.currentMonth);
868
- this.dispatchMonthChange();
869
1050
  }
870
1051
  break;
871
1052
  }
@@ -875,21 +1056,60 @@ var Kalendly = (() => {
875
1056
  const newYear = vm.currentYear + 1;
876
1057
  this.yearInput = String(newYear);
877
1058
  this.updatePickerYear(newYear);
1059
+ this.dispatchMonthChange(newYear, vm.currentMonth);
878
1060
  this.actions.jump(newYear, vm.currentMonth);
879
- this.dispatchMonthChange();
880
1061
  }
881
1062
  break;
882
1063
  }
883
1064
  case "select-month": {
884
1065
  const month = parseInt(actionEl.dataset.month || "0", 10);
885
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1066
+ const year = this.engine.getViewModel().currentYear;
1067
+ this.dispatchMonthChange(year, month);
1068
+ this.actions.jump(year, month);
886
1069
  this.pickerOpen = false;
887
- this.dispatchMonthChange();
888
1070
  break;
889
1071
  }
890
1072
  case "close-popup":
1073
+ this._timeRangeDate = null;
1074
+ this._timeRangeStart = null;
1075
+ this._timeRangeEnd = null;
1076
+ this._timeRangeComplete = false;
891
1077
  this.engine.clearSelection();
892
1078
  break;
1079
+ case "select-slot": {
1080
+ const slotStart = actionEl.dataset.startTime;
1081
+ const slotEnd = actionEl.dataset.endTime;
1082
+ const slotDate = new Date(actionEl.dataset.date);
1083
+ if (this._timeRangeComplete) {
1084
+ this._timeRangeDate = slotDate;
1085
+ this._timeRangeStart = slotStart;
1086
+ this._timeRangeEnd = slotEnd;
1087
+ this._timeRangeComplete = false;
1088
+ } else if (this._timeRangeStart === null) {
1089
+ this._timeRangeDate = slotDate;
1090
+ this._timeRangeStart = slotStart;
1091
+ this._timeRangeEnd = slotEnd;
1092
+ } else {
1093
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1094
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1095
+ this._timeRangeStart = newStart;
1096
+ this._timeRangeEnd = newEnd;
1097
+ this._timeRangeComplete = true;
1098
+ }
1099
+ this.render();
1100
+ this.dispatchEvent(
1101
+ new CustomEvent("cal-availability-select", {
1102
+ bubbles: true,
1103
+ composed: true,
1104
+ detail: {
1105
+ date: this._timeRangeDate,
1106
+ startTime: this._timeRangeStart,
1107
+ endTime: this._timeRangeEnd
1108
+ }
1109
+ })
1110
+ );
1111
+ break;
1112
+ }
893
1113
  }
894
1114
  };
895
1115
  this.delegatedInputHandler = (e) => {
@@ -914,125 +1134,99 @@ var Kalendly = (() => {
914
1134
  target.value = this.yearInput;
915
1135
  target.classList.remove("invalid");
916
1136
  } else if (year !== vm.currentYear) {
1137
+ this.dispatchMonthChange(year, vm.currentMonth);
917
1138
  this.actions.jump(year, vm.currentMonth);
918
- this.dispatchMonthChange();
919
1139
  }
920
1140
  };
921
1141
  this.delegatedKeydownHandler = (e) => {
922
1142
  const target = e.target;
923
1143
  if (!target.matches("[data-year-input]")) return;
924
- if (e.key === "Enter") {
925
- target.blur();
926
- }
1144
+ if (e.key === "Enter") target.blur();
927
1145
  };
928
- this.container.addEventListener("click", this.delegatedClickHandler);
929
- this.container.addEventListener("input", this.delegatedInputHandler);
930
- this.container.addEventListener("blur", this.delegatedBlurHandler, true);
931
- this.container.addEventListener("keydown", this.delegatedKeydownHandler);
1146
+ this.addEventListener("click", this.delegatedClickHandler);
1147
+ this.addEventListener("input", this.delegatedInputHandler);
1148
+ this.addEventListener("blur", this.delegatedBlurHandler, true);
1149
+ this.addEventListener("keydown", this.delegatedKeydownHandler);
932
1150
  this.clickOutsideHandler = (e) => {
933
1151
  const target = e.target;
934
- if (this.container.contains(target)) return;
1152
+ if (this.contains(target)) return;
935
1153
  if (this.pickerOpen) {
936
1154
  this.pickerOpen = false;
937
1155
  this.render();
938
1156
  return;
939
1157
  }
940
- const popup = this.container.querySelector(".date-popup");
1158
+ const popup = this.querySelector(".date-popup");
941
1159
  if (popup) {
942
1160
  this.engine.clearSelection();
943
1161
  }
944
1162
  };
945
1163
  document.addEventListener("click", this.clickOutsideHandler);
946
1164
  }
947
- // Targeted update for picker year - avoids full DOM rebuild
948
1165
  updatePickerYear(year) {
949
- const input = this.container.querySelector(
950
- "[data-year-input]"
951
- );
952
- if (input) {
953
- input.value = String(year);
954
- }
955
- const today = /* @__PURE__ */ new Date();
956
- const todayYear = today.getFullYear();
957
- const minYear = this.props.minYear ?? todayYear - 30;
958
- const maxYear = this.props.maxYear ?? todayYear + 10;
959
- const prevBtn = this.container.querySelector(
1166
+ const input = this.querySelector("[data-year-input]");
1167
+ if (input) input.value = String(year);
1168
+ const prevBtn = this.querySelector(
960
1169
  '[data-action="year-prev"]'
961
1170
  );
962
- const nextBtn = this.container.querySelector(
1171
+ const nextBtn = this.querySelector(
963
1172
  '[data-action="year-next"]'
964
1173
  );
965
- if (prevBtn) prevBtn.disabled = year <= minYear;
966
- if (nextBtn) nextBtn.disabled = year >= maxYear;
1174
+ if (prevBtn) prevBtn.disabled = year <= this.minYear;
1175
+ if (nextBtn) nextBtn.disabled = year >= this.maxYear;
967
1176
  }
968
- dispatchMonthChange() {
1177
+ dispatchMonthChange(year, month) {
1178
+ if (!this.engine) return;
969
1179
  const vm = this.engine.getViewModel();
970
- this.container.dispatchEvent(
971
- new CustomEvent("monthChange", {
972
- detail: { year: vm.currentYear, month: vm.currentMonth }
1180
+ this.dispatchEvent(
1181
+ new CustomEvent("cal-month-change", {
1182
+ bubbles: true,
1183
+ composed: true,
1184
+ detail: {
1185
+ year: year ?? vm.currentYear,
1186
+ month: month ?? vm.currentMonth
1187
+ }
973
1188
  })
974
1189
  );
975
1190
  }
1191
+ // Public API
976
1192
  updateEvents(events) {
977
- this.engine.updateEvents(events);
1193
+ this.events = events;
978
1194
  }
979
1195
  updateTheme(theme) {
980
- this.props.theme = theme;
981
- this.applyTheme();
1196
+ this.theme = theme;
982
1197
  }
983
1198
  getCurrentDate() {
984
- return this.engine.getViewModel().selectedDate;
1199
+ return this.engine?.getViewModel().selectedDate ?? null;
985
1200
  }
986
1201
  goToDate(date) {
987
- this.actions.jump(date.getFullYear(), date.getMonth());
1202
+ const year = date.getFullYear();
1203
+ const month = date.getMonth();
1204
+ this.dispatchMonthChange(year, month);
1205
+ this.actions?.jump(year, month);
988
1206
  }
989
1207
  getEngine() {
1208
+ if (!this.engine)
1209
+ throw new Error("CalendarElement is not connected to the DOM");
990
1210
  return this.engine;
991
1211
  }
992
- destroy() {
993
- if (this.unsubscribe) {
994
- this.unsubscribe();
995
- this.unsubscribe = null;
996
- }
997
- if (this.delegatedClickHandler) {
998
- this.container.removeEventListener("click", this.delegatedClickHandler);
999
- this.delegatedClickHandler = null;
1000
- }
1001
- if (this.delegatedInputHandler) {
1002
- this.container.removeEventListener("input", this.delegatedInputHandler);
1003
- this.delegatedInputHandler = null;
1004
- }
1005
- if (this.delegatedBlurHandler) {
1006
- this.container.removeEventListener(
1007
- "blur",
1008
- this.delegatedBlurHandler,
1009
- true
1010
- );
1011
- this.delegatedBlurHandler = null;
1012
- }
1013
- if (this.delegatedKeydownHandler) {
1014
- this.container.removeEventListener(
1015
- "keydown",
1016
- this.delegatedKeydownHandler
1017
- );
1018
- this.delegatedKeydownHandler = null;
1019
- }
1020
- if (this.clickOutsideHandler) {
1021
- document.removeEventListener("click", this.clickOutsideHandler);
1022
- this.clickOutsideHandler = null;
1023
- }
1024
- this.listenersAttached = false;
1025
- this.engine.destroy();
1026
- this.container.innerHTML = "";
1027
- this.container.classList.remove("kalendly-calendar");
1028
- if (this.props.className) {
1029
- this.container.classList.remove(this.props.className);
1030
- }
1031
- }
1032
1212
  };
1033
- function createCalendar(props) {
1034
- return new VanillaCalendar(props);
1213
+ CalendarElement.observedAttributes = [
1214
+ "initial-date",
1215
+ "min-year",
1216
+ "max-year",
1217
+ "week-starts-on",
1218
+ "title",
1219
+ "use-short-month-names",
1220
+ "availability-mode",
1221
+ "selectable",
1222
+ "loading"
1223
+ ];
1224
+ function defineCalendarElement(tagName = "kal-calendar") {
1225
+ if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1226
+ customElements.define(tagName, CalendarElement);
1227
+ }
1035
1228
  }
1036
- return __toCommonJS(index_exports);
1229
+ return __toCommonJS(CalendarElement_exports);
1037
1230
  })();
1231
+ if (typeof customElements !== 'undefined' && !customElements.get('kal-calendar')) { customElements.define('kal-calendar', Kalendly.CalendarElement); }
1038
1232
  //# sourceMappingURL=index.umd.js.map