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.mjs CHANGED
@@ -31,12 +31,28 @@ 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 eventCoversDate(event, date) {
35
+ const start = normalizeDate(new Date(event.date)).getTime();
36
+ const target = normalizeDate(date).getTime();
37
+ if (Number.isNaN(start)) return false;
38
+ if (event.endDate === void 0 || event.endDate === null) {
39
+ return start === target;
40
+ }
41
+ const end = normalizeDate(new Date(event.endDate)).getTime();
42
+ if (Number.isNaN(end)) {
43
+ throw new Error(
44
+ `<kal-calendar> event ${event.id} has an unreadable endDate: ${String(event.endDate)}.`
45
+ );
46
+ }
47
+ if (end < start) {
48
+ throw new Error(
49
+ `<kal-calendar> event ${event.id} has an endDate before its date: ${String(event.endDate)} < ${String(event.date)}.`
50
+ );
51
+ }
52
+ return target >= start && target <= end;
53
+ }
34
54
  function getEventsForDate(events, date) {
35
- const normalizedTargetDate = normalizeDate(date);
36
- return events.filter((event) => {
37
- const eventDate = normalizeDate(new Date(event.date));
38
- return eventDate.getTime() === normalizedTargetDate.getTime();
39
- });
55
+ return events.filter((event) => eventCoversDate(event, date));
40
56
  }
41
57
  function hasEvents(events, date) {
42
58
  return getEventsForDate(events, date).length > 0;
@@ -180,7 +196,67 @@ function safeColor(value) {
180
196
  const trimmed = String(value).trim();
181
197
  return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
182
198
  }
183
- var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES;
199
+ function parseTimeToMinutes(time) {
200
+ if (typeof time !== "string") return null;
201
+ const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
202
+ if (!match) return null;
203
+ const hours = Number(match[1]);
204
+ const mins = Number(match[2]);
205
+ if (hours > 24 || mins > 59 || hours === 24 && mins > 0) return null;
206
+ return hours * 60 + mins;
207
+ }
208
+ function formatMinutes(total) {
209
+ const hours = Math.floor(total / 60);
210
+ const mins = total % 60;
211
+ return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
212
+ }
213
+ function eventInterval(event, slotDuration, onDay) {
214
+ const day = normalizeDate(onDay).getTime();
215
+ if (Number.isNaN(day)) return null;
216
+ const base = day / 6e4;
217
+ const wholeDay = [base, base + MINUTES_PER_DAY];
218
+ if (event.allDay || !event.startTime) return wholeDay;
219
+ const from = parseTimeToMinutes(event.startTime);
220
+ if (from === null) return wholeDay;
221
+ if (event.endTime === void 0 || event.endTime === null) {
222
+ return [base + from, base + from + slotDuration];
223
+ }
224
+ const to = parseTimeToMinutes(event.endTime);
225
+ if (to === null) return wholeDay;
226
+ return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];
227
+ }
228
+ function mergeIntervals(intervals) {
229
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
230
+ return sorted.reduce((merged, [start, end]) => {
231
+ const last = merged[merged.length - 1];
232
+ if (last && start <= last[1]) last[1] = Math.max(last[1], end);
233
+ else merged.push([start, end]);
234
+ return merged;
235
+ }, []);
236
+ }
237
+ function bookedSlots(events, date, slotDuration = DEFAULT_SLOT_DURATION) {
238
+ const base = normalizeDate(date).getTime() / 6e4;
239
+ const previousDay = new Date(date);
240
+ previousDay.setDate(previousDay.getDate() - 1);
241
+ const intervals = [];
242
+ for (const event of new Set(events)) {
243
+ for (const day of [previousDay, date]) {
244
+ if (!eventCoversDate(event, day)) continue;
245
+ const interval = eventInterval(event, slotDuration, day);
246
+ if (interval) intervals.push(interval);
247
+ }
248
+ }
249
+ const merged = mergeIntervals(intervals);
250
+ return Array.from(
251
+ { length: Math.floor(MINUTES_PER_DAY / slotDuration) },
252
+ (_, index) => {
253
+ const start = base + index * slotDuration;
254
+ const end = start + slotDuration;
255
+ return merged.some(([from, to]) => start < to && end > from);
256
+ }
257
+ );
258
+ }
259
+ var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES, MINUTES_PER_DAY, DEFAULT_SLOT_DURATION;
184
260
  var init_utils = __esm({
185
261
  "src/core/utils.ts"() {
186
262
  "use strict";
@@ -236,6 +312,8 @@ var init_utils = __esm({
236
312
  '"': "&quot;",
237
313
  "'": "&#39;"
238
314
  };
315
+ MINUTES_PER_DAY = 1440;
316
+ DEFAULT_SLOT_DURATION = 60;
239
317
  }
240
318
  });
241
319
 
@@ -649,6 +727,29 @@ var init_CalendarElement = __esm({
649
727
  }
650
728
  return title;
651
729
  }
730
+ get slotDuration() {
731
+ const raw = this.getAttribute("slot-duration");
732
+ if (raw === null) return DEFAULT_SLOT_DURATION;
733
+ const minutes = Number(raw);
734
+ if (Number.isInteger(minutes) && minutes > 0 && MINUTES_PER_DAY % minutes === 0) {
735
+ return minutes;
736
+ }
737
+ console.warn(
738
+ `<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}.`
739
+ );
740
+ return DEFAULT_SLOT_DURATION;
741
+ }
742
+ warnMissingEndTime(events) {
743
+ for (const event of events) {
744
+ if (!event.startTime || event.endTime) continue;
745
+ const key = String(event.id);
746
+ if (_CalendarElement.warnedMissingEnd.has(key)) continue;
747
+ _CalendarElement.warnedMissingEnd.add(key);
748
+ console.warn(
749
+ `<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.`
750
+ );
751
+ }
752
+ }
652
753
  get monthCount() {
653
754
  const requested = Number(this.getAttribute("months") ?? 1);
654
755
  if (!Number.isInteger(requested) || requested < 1) return 1;
@@ -910,42 +1011,39 @@ var init_CalendarElement = __esm({
910
1011
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
911
1012
  const renderEvent = this._renderEvent || defaultRenderEvent;
912
1013
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
913
- const renderTimeGrid = (events, date) => {
914
- const startHour = (time) => {
915
- if (typeof time !== "string") return null;
916
- const hour = Number(time.split(":")[0]);
917
- return Number.isInteger(hour) && hour >= 0 && hour <= 24 ? hour : null;
918
- };
919
- const isHourBooked = (hour) => events.some((event) => {
920
- if (!event.startTime) return true;
921
- const from = startHour(event.startTime);
922
- const to = event.endTime ? startHour(event.endTime) : 24;
923
- if (from === null || to === null) return true;
924
- return hour >= from && hour < to;
925
- });
926
- const slots = Array.from({ length: 24 }, (_, hour) => {
927
- const startTime = `${String(hour).padStart(2, "0")}:00`;
928
- const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
929
- const booked = isHourBooked(hour);
930
- const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
1014
+ const renderTimeGrid = (date) => {
1015
+ const slotDuration = this.slotDuration;
1016
+ const previousDay = new Date(date);
1017
+ previousDay.setDate(previousDay.getDate() - 1);
1018
+ const events = [
1019
+ ...this.engine.getEventsForDate(previousDay),
1020
+ ...this.engine.getEventsForDate(date)
1021
+ ];
1022
+ this.warnMissingEndTime(events);
1023
+ const booked = bookedSlots(events, date, slotDuration);
1024
+ 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;
931
1028
  const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
932
1029
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
933
1030
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
934
1031
  const slotClasses = [
935
1032
  "time-grid-slot",
936
- booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
1033
+ isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
937
1034
  isRangeStart ? "time-grid-slot-range-start" : "",
938
1035
  isRangeEnd ? "time-grid-slot-range-end" : "",
939
1036
  isInRange ? "time-grid-slot-in-range" : ""
940
1037
  ].filter(Boolean).join(" ");
941
- const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
1038
+ const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
942
1039
  return `
943
1040
  <div class="${slotClasses}" ${slotAttrs}>
944
1041
  <span class="time-grid-label">${startTime}</span>
945
- <span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
1042
+ <span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
946
1043
  </div>`;
947
1044
  });
948
- return `<div class="time-grid">${slots.join("")}</div>`;
1045
+ const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
1046
+ return `<div class="${gridClasses}">${slots.join("")}</div>`;
949
1047
  };
950
1048
  const multiMonth = viewModel.panes.length > 1;
951
1049
  const renderPane = (pane) => `
@@ -970,6 +1068,12 @@ var init_CalendarElement = __esm({
970
1068
  ${week.map((calendarDate, dayIndex) => {
971
1069
  const classes = getCellClasses(calendarDate);
972
1070
  const cellAttrs = [];
1071
+ if (viewModel.selectedDate && isSameDay2(
1072
+ calendarDate.date,
1073
+ viewModel.selectedDate
1074
+ )) {
1075
+ classes.push("calendar-cell-selected");
1076
+ }
973
1077
  if (availabilityMode && calendarDate.isCurrentMonth) {
974
1078
  const bucket = this.resolveBucket(
975
1079
  calendarDate.date
@@ -1122,7 +1226,7 @@ var init_CalendarElement = __esm({
1122
1226
  ` : ""}
1123
1227
 
1124
1228
  <div class="events-container">
1125
- ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
1229
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
1126
1230
  </div>
1127
1231
  </div>
1128
1232
  ` : ""}
@@ -1283,6 +1387,20 @@ var init_CalendarElement = __esm({
1283
1387
  const slotStart = actionEl.dataset.startTime;
1284
1388
  const slotEnd = actionEl.dataset.endTime;
1285
1389
  const slotDate = new Date(actionEl.dataset.date);
1390
+ const slotBooked = actionEl.dataset.booked === "true";
1391
+ this.dispatchEvent(
1392
+ new CustomEvent("cal-slot-select", {
1393
+ bubbles: true,
1394
+ composed: true,
1395
+ detail: {
1396
+ date: slotDate,
1397
+ startTime: slotStart,
1398
+ endTime: slotEnd,
1399
+ booked: slotBooked
1400
+ }
1401
+ })
1402
+ );
1403
+ if (slotBooked || !this.hasAttribute("selectable")) break;
1286
1404
  if (this._timeRangeComplete) {
1287
1405
  this._timeRangeDate = slotDate;
1288
1406
  this._timeRangeStart = slotStart;
@@ -1423,6 +1541,7 @@ var init_CalendarElement = __esm({
1423
1541
  "week-starts-on",
1424
1542
  "heading",
1425
1543
  "months",
1544
+ "slot-duration",
1426
1545
  "title",
1427
1546
  "use-short-month-names",
1428
1547
  "availability-mode",
@@ -1474,6 +1593,7 @@ var init_CalendarElement = __esm({
1474
1593
  badgeTentativeBg: "--calendar-badge-tentative-bg",
1475
1594
  badgeTentativeText: "--calendar-badge-tentative-text"
1476
1595
  };
1596
+ _CalendarElement.warnedMissingEnd = /* @__PURE__ */ new Set();
1477
1597
  _CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
1478
1598
  _CalendarElement.titleDeprecationWarned = false;
1479
1599
  CalendarElement = _CalendarElement;
@@ -1495,12 +1615,18 @@ export {
1495
1615
  CalendarEngine,
1496
1616
  DAYS,
1497
1617
  DEFAULT_CATEGORY_COLORS,
1618
+ DEFAULT_SLOT_DURATION,
1619
+ MINUTES_PER_DAY,
1498
1620
  MONTHS,
1499
1621
  MONTHS_FULL,
1622
+ bookedSlots,
1500
1623
  defineCalendarElement,
1501
1624
  escapeHtml,
1625
+ eventCoversDate,
1626
+ eventInterval,
1502
1627
  formatAttendees,
1503
1628
  formatDateForDisplay,
1629
+ formatMinutes,
1504
1630
  formatTimeRange,
1505
1631
  generateCalendarDates,
1506
1632
  generateYears,
@@ -1514,7 +1640,9 @@ export {
1514
1640
  isToday,
1515
1641
  isValidHexColor,
1516
1642
  mergeCategoryColors,
1643
+ mergeIntervals,
1517
1644
  normalizeDate,
1645
+ parseTimeToMinutes,
1518
1646
  safeColor,
1519
1647
  safeUrl,
1520
1648
  slugifyToken,