kalendly 0.3.1 → 0.3.3

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
@@ -31,6 +31,54 @@ function generateYears(minYear, maxYear) {
31
31
  const max = maxYear ?? currentYear + 10;
32
32
  return Array.from({ length: max - min + 1 }, (_, i) => min + i);
33
33
  }
34
+ function parseHourRanges(value, slotDuration) {
35
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
36
+ if (parts.length === 0) {
37
+ throw new Error(
38
+ `<kal-calendar> available-hours is empty. Omit the attribute to allow every hour, or name at least one HH:MM-HH:MM range.`
39
+ );
40
+ }
41
+ const ranges = parts.map((part) => {
42
+ const halves = part.split("-");
43
+ if (halves.length !== 2) {
44
+ throw new Error(
45
+ `<kal-calendar> available-hours range "${part}" is not HH:MM-HH:MM.`
46
+ );
47
+ }
48
+ const [start, end] = halves.map((half) => parseTimeToMinutes(half.trim()));
49
+ if (start === null || end === null) {
50
+ throw new Error(
51
+ `<kal-calendar> available-hours range "${part}" has an unreadable time. Use 24-hour HH:MM.`
52
+ );
53
+ }
54
+ if (start >= end) {
55
+ throw new Error(
56
+ `<kal-calendar> available-hours range "${part}" starts at or after it ends.`
57
+ );
58
+ }
59
+ if (start % slotDuration !== 0 || end % slotDuration !== 0) {
60
+ throw new Error(
61
+ `<kal-calendar> available-hours range "${part}" does not land on a ${slotDuration}-minute slot boundary.`
62
+ );
63
+ }
64
+ return [start, end];
65
+ });
66
+ if (mergeIntervals(ranges).length !== ranges.length) {
67
+ throw new Error(
68
+ `<kal-calendar> available-hours ranges overlap or touch: "${value}". Write each bookable window once.`
69
+ );
70
+ }
71
+ return ranges;
72
+ }
73
+ function isDateWithinWindow(date, min, max) {
74
+ const target = normalizeDate(date).getTime();
75
+ if (min && target < normalizeDate(min).getTime()) return false;
76
+ if (max && target > normalizeDate(max).getTime()) return false;
77
+ return true;
78
+ }
79
+ function isDayAllowed(date, days) {
80
+ return days === null || days.includes(date.getDay());
81
+ }
34
82
  function eventCoversDate(event, date) {
35
83
  const start = normalizeDate(new Date(event.date)).getTime();
36
84
  const target = normalizeDate(date).getTime();
@@ -472,12 +520,14 @@ var init_calendar_engine = __esm({
472
520
  /**
473
521
  * Select a specific date
474
522
  */
475
- selectDate(date, dayIndex) {
523
+ selectDate(date, dayIndex, navigate = true) {
476
524
  this.state.selectedDate = date;
477
525
  this.state.selectedDayIndex = dayIndex ?? null;
478
526
  this.state.currentDate = date.getDate();
479
- this.state.currentMonth = date.getMonth();
480
- this.state.currentYear = date.getFullYear();
527
+ if (navigate) {
528
+ this.state.currentMonth = date.getMonth();
529
+ this.state.currentYear = date.getFullYear();
530
+ }
481
531
  this.updateTasks();
482
532
  this.notify();
483
533
  }
@@ -505,8 +555,8 @@ var init_calendar_engine = __esm({
505
555
  /**
506
556
  * Handle date cell click
507
557
  */
508
- handleDateClick(date, dayIndex) {
509
- this.selectDate(date, dayIndex);
558
+ handleDateClick(date, dayIndex, options) {
559
+ this.selectDate(date, dayIndex, options?.navigate !== false);
510
560
  }
511
561
  /**
512
562
  * Check if date has events
@@ -763,6 +813,41 @@ var init_CalendarElement = __esm({
763
813
  const val = this.getAttribute("max-year");
764
814
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() + 10;
765
815
  }
816
+ boundaryDate(attr) {
817
+ const val = this.getAttribute(attr);
818
+ if (!val) return null;
819
+ const parsed = new Date(val);
820
+ if (Number.isNaN(parsed.getTime())) {
821
+ throw new Error(
822
+ `<kal-calendar> ${attr} is unreadable: "${val}". Use a value Date can parse, the same as initial-date.`
823
+ );
824
+ }
825
+ return parsed;
826
+ }
827
+ get availableDays() {
828
+ const val = this.getAttribute("available-days");
829
+ if (val === null) return null;
830
+ const parts = val.split(",").map((part) => part.trim()).filter(Boolean);
831
+ if (parts.length === 0) {
832
+ throw new Error(
833
+ `<kal-calendar> available-days is empty. Omit the attribute to allow every day, or name at least one weekday.`
834
+ );
835
+ }
836
+ return parts.map((part) => {
837
+ const day = Number(part);
838
+ if (!Number.isInteger(day) || day < 0 || day > 6) {
839
+ throw new Error(
840
+ `<kal-calendar> available-days must list integers 0-6, 0 = Sunday. Got "${part}".`
841
+ );
842
+ }
843
+ return day;
844
+ });
845
+ }
846
+ get availableHours() {
847
+ const val = this.getAttribute("available-hours");
848
+ if (val === null) return null;
849
+ return parseHourRanges(val, this.slotDuration);
850
+ }
766
851
  initEngine() {
767
852
  const initialDateAttr = this.getAttribute("initial-date");
768
853
  const initialDate = initialDateAttr ? new Date(initialDateAttr) : void 0;
@@ -881,8 +966,26 @@ var init_CalendarElement = __esm({
881
966
  if (declared.size === 0) return "open";
882
967
  return this.knownBuckets.find((bucket) => declared.has(bucket));
883
968
  }
969
+ assertHorizonOrdered() {
970
+ const min = this.boundaryDate("min-date");
971
+ const max = this.boundaryDate("max-date");
972
+ if (min && max && min.getTime() > max.getTime()) {
973
+ throw new Error(
974
+ `<kal-calendar> min-date is after max-date: "${this.getAttribute("min-date")}" > "${this.getAttribute("max-date")}".`
975
+ );
976
+ }
977
+ }
978
+ // Whether the vendor offers this day at all, before any event is considered.
979
+ isDateOffered(date) {
980
+ return isDateWithinWindow(
981
+ date,
982
+ this.boundaryDate("min-date"),
983
+ this.boundaryDate("max-date")
984
+ ) && isDayAllowed(date, this.availableDays);
985
+ }
884
986
  isDateSelectable(date) {
885
987
  if (!this.engine) return false;
988
+ if (!this.isDateOffered(date)) return false;
886
989
  if (this._selectableStatuses) {
887
990
  return this._selectableStatuses.includes(this.resolveBucket(date));
888
991
  }
@@ -902,6 +1005,11 @@ var init_CalendarElement = __esm({
902
1005
  this.assertStatusesDeclared();
903
1006
  this.assertStatusesKnown();
904
1007
  }
1008
+ this.boundaryDate("min-date");
1009
+ this.boundaryDate("max-date");
1010
+ void this.availableDays;
1011
+ void this.availableHours;
1012
+ this.assertHorizonOrdered();
905
1013
  const today = /* @__PURE__ */ new Date();
906
1014
  const todayMonth = today.getMonth();
907
1015
  const todayYear = today.getFullYear();
@@ -945,7 +1053,7 @@ var init_CalendarElement = __esm({
945
1053
  return `
946
1054
  <div class="event-card" style="border-left-color: ${borderColor}">
947
1055
  <div class="event-header">
948
- <div class="event-title">${escapeHtml(event.name)}</div>
1056
+ ${event.name ? `<div class="event-title">${escapeHtml(event.name)}</div>` : ""}
949
1057
  <div class="event-badges">
950
1058
  ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
951
1059
  ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
@@ -1021,25 +1129,31 @@ var init_CalendarElement = __esm({
1021
1129
  ];
1022
1130
  this.warnMissingEndTime(events);
1023
1131
  const booked = bookedSlots(events, date, slotDuration);
1132
+ const hours = this.availableHours;
1024
1133
  const slots = booked.map((isBooked, index) => {
1025
- const startTime = formatMinutes(index * slotDuration);
1026
- const endTime = formatMinutes((index + 1) * slotDuration);
1027
- const inTimeRange = !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
1134
+ const slotStart = index * slotDuration;
1135
+ const slotEnd = slotStart + slotDuration;
1136
+ const startTime = formatMinutes(slotStart);
1137
+ const endTime = formatMinutes(slotEnd);
1138
+ const offered = hours === null || hours.some(([from, to]) => slotStart >= from && slotEnd <= to);
1139
+ const inTimeRange = offered && !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
1028
1140
  const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
1029
1141
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
1030
1142
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
1031
1143
  const slotClasses = [
1032
1144
  "time-grid-slot",
1145
+ offered ? "" : "time-grid-slot-out-of-range",
1033
1146
  isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
1034
1147
  isRangeStart ? "time-grid-slot-range-start" : "",
1035
1148
  isRangeEnd ? "time-grid-slot-range-end" : "",
1036
1149
  isInRange ? "time-grid-slot-in-range" : ""
1037
1150
  ].filter(Boolean).join(" ");
1038
- const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
1151
+ const slotAttrs = `${offered ? 'data-action="select-slot" ' : ""}data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
1152
+ const status = !offered ? "Closed" : isBooked ? "Booked" : "Available";
1039
1153
  return `
1040
1154
  <div class="${slotClasses}" ${slotAttrs}>
1041
1155
  <span class="time-grid-label">${startTime}</span>
1042
- <span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
1156
+ <span class="time-grid-status">${status}</span>
1043
1157
  </div>`;
1044
1158
  });
1045
1159
  const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
@@ -1074,7 +1188,7 @@ var init_CalendarElement = __esm({
1074
1188
  )) {
1075
1189
  classes.push("calendar-cell-selected");
1076
1190
  }
1077
- if (availabilityMode && calendarDate.isCurrentMonth) {
1191
+ if (availabilityMode) {
1078
1192
  const bucket = this.resolveBucket(
1079
1193
  calendarDate.date
1080
1194
  );
@@ -1095,7 +1209,7 @@ var init_CalendarElement = __esm({
1095
1209
  `aria-label="${escapeHtml(bucket)}"`
1096
1210
  );
1097
1211
  }
1098
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
1212
+ if (availabilityMode === "day" && selectable) {
1099
1213
  const d = calendarDate.date;
1100
1214
  if (this._rangeStart && isSameDay2(d, this._rangeStart))
1101
1215
  classes.push("availability-range-start");
@@ -1107,12 +1221,18 @@ var init_CalendarElement = __esm({
1107
1221
  }
1108
1222
  }
1109
1223
  const dateString = calendarDate.date.toISOString();
1224
+ const offered = this.isDateOffered(
1225
+ calendarDate.date
1226
+ );
1227
+ if (!offered) {
1228
+ classes.push("calendar-cell-out-of-range");
1229
+ }
1110
1230
  return `
1111
1231
  <td
1112
1232
  class="${classes.join(" ")}"
1113
1233
  data-date="${dateString}"
1114
1234
  data-day-index="${dayIndex}"
1115
- data-clickable="true"
1235
+ ${offered ? 'data-clickable="true"' : ""}
1116
1236
  ${cellAttrs.join(" ")}
1117
1237
  >
1118
1238
  ${calendarDate.date.getDate()}
@@ -1300,7 +1420,7 @@ var init_CalendarElement = __esm({
1300
1420
  this._timeRangeEnd = null;
1301
1421
  this._timeRangeComplete = false;
1302
1422
  }
1303
- this.engine.handleDateClick(date, dayIndex);
1423
+ this.engine.handleDateClick(date, dayIndex, { navigate: false });
1304
1424
  this.dispatchEvent(
1305
1425
  new CustomEvent("cal-date-select", {
1306
1426
  bubbles: true,
@@ -1538,6 +1658,10 @@ var init_CalendarElement = __esm({
1538
1658
  "initial-date",
1539
1659
  "min-year",
1540
1660
  "max-year",
1661
+ "min-date",
1662
+ "max-date",
1663
+ "available-days",
1664
+ "available-hours",
1541
1665
  "week-starts-on",
1542
1666
  "heading",
1543
1667
  "months",
@@ -1559,6 +1683,18 @@ var init_CalendarElement = __esm({
1559
1683
  borderColor: "--calendar-border-color",
1560
1684
  todayOutline: "--calendar-today-outline",
1561
1685
  selectedBg: "--calendar-selected-bg",
1686
+ outOfRangeBg: "--calendar-out-of-range-bg",
1687
+ outOfRangeFg: "--calendar-out-of-range-fg",
1688
+ navArrowFg: "--calendar-nav-arrow-fg",
1689
+ navArrowBg: "--calendar-nav-arrow-bg",
1690
+ navArrowBorder: "--calendar-nav-arrow-border",
1691
+ navArrowHoverFg: "--calendar-nav-arrow-hover-fg",
1692
+ navArrowHoverBg: "--calendar-nav-arrow-hover-bg",
1693
+ inputInvalidBg: "--calendar-input-invalid-bg",
1694
+ popupHeaderFg: "--calendar-popup-header-fg",
1695
+ popupCloseFg: "--calendar-popup-close-fg",
1696
+ popupCloseBg: "--calendar-popup-close-bg",
1697
+ popupCloseHoverBg: "--calendar-popup-close-hover-bg",
1562
1698
  headerBg: "--calendar-header-bg",
1563
1699
  popupBg: "--calendar-popup-bg",
1564
1700
  pickerBg: "--calendar-picker-bg",
@@ -1636,12 +1772,15 @@ export {
1636
1772
  getEventsForDate,
1637
1773
  getMonthYearText,
1638
1774
  hasEvents,
1775
+ isDateWithinWindow,
1776
+ isDayAllowed,
1639
1777
  isSameDay,
1640
1778
  isToday,
1641
1779
  isValidHexColor,
1642
1780
  mergeCategoryColors,
1643
1781
  mergeIntervals,
1644
1782
  normalizeDate,
1783
+ parseHourRanges,
1645
1784
  parseTimeToMinutes,
1646
1785
  safeColor,
1647
1786
  safeUrl,