kalendly 0.3.1 → 0.3.2

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();
@@ -763,6 +811,41 @@ var init_CalendarElement = __esm({
763
811
  const val = this.getAttribute("max-year");
764
812
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() + 10;
765
813
  }
814
+ boundaryDate(attr) {
815
+ const val = this.getAttribute(attr);
816
+ if (!val) return null;
817
+ const parsed = new Date(val);
818
+ if (Number.isNaN(parsed.getTime())) {
819
+ throw new Error(
820
+ `<kal-calendar> ${attr} is unreadable: "${val}". Use a value Date can parse, the same as initial-date.`
821
+ );
822
+ }
823
+ return parsed;
824
+ }
825
+ get availableDays() {
826
+ const val = this.getAttribute("available-days");
827
+ if (val === null) return null;
828
+ const parts = val.split(",").map((part) => part.trim()).filter(Boolean);
829
+ if (parts.length === 0) {
830
+ throw new Error(
831
+ `<kal-calendar> available-days is empty. Omit the attribute to allow every day, or name at least one weekday.`
832
+ );
833
+ }
834
+ return parts.map((part) => {
835
+ const day = Number(part);
836
+ if (!Number.isInteger(day) || day < 0 || day > 6) {
837
+ throw new Error(
838
+ `<kal-calendar> available-days must list integers 0-6, 0 = Sunday. Got "${part}".`
839
+ );
840
+ }
841
+ return day;
842
+ });
843
+ }
844
+ get availableHours() {
845
+ const val = this.getAttribute("available-hours");
846
+ if (val === null) return null;
847
+ return parseHourRanges(val, this.slotDuration);
848
+ }
766
849
  initEngine() {
767
850
  const initialDateAttr = this.getAttribute("initial-date");
768
851
  const initialDate = initialDateAttr ? new Date(initialDateAttr) : void 0;
@@ -881,8 +964,26 @@ var init_CalendarElement = __esm({
881
964
  if (declared.size === 0) return "open";
882
965
  return this.knownBuckets.find((bucket) => declared.has(bucket));
883
966
  }
967
+ assertHorizonOrdered() {
968
+ const min = this.boundaryDate("min-date");
969
+ const max = this.boundaryDate("max-date");
970
+ if (min && max && min.getTime() > max.getTime()) {
971
+ throw new Error(
972
+ `<kal-calendar> min-date is after max-date: "${this.getAttribute("min-date")}" > "${this.getAttribute("max-date")}".`
973
+ );
974
+ }
975
+ }
976
+ // Whether the vendor offers this day at all, before any event is considered.
977
+ isDateOffered(date) {
978
+ return isDateWithinWindow(
979
+ date,
980
+ this.boundaryDate("min-date"),
981
+ this.boundaryDate("max-date")
982
+ ) && isDayAllowed(date, this.availableDays);
983
+ }
884
984
  isDateSelectable(date) {
885
985
  if (!this.engine) return false;
986
+ if (!this.isDateOffered(date)) return false;
886
987
  if (this._selectableStatuses) {
887
988
  return this._selectableStatuses.includes(this.resolveBucket(date));
888
989
  }
@@ -902,6 +1003,11 @@ var init_CalendarElement = __esm({
902
1003
  this.assertStatusesDeclared();
903
1004
  this.assertStatusesKnown();
904
1005
  }
1006
+ this.boundaryDate("min-date");
1007
+ this.boundaryDate("max-date");
1008
+ void this.availableDays;
1009
+ void this.availableHours;
1010
+ this.assertHorizonOrdered();
905
1011
  const today = /* @__PURE__ */ new Date();
906
1012
  const todayMonth = today.getMonth();
907
1013
  const todayYear = today.getFullYear();
@@ -945,7 +1051,7 @@ var init_CalendarElement = __esm({
945
1051
  return `
946
1052
  <div class="event-card" style="border-left-color: ${borderColor}">
947
1053
  <div class="event-header">
948
- <div class="event-title">${escapeHtml(event.name)}</div>
1054
+ ${event.name ? `<div class="event-title">${escapeHtml(event.name)}</div>` : ""}
949
1055
  <div class="event-badges">
950
1056
  ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
951
1057
  ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
@@ -1021,25 +1127,31 @@ var init_CalendarElement = __esm({
1021
1127
  ];
1022
1128
  this.warnMissingEndTime(events);
1023
1129
  const booked = bookedSlots(events, date, slotDuration);
1130
+ const hours = this.availableHours;
1024
1131
  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;
1132
+ const slotStart = index * slotDuration;
1133
+ const slotEnd = slotStart + slotDuration;
1134
+ const startTime = formatMinutes(slotStart);
1135
+ const endTime = formatMinutes(slotEnd);
1136
+ const offered = hours === null || hours.some(([from, to]) => slotStart >= from && slotEnd <= to);
1137
+ const inTimeRange = offered && !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
1028
1138
  const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
1029
1139
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
1030
1140
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
1031
1141
  const slotClasses = [
1032
1142
  "time-grid-slot",
1143
+ offered ? "" : "time-grid-slot-out-of-range",
1033
1144
  isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
1034
1145
  isRangeStart ? "time-grid-slot-range-start" : "",
1035
1146
  isRangeEnd ? "time-grid-slot-range-end" : "",
1036
1147
  isInRange ? "time-grid-slot-in-range" : ""
1037
1148
  ].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}"`;
1149
+ const slotAttrs = `${offered ? 'data-action="select-slot" ' : ""}data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
1150
+ const status = !offered ? "Closed" : isBooked ? "Booked" : "Available";
1039
1151
  return `
1040
1152
  <div class="${slotClasses}" ${slotAttrs}>
1041
1153
  <span class="time-grid-label">${startTime}</span>
1042
- <span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
1154
+ <span class="time-grid-status">${status}</span>
1043
1155
  </div>`;
1044
1156
  });
1045
1157
  const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
@@ -1107,12 +1219,18 @@ var init_CalendarElement = __esm({
1107
1219
  }
1108
1220
  }
1109
1221
  const dateString = calendarDate.date.toISOString();
1222
+ const offered = this.isDateOffered(
1223
+ calendarDate.date
1224
+ );
1225
+ if (!offered) {
1226
+ classes.push("calendar-cell-out-of-range");
1227
+ }
1110
1228
  return `
1111
1229
  <td
1112
1230
  class="${classes.join(" ")}"
1113
1231
  data-date="${dateString}"
1114
1232
  data-day-index="${dayIndex}"
1115
- data-clickable="true"
1233
+ ${offered ? 'data-clickable="true"' : ""}
1116
1234
  ${cellAttrs.join(" ")}
1117
1235
  >
1118
1236
  ${calendarDate.date.getDate()}
@@ -1538,6 +1656,10 @@ var init_CalendarElement = __esm({
1538
1656
  "initial-date",
1539
1657
  "min-year",
1540
1658
  "max-year",
1659
+ "min-date",
1660
+ "max-date",
1661
+ "available-days",
1662
+ "available-hours",
1541
1663
  "week-starts-on",
1542
1664
  "heading",
1543
1665
  "months",
@@ -1559,6 +1681,8 @@ var init_CalendarElement = __esm({
1559
1681
  borderColor: "--calendar-border-color",
1560
1682
  todayOutline: "--calendar-today-outline",
1561
1683
  selectedBg: "--calendar-selected-bg",
1684
+ outOfRangeBg: "--calendar-out-of-range-bg",
1685
+ outOfRangeFg: "--calendar-out-of-range-fg",
1562
1686
  headerBg: "--calendar-header-bg",
1563
1687
  popupBg: "--calendar-popup-bg",
1564
1688
  pickerBg: "--calendar-picker-bg",
@@ -1636,12 +1760,15 @@ export {
1636
1760
  getEventsForDate,
1637
1761
  getMonthYearText,
1638
1762
  hasEvents,
1763
+ isDateWithinWindow,
1764
+ isDayAllowed,
1639
1765
  isSameDay,
1640
1766
  isToday,
1641
1767
  isValidHexColor,
1642
1768
  mergeCategoryColors,
1643
1769
  mergeIntervals,
1644
1770
  normalizeDate,
1771
+ parseHourRanges,
1645
1772
  parseTimeToMinutes,
1646
1773
  safeColor,
1647
1774
  safeUrl,