kalendly 0.3.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -43,12 +43,28 @@ function generateYears(minYear, maxYear) {
43
43
  const max = maxYear ?? currentYear + 10;
44
44
  return Array.from({ length: max - min + 1 }, (_, i) => min + i);
45
45
  }
46
+ function eventCoversDate(event, date) {
47
+ const start = normalizeDate(new Date(event.date)).getTime();
48
+ const target = normalizeDate(date).getTime();
49
+ if (Number.isNaN(start)) return false;
50
+ if (event.endDate === void 0 || event.endDate === null) {
51
+ return start === target;
52
+ }
53
+ const end = normalizeDate(new Date(event.endDate)).getTime();
54
+ if (Number.isNaN(end)) {
55
+ throw new Error(
56
+ `<kal-calendar> event ${event.id} has an unreadable endDate: ${String(event.endDate)}.`
57
+ );
58
+ }
59
+ if (end < start) {
60
+ throw new Error(
61
+ `<kal-calendar> event ${event.id} has an endDate before its date: ${String(event.endDate)} < ${String(event.date)}.`
62
+ );
63
+ }
64
+ return target >= start && target <= end;
65
+ }
46
66
  function getEventsForDate(events, date) {
47
- const normalizedTargetDate = normalizeDate(date);
48
- return events.filter((event) => {
49
- const eventDate = normalizeDate(new Date(event.date));
50
- return eventDate.getTime() === normalizedTargetDate.getTime();
51
- });
67
+ return events.filter((event) => eventCoversDate(event, date));
52
68
  }
53
69
  function hasEvents(events, date) {
54
70
  return getEventsForDate(events, date).length > 0;
@@ -192,7 +208,67 @@ function safeColor(value) {
192
208
  const trimmed = String(value).trim();
193
209
  return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
194
210
  }
195
- var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES;
211
+ function parseTimeToMinutes(time) {
212
+ if (typeof time !== "string") return null;
213
+ const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
214
+ if (!match) return null;
215
+ const hours = Number(match[1]);
216
+ const mins = Number(match[2]);
217
+ if (hours > 24 || mins > 59 || hours === 24 && mins > 0) return null;
218
+ return hours * 60 + mins;
219
+ }
220
+ function formatMinutes(total) {
221
+ const hours = Math.floor(total / 60);
222
+ const mins = total % 60;
223
+ return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
224
+ }
225
+ function eventInterval(event, slotDuration, onDay) {
226
+ const day = normalizeDate(onDay).getTime();
227
+ if (Number.isNaN(day)) return null;
228
+ const base = day / 6e4;
229
+ const wholeDay = [base, base + MINUTES_PER_DAY];
230
+ if (event.allDay || !event.startTime) return wholeDay;
231
+ const from = parseTimeToMinutes(event.startTime);
232
+ if (from === null) return wholeDay;
233
+ if (event.endTime === void 0 || event.endTime === null) {
234
+ return [base + from, base + from + slotDuration];
235
+ }
236
+ const to = parseTimeToMinutes(event.endTime);
237
+ if (to === null) return wholeDay;
238
+ return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];
239
+ }
240
+ function mergeIntervals(intervals) {
241
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
242
+ return sorted.reduce((merged, [start, end]) => {
243
+ const last = merged[merged.length - 1];
244
+ if (last && start <= last[1]) last[1] = Math.max(last[1], end);
245
+ else merged.push([start, end]);
246
+ return merged;
247
+ }, []);
248
+ }
249
+ function bookedSlots(events, date, slotDuration = DEFAULT_SLOT_DURATION) {
250
+ const base = normalizeDate(date).getTime() / 6e4;
251
+ const previousDay = new Date(date);
252
+ previousDay.setDate(previousDay.getDate() - 1);
253
+ const intervals = [];
254
+ for (const event of new Set(events)) {
255
+ for (const day of [previousDay, date]) {
256
+ if (!eventCoversDate(event, day)) continue;
257
+ const interval = eventInterval(event, slotDuration, day);
258
+ if (interval) intervals.push(interval);
259
+ }
260
+ }
261
+ const merged = mergeIntervals(intervals);
262
+ return Array.from(
263
+ { length: Math.floor(MINUTES_PER_DAY / slotDuration) },
264
+ (_, index) => {
265
+ const start = base + index * slotDuration;
266
+ const end = start + slotDuration;
267
+ return merged.some(([from, to]) => start < to && end > from);
268
+ }
269
+ );
270
+ }
271
+ var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES, MINUTES_PER_DAY, DEFAULT_SLOT_DURATION;
196
272
  var init_utils = __esm({
197
273
  "src/core/utils.ts"() {
198
274
  "use strict";
@@ -248,6 +324,8 @@ var init_utils = __esm({
248
324
  '"': "&quot;",
249
325
  "'": "&#39;"
250
326
  };
327
+ MINUTES_PER_DAY = 1440;
328
+ DEFAULT_SLOT_DURATION = 60;
251
329
  }
252
330
  });
253
331
 
@@ -661,6 +739,29 @@ var init_CalendarElement = __esm({
661
739
  }
662
740
  return title;
663
741
  }
742
+ get slotDuration() {
743
+ const raw = this.getAttribute("slot-duration");
744
+ if (raw === null) return DEFAULT_SLOT_DURATION;
745
+ const minutes = Number(raw);
746
+ if (Number.isInteger(minutes) && minutes > 0 && MINUTES_PER_DAY % minutes === 0) {
747
+ return minutes;
748
+ }
749
+ console.warn(
750
+ `<kal-calendar> slot-duration="${raw}" must be a positive whole number of minutes that divides ${MINUTES_PER_DAY} \u2014 falling back to ${DEFAULT_SLOT_DURATION}.`
751
+ );
752
+ return DEFAULT_SLOT_DURATION;
753
+ }
754
+ warnMissingEndTime(events) {
755
+ for (const event of events) {
756
+ if (!event.startTime || event.endTime) continue;
757
+ const key = String(event.id);
758
+ if (_CalendarElement.warnedMissingEnd.has(key)) continue;
759
+ _CalendarElement.warnedMissingEnd.add(key);
760
+ console.warn(
761
+ `<kal-calendar> event ${key} has startTime but no endTime \u2014 occupying one ${this.slotDuration}-minute slot. Send an endTime to say how long it runs.`
762
+ );
763
+ }
764
+ }
664
765
  get monthCount() {
665
766
  const requested = Number(this.getAttribute("months") ?? 1);
666
767
  if (!Number.isInteger(requested) || requested < 1) return 1;
@@ -922,42 +1023,39 @@ var init_CalendarElement = __esm({
922
1023
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
923
1024
  const renderEvent = this._renderEvent || defaultRenderEvent;
924
1025
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
925
- const renderTimeGrid = (events, date) => {
926
- const startHour = (time) => {
927
- if (typeof time !== "string") return null;
928
- const hour = Number(time.split(":")[0]);
929
- return Number.isInteger(hour) && hour >= 0 && hour <= 24 ? hour : null;
930
- };
931
- const isHourBooked = (hour) => events.some((event) => {
932
- if (!event.startTime) return true;
933
- const from = startHour(event.startTime);
934
- const to = event.endTime ? startHour(event.endTime) : 24;
935
- if (from === null || to === null) return true;
936
- return hour >= from && hour < to;
937
- });
938
- const slots = Array.from({ length: 24 }, (_, hour) => {
939
- const startTime = `${String(hour).padStart(2, "0")}:00`;
940
- const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
941
- const booked = isHourBooked(hour);
942
- const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
1026
+ const renderTimeGrid = (date) => {
1027
+ const slotDuration = this.slotDuration;
1028
+ const previousDay = new Date(date);
1029
+ previousDay.setDate(previousDay.getDate() - 1);
1030
+ const events = [
1031
+ ...this.engine.getEventsForDate(previousDay),
1032
+ ...this.engine.getEventsForDate(date)
1033
+ ];
1034
+ this.warnMissingEndTime(events);
1035
+ const booked = bookedSlots(events, date, slotDuration);
1036
+ const slots = booked.map((isBooked, index) => {
1037
+ const startTime = formatMinutes(index * slotDuration);
1038
+ const endTime = formatMinutes((index + 1) * slotDuration);
1039
+ const inTimeRange = !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
943
1040
  const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
944
1041
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
945
1042
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
946
1043
  const slotClasses = [
947
1044
  "time-grid-slot",
948
- booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
1045
+ isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
949
1046
  isRangeStart ? "time-grid-slot-range-start" : "",
950
1047
  isRangeEnd ? "time-grid-slot-range-end" : "",
951
1048
  isInRange ? "time-grid-slot-in-range" : ""
952
1049
  ].filter(Boolean).join(" ");
953
- const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
1050
+ const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
954
1051
  return `
955
1052
  <div class="${slotClasses}" ${slotAttrs}>
956
1053
  <span class="time-grid-label">${startTime}</span>
957
- <span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
1054
+ <span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
958
1055
  </div>`;
959
1056
  });
960
- return `<div class="time-grid">${slots.join("")}</div>`;
1057
+ const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
1058
+ return `<div class="${gridClasses}">${slots.join("")}</div>`;
961
1059
  };
962
1060
  const multiMonth = viewModel.panes.length > 1;
963
1061
  const renderPane = (pane) => `
@@ -982,6 +1080,12 @@ var init_CalendarElement = __esm({
982
1080
  ${week.map((calendarDate, dayIndex) => {
983
1081
  const classes = getCellClasses(calendarDate);
984
1082
  const cellAttrs = [];
1083
+ if (viewModel.selectedDate && isSameDay2(
1084
+ calendarDate.date,
1085
+ viewModel.selectedDate
1086
+ )) {
1087
+ classes.push("calendar-cell-selected");
1088
+ }
985
1089
  if (availabilityMode && calendarDate.isCurrentMonth) {
986
1090
  const bucket = this.resolveBucket(
987
1091
  calendarDate.date
@@ -1134,7 +1238,7 @@ var init_CalendarElement = __esm({
1134
1238
  ` : ""}
1135
1239
 
1136
1240
  <div class="events-container">
1137
- ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
1241
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
1138
1242
  </div>
1139
1243
  </div>
1140
1244
  ` : ""}
@@ -1295,6 +1399,20 @@ var init_CalendarElement = __esm({
1295
1399
  const slotStart = actionEl.dataset.startTime;
1296
1400
  const slotEnd = actionEl.dataset.endTime;
1297
1401
  const slotDate = new Date(actionEl.dataset.date);
1402
+ const slotBooked = actionEl.dataset.booked === "true";
1403
+ this.dispatchEvent(
1404
+ new CustomEvent("cal-slot-select", {
1405
+ bubbles: true,
1406
+ composed: true,
1407
+ detail: {
1408
+ date: slotDate,
1409
+ startTime: slotStart,
1410
+ endTime: slotEnd,
1411
+ booked: slotBooked
1412
+ }
1413
+ })
1414
+ );
1415
+ if (slotBooked || !this.hasAttribute("selectable")) break;
1298
1416
  if (this._timeRangeComplete) {
1299
1417
  this._timeRangeDate = slotDate;
1300
1418
  this._timeRangeStart = slotStart;
@@ -1435,6 +1553,7 @@ var init_CalendarElement = __esm({
1435
1553
  "week-starts-on",
1436
1554
  "heading",
1437
1555
  "months",
1556
+ "slot-duration",
1438
1557
  "title",
1439
1558
  "use-short-month-names",
1440
1559
  "availability-mode",
@@ -1486,6 +1605,7 @@ var init_CalendarElement = __esm({
1486
1605
  badgeTentativeBg: "--calendar-badge-tentative-bg",
1487
1606
  badgeTentativeText: "--calendar-badge-tentative-text"
1488
1607
  };
1608
+ _CalendarElement.warnedMissingEnd = /* @__PURE__ */ new Set();
1489
1609
  _CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
1490
1610
  _CalendarElement.titleDeprecationWarned = false;
1491
1611
  CalendarElement = _CalendarElement;
@@ -1499,12 +1619,18 @@ __export(web_components_exports, {
1499
1619
  CalendarEngine: () => CalendarEngine,
1500
1620
  DAYS: () => DAYS,
1501
1621
  DEFAULT_CATEGORY_COLORS: () => DEFAULT_CATEGORY_COLORS,
1622
+ DEFAULT_SLOT_DURATION: () => DEFAULT_SLOT_DURATION,
1623
+ MINUTES_PER_DAY: () => MINUTES_PER_DAY,
1502
1624
  MONTHS: () => MONTHS,
1503
1625
  MONTHS_FULL: () => MONTHS_FULL,
1626
+ bookedSlots: () => bookedSlots,
1504
1627
  defineCalendarElement: () => defineCalendarElement,
1505
1628
  escapeHtml: () => escapeHtml,
1629
+ eventCoversDate: () => eventCoversDate,
1630
+ eventInterval: () => eventInterval,
1506
1631
  formatAttendees: () => formatAttendees,
1507
1632
  formatDateForDisplay: () => formatDateForDisplay,
1633
+ formatMinutes: () => formatMinutes,
1508
1634
  formatTimeRange: () => formatTimeRange,
1509
1635
  generateCalendarDates: () => generateCalendarDates,
1510
1636
  generateYears: () => generateYears,
@@ -1518,7 +1644,9 @@ __export(web_components_exports, {
1518
1644
  isToday: () => isToday,
1519
1645
  isValidHexColor: () => isValidHexColor,
1520
1646
  mergeCategoryColors: () => mergeCategoryColors,
1647
+ mergeIntervals: () => mergeIntervals,
1521
1648
  normalizeDate: () => normalizeDate,
1649
+ parseTimeToMinutes: () => parseTimeToMinutes,
1522
1650
  safeColor: () => safeColor,
1523
1651
  safeUrl: () => safeUrl,
1524
1652
  slugifyToken: () => slugifyToken,
@@ -1540,12 +1668,18 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1540
1668
  CalendarEngine,
1541
1669
  DAYS,
1542
1670
  DEFAULT_CATEGORY_COLORS,
1671
+ DEFAULT_SLOT_DURATION,
1672
+ MINUTES_PER_DAY,
1543
1673
  MONTHS,
1544
1674
  MONTHS_FULL,
1675
+ bookedSlots,
1545
1676
  defineCalendarElement,
1546
1677
  escapeHtml,
1678
+ eventCoversDate,
1679
+ eventInterval,
1547
1680
  formatAttendees,
1548
1681
  formatDateForDisplay,
1682
+ formatMinutes,
1549
1683
  formatTimeRange,
1550
1684
  generateCalendarDates,
1551
1685
  generateYears,
@@ -1559,7 +1693,9 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1559
1693
  isToday,
1560
1694
  isValidHexColor,
1561
1695
  mergeCategoryColors,
1696
+ mergeIntervals,
1562
1697
  normalizeDate,
1698
+ parseTimeToMinutes,
1563
1699
  safeColor,
1564
1700
  safeUrl,
1565
1701
  slugifyToken,