kalendly 0.2.2 → 0.3.0

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
@@ -95,26 +95,16 @@ function generateCalendarDates(year, month, events = [], weekStartsOn = 0) {
95
95
  }
96
96
  return dates;
97
97
  }
98
- function getPopupPositionClass(selectedDayIndex) {
99
- if (selectedDayIndex === null) return "popup-center-bottom";
100
- if (selectedDayIndex < 3) {
101
- return "popup-right";
102
- } else if (selectedDayIndex > 4) {
103
- return "popup-left";
104
- } else {
105
- return "popup-center-bottom";
106
- }
107
- }
108
98
  function getCellClasses(calendarDate) {
109
99
  const classes = [];
110
100
  if (!calendarDate.isCurrentMonth) {
111
- classes.push("other-month");
101
+ classes.push("calendar-cell-other-month");
112
102
  }
113
103
  if (calendarDate.isToday) {
114
- classes.push("schedule--current--exam");
104
+ classes.push("calendar-cell-today");
115
105
  }
116
106
  if (calendarDate.hasEvents) {
117
- classes.push("has--event");
107
+ classes.push("calendar-cell-has-event");
118
108
  }
119
109
  return classes;
120
110
  }
@@ -173,7 +163,24 @@ function getCategoryColor(category, customColors) {
173
163
  const color = colorMap[category] || colorMap.other || "#fc8917";
174
164
  return isValidHexColor(color) ? color : "#fc8917";
175
165
  }
176
- var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS;
166
+ function escapeHtml(value) {
167
+ if (value === null || value === void 0) return "";
168
+ return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
169
+ }
170
+ function slugifyToken(value) {
171
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
172
+ }
173
+ function safeUrl(value) {
174
+ const normalized = String(value).split("").filter((char) => char.charCodeAt(0) > 32).join("");
175
+ if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;
176
+ const schemeless = !/^[^/?#]*:/.test(normalized);
177
+ return schemeless ? normalized : "#";
178
+ }
179
+ function safeColor(value) {
180
+ const trimmed = String(value).trim();
181
+ return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
182
+ }
183
+ var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES;
177
184
  var init_utils = __esm({
178
185
  "src/core/utils.ts"() {
179
186
  "use strict";
@@ -222,6 +229,13 @@ var init_utils = __esm({
222
229
  appointment: "#f59e0b",
223
230
  other: "#6b7280"
224
231
  };
232
+ HTML_ESCAPES = {
233
+ "&": "&amp;",
234
+ "<": "&lt;",
235
+ ">": "&gt;",
236
+ '"': "&quot;",
237
+ "'": "&#39;"
238
+ };
225
239
  }
226
240
  });
227
241
 
@@ -269,24 +283,38 @@ var init_calendar_engine = __esm({
269
283
  * Get view model with computed properties
270
284
  */
271
285
  getViewModel() {
272
- const calendarDates = generateCalendarDates(
273
- this.state.currentYear,
274
- this.state.currentMonth,
275
- this.config.events,
276
- this.config.weekStartsOn
286
+ const panes = Array.from(
287
+ { length: Math.max(1, this.config.monthCount ?? 1) },
288
+ (_, offset) => {
289
+ const anchor = new Date(
290
+ this.state.currentYear,
291
+ this.state.currentMonth + offset,
292
+ 1
293
+ );
294
+ const year = anchor.getFullYear();
295
+ const month = anchor.getMonth();
296
+ return {
297
+ year,
298
+ month,
299
+ monthAndYearText: getMonthYearText(year, month),
300
+ calendarDates: generateCalendarDates(
301
+ year,
302
+ month,
303
+ this.config.events,
304
+ this.config.weekStartsOn
305
+ )
306
+ };
307
+ }
277
308
  );
278
309
  return {
279
310
  ...this.state,
280
311
  months: MONTHS,
281
312
  days: DAYS,
282
313
  years: generateYears(this.config.minYear, this.config.maxYear),
283
- monthAndYearText: getMonthYearText(
284
- this.state.currentYear,
285
- this.state.currentMonth
286
- ),
314
+ monthAndYearText: panes[0].monthAndYearText,
287
315
  scheduleDay: this.state.selectedDate ? formatDateForDisplay(this.state.selectedDate) : "",
288
- calendarDates,
289
- popupPositionClass: getPopupPositionClass(this.state.selectedDayIndex)
316
+ panes,
317
+ calendarDates: panes[0].calendarDates
290
318
  };
291
319
  }
292
320
  /**
@@ -482,13 +510,13 @@ function defineCalendarElement(tagName = "kal-calendar") {
482
510
  customElements.define(tagName, CalendarElement);
483
511
  }
484
512
  }
485
- var isSameDay2, CalendarElement;
513
+ var isSameDay2, _CalendarElement, CalendarElement;
486
514
  var init_CalendarElement = __esm({
487
515
  "src/web-components/CalendarElement.ts"() {
488
516
  "use strict";
489
517
  init_core();
490
518
  isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
491
- CalendarElement = class extends HTMLElement {
519
+ _CalendarElement = class _CalendarElement extends HTMLElement {
492
520
  constructor() {
493
521
  super(...arguments);
494
522
  this.engine = null;
@@ -509,6 +537,10 @@ var init_CalendarElement = __esm({
509
537
  this._categoryColors = null;
510
538
  this._renderEvent = null;
511
539
  this._renderNoEvents = null;
540
+ this._availabilityColors = null;
541
+ this._selectableStatuses = null;
542
+ this._initError = null;
543
+ this.pendingUpdate = null;
512
544
  // Selection state (availability mode — range)
513
545
  this._rangeStart = null;
514
546
  this._rangeEnd = null;
@@ -521,6 +553,7 @@ var init_CalendarElement = __esm({
521
553
  return this._events;
522
554
  }
523
555
  set events(val) {
556
+ this._initError = null;
524
557
  this._events = val;
525
558
  if (this.engine) {
526
559
  this.engine.updateEvents(val);
@@ -536,13 +569,28 @@ var init_CalendarElement = __esm({
536
569
  this.engine.updateCategoryColors(val);
537
570
  }
538
571
  }
572
+ get availabilityColors() {
573
+ return this._availabilityColors ?? {};
574
+ }
575
+ set availabilityColors(val) {
576
+ this._initError = null;
577
+ this._availabilityColors = val;
578
+ if (this.engine) this.scheduleRender();
579
+ }
580
+ get selectableStatuses() {
581
+ return this._selectableStatuses ?? [];
582
+ }
583
+ set selectableStatuses(val) {
584
+ this._selectableStatuses = val;
585
+ if (this.engine) this.scheduleRender();
586
+ }
539
587
  set renderEvent(val) {
540
588
  this._renderEvent = val;
541
- if (this.engine) this.render();
589
+ if (this.engine) this.scheduleRender();
542
590
  }
543
591
  set renderNoEvents(val) {
544
592
  this._renderNoEvents = val;
545
- if (this.engine) this.render();
593
+ if (this.engine) this.scheduleRender();
546
594
  }
547
595
  get loading() {
548
596
  return this.hasAttribute("loading");
@@ -551,10 +599,22 @@ var init_CalendarElement = __esm({
551
599
  if (val) this.setAttribute("loading", "");
552
600
  else this.removeAttribute("loading");
553
601
  }
602
+ // Custom element reactions report rather than propagate, so a failure here
603
+ // is kept and resurfaced at the next call the integrator makes
604
+ reaction(run) {
605
+ try {
606
+ run();
607
+ } catch (error) {
608
+ this._initError = error;
609
+ throw error;
610
+ }
611
+ }
554
612
  connectedCallback() {
555
613
  this.classList.add("kalendly-calendar");
556
- this.initEngine();
557
- this.render();
614
+ this.reaction(() => {
615
+ this.initEngine();
616
+ this.render();
617
+ });
558
618
  }
559
619
  disconnectedCallback() {
560
620
  this.cleanup();
@@ -564,7 +624,7 @@ var init_CalendarElement = __esm({
564
624
  attributeChangedCallback(name, oldVal, newVal) {
565
625
  if (oldVal === newVal) return;
566
626
  if (name === "loading") {
567
- this.render();
627
+ this.scheduleRender();
568
628
  return;
569
629
  }
570
630
  if (name === "selectable" && newVal === null) {
@@ -577,6 +637,23 @@ var init_CalendarElement = __esm({
577
637
  }
578
638
  this.reinit();
579
639
  }
640
+ get headingText() {
641
+ const heading = this.getAttribute("heading");
642
+ if (heading !== null) return heading;
643
+ const title = this.getAttribute("title");
644
+ if (title !== null && !_CalendarElement.titleDeprecationWarned) {
645
+ _CalendarElement.titleDeprecationWarned = true;
646
+ console.warn(
647
+ `<kal-calendar> the title attribute is deprecated and will be removed in a future release \u2014 use heading instead. title is a global HTML attribute, so the browser also renders it as a tooltip over the whole calendar.`
648
+ );
649
+ }
650
+ return title;
651
+ }
652
+ get monthCount() {
653
+ const requested = Number(this.getAttribute("months") ?? 1);
654
+ if (!Number.isInteger(requested) || requested < 1) return 1;
655
+ return Math.min(requested, 2);
656
+ }
580
657
  get minYear() {
581
658
  const val = this.getAttribute("min-year");
582
659
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() - 30;
@@ -596,19 +673,20 @@ var init_CalendarElement = __esm({
596
673
  minYear: this.minYear,
597
674
  maxYear: this.maxYear,
598
675
  weekStartsOn,
599
- categoryColors: this._categoryColors ?? void 0
676
+ categoryColors: this._categoryColors ?? void 0,
677
+ monthCount: this.monthCount
600
678
  });
601
679
  this.actions = this.engine.getActions();
602
680
  this.applyTheme();
603
681
  this.unsubscribe = this.engine.subscribe(() => {
604
- this.render();
682
+ this.scheduleRender();
605
683
  });
606
684
  }
607
685
  reinit() {
608
686
  if (!this.engine) return;
609
687
  this.cleanup();
610
688
  this.initEngine();
611
- this.render();
689
+ this.scheduleRender();
612
690
  }
613
691
  cleanup() {
614
692
  if (this.unsubscribe) {
@@ -645,42 +723,84 @@ var init_CalendarElement = __esm({
645
723
  applyTheme() {
646
724
  if (!this._theme) return;
647
725
  const root = document.documentElement;
648
- const t = this._theme;
649
- if (t.primary)
650
- root.style.setProperty("--calendar-primary-color", t.primary);
651
- if (t.secondary)
652
- root.style.setProperty("--calendar-secondary-color", t.secondary);
653
- if (t.tertiary)
654
- root.style.setProperty("--calendar-tertiary-color", t.tertiary);
655
- if (t.textColor)
656
- root.style.setProperty("--calendar-text-color", t.textColor);
657
- if (t.textLight)
658
- root.style.setProperty("--calendar-text-light", t.textLight);
659
- if (t.background)
660
- root.style.setProperty("--calendar-background", t.background);
661
- if (t.cellHover)
662
- root.style.setProperty("--calendar-cell-hover", t.cellHover);
663
- if (t.borderColor)
664
- root.style.setProperty("--calendar-border-color", t.borderColor);
665
- if (t.todayOutline)
666
- root.style.setProperty("--calendar-today-outline", t.todayOutline);
667
- if (t.selectedBg)
668
- root.style.setProperty("--calendar-selected-bg", t.selectedBg);
669
- if (t.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
670
- if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
671
- if (t.eventIndicator)
672
- root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
726
+ for (const key of Object.keys(_CalendarElement.themeMap)) {
727
+ const value = this._theme[key];
728
+ if (value) root.style.setProperty(_CalendarElement.themeMap[key], value);
729
+ }
730
+ }
731
+ get knownBuckets() {
732
+ return [
733
+ ..._CalendarElement.BUILT_IN_BUCKETS,
734
+ ...Object.keys(this._availabilityColors ?? {})
735
+ ];
736
+ }
737
+ get updateComplete() {
738
+ return this.pendingUpdate ?? Promise.resolve();
739
+ }
740
+ scheduleRender() {
741
+ if (this.pendingUpdate) return;
742
+ this.pendingUpdate = Promise.resolve().then(() => {
743
+ this.pendingUpdate = null;
744
+ try {
745
+ this.render();
746
+ } catch (error) {
747
+ this._initError = error;
748
+ throw error;
749
+ }
750
+ });
751
+ }
752
+ rethrowInitError() {
753
+ if (this._initError) throw this._initError;
754
+ }
755
+ assertStatusesDeclared() {
756
+ if (!this.getAttribute("availability-mode")) return;
757
+ const missing = this._events.filter(
758
+ (event) => typeof event.availabilityStatus !== "string" || event.availabilityStatus === ""
759
+ ).map((event) => String(event.id));
760
+ if (missing.length) {
761
+ throw new Error(
762
+ `<kal-calendar> availability-mode requires availabilityStatus on every event. Missing on: ${missing.join(", ")}.`
763
+ );
764
+ }
765
+ }
766
+ assertStatusesKnown() {
767
+ const known = new Set(this.knownBuckets);
768
+ const unknown = this._events.filter((event) => !known.has(event.availabilityStatus)).map((event) => `${event.id} (${event.availabilityStatus})`);
769
+ if (unknown.length) {
770
+ throw new Error(
771
+ `<kal-calendar> availabilityStatus must name a built-in bucket (${_CalendarElement.BUILT_IN_BUCKETS.join(", ")}) or a key of availabilityColors. Unrecognised on: ${unknown.join(", ")}.`
772
+ );
773
+ }
774
+ }
775
+ resolveBucket(date) {
776
+ if (!this.engine) return "open";
777
+ const declared = new Set(
778
+ this.engine.getEventsForDate(date).map((event) => event.availabilityStatus)
779
+ );
780
+ if (declared.size === 0) return "open";
781
+ return this.knownBuckets.find((bucket) => declared.has(bucket));
782
+ }
783
+ isDateSelectable(date) {
784
+ if (!this.engine) return false;
785
+ if (this._selectableStatuses) {
786
+ return this._selectableStatuses.includes(this.resolveBucket(date));
787
+ }
788
+ return this.engine.getEventsForDate(date).length === 0;
673
789
  }
674
790
  render() {
675
791
  if (!this.engine) return;
676
792
  const viewModel = this.engine.getViewModel();
677
793
  const useShortMonths = this.hasAttribute("use-short-month-names");
678
- const title = this.getAttribute("title");
794
+ const title = this.headingText;
679
795
  const minYear = this.minYear;
680
796
  const maxYear = this.maxYear;
681
797
  const availabilityMode = this.getAttribute("availability-mode");
682
798
  const selectable = this.hasAttribute("selectable");
683
799
  const isLoading = this.loading;
800
+ if (availabilityMode) {
801
+ this.assertStatusesDeclared();
802
+ this.assertStatusesKnown();
803
+ }
684
804
  const today = /* @__PURE__ */ new Date();
685
805
  const todayMonth = today.getMonth();
686
806
  const todayYear = today.getFullYear();
@@ -689,7 +809,7 @@ var init_CalendarElement = __esm({
689
809
  const defaultRenderEvent = (event) => {
690
810
  const timeRange = formatTimeRange(event);
691
811
  const attendeesList = formatAttendees(event.attendees);
692
- let borderColor = event.color || "#3b82f6";
812
+ let borderColor = safeColor(event.color || "#3b82f6");
693
813
  if (event.category) {
694
814
  borderColor = this.engine.getCategoryColor(event.category);
695
815
  }
@@ -724,56 +844,56 @@ var init_CalendarElement = __esm({
724
844
  return `
725
845
  <div class="event-card" style="border-left-color: ${borderColor}">
726
846
  <div class="event-header">
727
- <div class="event-title">${event.name}</div>
847
+ <div class="event-title">${escapeHtml(event.name)}</div>
728
848
  <div class="event-badges">
729
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
730
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
731
- ${event.status && event.status !== "scheduled" ? `<span class="badge status-${event.status}">${getStatusLabel(event.status)}</span>` : ""}
849
+ ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
850
+ ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
851
+ ${event.status && event.status !== "scheduled" ? `<span class="badge status status-${slugifyToken(event.status)}">${escapeHtml(getStatusLabel(event.status))}</span>` : ""}
732
852
  </div>
733
853
  </div>
734
854
 
735
855
  ${timeRange ? `
736
856
  <div class="event-time">
737
857
  <span class="event-time-label">Time:</span>
738
- <span class="event-time-value">${timeRange}</span>
858
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
739
859
  </div>
740
860
  ` : ""}
741
861
 
742
862
  ${event.description ? `
743
- <div class="event-description">${event.description}</div>
863
+ <div class="event-description">${escapeHtml(event.description)}</div>
744
864
  ` : ""}
745
865
 
746
866
  ${event.location ? `
747
867
  <div class="event-time">
748
868
  <span class="event-time-label">Location:</span>
749
- <span class="event-time-value">${event.location}</span>
869
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
750
870
  </div>
751
871
  ` : ""}
752
872
 
753
873
  ${attendeesList ? `
754
874
  <div class="event-time">
755
875
  <span class="event-time-label">Attendees:</span>
756
- <span class="event-time-value">${attendeesList}</span>
876
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
757
877
  </div>
758
878
  ` : ""}
759
879
 
760
880
  ${event.organizer ? `
761
881
  <div class="event-time">
762
882
  <span class="event-time-label">Organizer:</span>
763
- <span class="event-time-value">${event.organizer}</span>
883
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
764
884
  </div>
765
885
  ` : ""}
766
886
 
767
887
  ${event.notes ? `
768
888
  <div class="event-time">
769
889
  <span class="event-time-label">Notes:</span>
770
- <span class="event-time-value">${event.notes}</span>
890
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
771
891
  </div>
772
892
  ` : ""}
773
893
 
774
894
  ${event.url ? `
775
895
  <div class="event-time">
776
- <a href="${event.url}" target="_blank" rel="noopener noreferrer" class="event-link">
896
+ <a href="${escapeHtml(safeUrl(event.url))}" target="_blank" rel="noopener noreferrer" class="event-link">
777
897
  View Details \u2192
778
898
  </a>
779
899
  </div>
@@ -781,7 +901,7 @@ var init_CalendarElement = __esm({
781
901
 
782
902
  ${event.tags && event.tags.length > 0 ? `
783
903
  <div class="event-tags">
784
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
904
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
785
905
  </div>
786
906
  ` : ""}
787
907
  </div>
@@ -791,11 +911,17 @@ var init_CalendarElement = __esm({
791
911
  const renderEvent = this._renderEvent || defaultRenderEvent;
792
912
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
793
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
+ };
794
919
  const isHourBooked = (hour) => events.some((event) => {
795
- if (!event.startTime || !event.endTime) return true;
796
- const [startH] = event.startTime.split(":").map(Number);
797
- const [endH] = event.endTime.split(":").map(Number);
798
- return hour >= startH && hour < endH;
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;
799
925
  });
800
926
  const slots = Array.from({ length: 24 }, (_, hour) => {
801
927
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -806,81 +932,156 @@ var init_CalendarElement = __esm({
806
932
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
807
933
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
808
934
  const slotClasses = [
809
- "time-grid__slot",
810
- booked ? "time-grid__slot--booked" : "time-grid__slot--free",
811
- isRangeStart ? "time-grid__slot--range-start" : "",
812
- isRangeEnd ? "time-grid__slot--range-end" : "",
813
- isInRange ? "time-grid__slot--in-range" : ""
935
+ "time-grid-slot",
936
+ booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
937
+ isRangeStart ? "time-grid-slot-range-start" : "",
938
+ isRangeEnd ? "time-grid-slot-range-end" : "",
939
+ isInRange ? "time-grid-slot-in-range" : ""
814
940
  ].filter(Boolean).join(" ");
815
941
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
816
942
  return `
817
943
  <div class="${slotClasses}" ${slotAttrs}>
818
- <span class="time-grid__label">${startTime}</span>
819
- <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
944
+ <span class="time-grid-label">${startTime}</span>
945
+ <span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
820
946
  </div>`;
821
947
  });
822
948
  return `<div class="time-grid">${slots.join("")}</div>`;
823
949
  };
950
+ const multiMonth = viewModel.panes.length > 1;
951
+ const renderPane = (pane) => `
952
+ <div class="calendar-pane">
953
+ ${multiMonth ? `<div class="calendar-pane-caption">${escapeHtml(pane.monthAndYearText)}</div>` : ""}
954
+ <table class="calendar-table calendar-table-bordered">
955
+ <thead>
956
+ <tr>
957
+ ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
958
+ </tr>
959
+ </thead>
960
+ <tbody data-calendar-body>
961
+ ${isLoading ? Array.from(
962
+ { length: 6 },
963
+ () => `<tr>${Array.from(
964
+ { length: 7 },
965
+ () => `<td class="calendar-skeleton" aria-hidden="true"></td>`
966
+ ).join("")}</tr>`
967
+ ).join("") : pane.calendarDates.map(
968
+ (week) => `
969
+ <tr>
970
+ ${week.map((calendarDate, dayIndex) => {
971
+ const classes = getCellClasses(calendarDate);
972
+ const cellAttrs = [];
973
+ if (availabilityMode && calendarDate.isCurrentMonth) {
974
+ const bucket = this.resolveBucket(
975
+ calendarDate.date
976
+ );
977
+ const custom = (this._availabilityColors ?? {})[bucket];
978
+ classes.push(
979
+ `availability-${slugifyToken(bucket)}`
980
+ );
981
+ if (custom) {
982
+ classes.push("availability-status");
983
+ cellAttrs.push(
984
+ `style="--availability-color: ${escapeHtml(safeColor(custom))}"`
985
+ );
986
+ }
987
+ if (!this.isDateSelectable(calendarDate.date)) {
988
+ classes.push("availability-unselectable");
989
+ }
990
+ cellAttrs.push(
991
+ `aria-label="${escapeHtml(bucket)}"`
992
+ );
993
+ }
994
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
995
+ const d = calendarDate.date;
996
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
997
+ classes.push("availability-range-start");
998
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
999
+ classes.push("availability-range-end");
1000
+ if (this._rangeStart && this._rangeEnd) {
1001
+ if (d > this._rangeStart && d < this._rangeEnd)
1002
+ classes.push("availability-in-range");
1003
+ }
1004
+ }
1005
+ const dateString = calendarDate.date.toISOString();
1006
+ return `
1007
+ <td
1008
+ class="${classes.join(" ")}"
1009
+ data-date="${dateString}"
1010
+ data-day-index="${dayIndex}"
1011
+ data-clickable="true"
1012
+ ${cellAttrs.join(" ")}
1013
+ >
1014
+ ${calendarDate.date.getDate()}
1015
+ </td>
1016
+ `;
1017
+ }).join("")}
1018
+ </tr>
1019
+ `
1020
+ ).join("")}
1021
+ </tbody>
1022
+ </table>
1023
+ </div>
1024
+ `;
824
1025
  const html = `
825
1026
  ${title ? `
826
- <div class="page--title">
827
- <h1>${title}</h1>
1027
+ <div class="calendar-title">
1028
+ <h1>${escapeHtml(title)}</h1>
828
1029
  </div>
829
1030
  ` : ""}
830
1031
 
831
- <div class="calendar--content">
832
- <div class="calendar--card">
833
- <div class="calendar--nav-header">
834
- <button type="button" class="calendar--nav-arrow" data-action="previous" aria-label="Previous month">
1032
+ <div class="calendar-content">
1033
+ <div class="calendar-card">
1034
+ <div class="calendar-nav-header">
1035
+ <button type="button" class="calendar-nav-arrow" data-action="previous" aria-label="Previous month">
835
1036
  &#8249;
836
1037
  </button>
837
1038
 
838
- <div class="calendar--picker-container" data-picker-container>
1039
+ <div class="calendar-picker-container" data-picker-container>
839
1040
  <button
840
1041
  type="button"
841
- class="calendar--picker-btn"
1042
+ class="calendar-picker-btn"
842
1043
  data-action="toggle-picker"
843
1044
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
844
1045
  aria-haspopup="true"
845
1046
  >
846
1047
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
847
- <span class="calendar--picker-chevron">&#9662;</span>
1048
+ <span class="calendar-picker-chevron">&#9662;</span>
848
1049
  </button>
849
1050
 
850
1051
  ${this.pickerOpen ? `
851
- <div class="calendar--picker-dropdown">
852
- <div class="calendar--picker-year-row">
1052
+ <div class="calendar-picker-dropdown">
1053
+ <div class="calendar-picker-year-row">
853
1054
  <button
854
1055
  type="button"
855
- class="calendar--picker-year-arrow"
1056
+ class="calendar-picker-year-arrow"
856
1057
  data-action="year-prev"
857
1058
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
858
1059
  aria-label="Previous year"
859
1060
  >&#8249;</button>
860
1061
  <input
861
1062
  type="text"
862
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
863
- value="${this.yearInput || viewModel.currentYear}"
1063
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1064
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
864
1065
  data-year-input
865
1066
  aria-label="Year"
866
1067
  />
867
1068
  <button
868
1069
  type="button"
869
- class="calendar--picker-year-arrow"
1070
+ class="calendar-picker-year-arrow"
870
1071
  data-action="year-next"
871
1072
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
872
1073
  aria-label="Next year"
873
1074
  >&#8250;</button>
874
1075
  </div>
875
1076
 
876
- <div class="calendar--picker-months">
1077
+ <div class="calendar-picker-months">
877
1078
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
878
1079
  const isSelected = index === viewModel.currentMonth;
879
1080
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
880
1081
  return `
881
1082
  <button
882
1083
  type="button"
883
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1084
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
884
1085
  data-action="select-month"
885
1086
  data-month="${index}"
886
1087
  >${month}</button>
@@ -893,70 +1094,22 @@ var init_CalendarElement = __esm({
893
1094
 
894
1095
  <button
895
1096
  type="button"
896
- class="calendar--today-btn"
1097
+ class="calendar-today-btn"
897
1098
  data-action="today"
898
1099
  ${isCurrentMonth ? "disabled" : ""}
899
1100
  >Today</button>
900
1101
 
901
- <button type="button" class="calendar--nav-arrow" data-action="next" aria-label="Next month">
1102
+ <button type="button" class="calendar-nav-arrow" data-action="next" aria-label="Next month">
902
1103
  &#8250;
903
1104
  </button>
904
1105
  </div>
905
1106
 
906
- <table class="calendar--table calendar--table--bordered">
907
- <thead>
908
- <tr>
909
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
910
- </tr>
911
- </thead>
912
- <tbody data-calendar-body>
913
- ${isLoading ? Array.from(
914
- { length: 6 },
915
- () => `<tr>${Array.from(
916
- { length: 7 },
917
- () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
918
- ).join("")}</tr>`
919
- ).join("") : viewModel.calendarDates.map(
920
- (week) => `
921
- <tr>
922
- ${week.map((calendarDate, dayIndex) => {
923
- const classes = getCellClasses(calendarDate);
924
- if (availabilityMode && calendarDate.isCurrentMonth) {
925
- classes.push(
926
- calendarDate.hasEvents ? "availability--booked" : "availability--free"
927
- );
928
- }
929
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
930
- const d = calendarDate.date;
931
- if (this._rangeStart && isSameDay2(d, this._rangeStart))
932
- classes.push("availability--range-start");
933
- if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
934
- classes.push("availability--range-end");
935
- if (this._rangeStart && this._rangeEnd) {
936
- if (d > this._rangeStart && d < this._rangeEnd)
937
- classes.push("availability--in-range");
938
- }
939
- }
940
- const dateString = calendarDate.date.toISOString();
941
- return `
942
- <td
943
- class="${classes.join(" ")}"
944
- data-date="${dateString}"
945
- data-day-index="${dayIndex}"
946
- data-clickable="true"
947
- >
948
- ${calendarDate.date.getDate()}
949
- </td>
950
- `;
951
- }).join("")}
952
- </tr>
953
- `
954
- ).join("")}
955
- </tbody>
956
- </table>
1107
+ <div class="calendar-panes">
1108
+ ${viewModel.panes.map(renderPane).join("")}
1109
+ </div>
957
1110
 
958
1111
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
959
- <div class="date-popup ${viewModel.popupPositionClass}">
1112
+ <div class="date-popup">
960
1113
  <div class="popup-header">
961
1114
  <h2>${viewModel.scheduleDay}</h2>
962
1115
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -994,8 +1147,7 @@ var init_CalendarElement = __esm({
994
1147
  const availMode = this.getAttribute("availability-mode");
995
1148
  const isSelectable = this.hasAttribute("selectable");
996
1149
  if (availMode === "day" && isSelectable) {
997
- const isBooked = cell.classList.contains("availability--booked");
998
- if (!isBooked) {
1150
+ if (this.isDateSelectable(date)) {
999
1151
  let startDate;
1000
1152
  let endDate;
1001
1153
  if (this._rangeEnd !== null) {
@@ -1013,7 +1165,7 @@ var init_CalendarElement = __esm({
1013
1165
  cursor.setDate(cursor.getDate() + 1);
1014
1166
  let blocked = false;
1015
1167
  while (cursor < e2) {
1016
- if (this.engine.getEventsForDate(cursor).length > 0) {
1168
+ if (!this.isDateSelectable(cursor)) {
1017
1169
  blocked = true;
1018
1170
  break;
1019
1171
  }
@@ -1247,31 +1399,84 @@ var init_CalendarElement = __esm({
1247
1399
  this.theme = theme;
1248
1400
  }
1249
1401
  getCurrentDate() {
1402
+ this.rethrowInitError();
1250
1403
  return this.engine?.getViewModel().selectedDate ?? null;
1251
1404
  }
1252
1405
  goToDate(date) {
1406
+ this.rethrowInitError();
1253
1407
  const year = date.getFullYear();
1254
1408
  const month = date.getMonth();
1255
1409
  this.dispatchMonthChange(year, month);
1256
1410
  this.actions?.jump(year, month);
1257
1411
  }
1258
1412
  getEngine() {
1413
+ this.rethrowInitError();
1259
1414
  if (!this.engine)
1260
1415
  throw new Error("CalendarElement is not connected to the DOM");
1261
1416
  return this.engine;
1262
1417
  }
1263
1418
  };
1264
- CalendarElement.observedAttributes = [
1419
+ _CalendarElement.observedAttributes = [
1265
1420
  "initial-date",
1266
1421
  "min-year",
1267
1422
  "max-year",
1268
1423
  "week-starts-on",
1424
+ "heading",
1425
+ "months",
1269
1426
  "title",
1270
1427
  "use-short-month-names",
1271
1428
  "availability-mode",
1272
1429
  "selectable",
1273
1430
  "loading"
1274
1431
  ];
1432
+ _CalendarElement.themeMap = {
1433
+ primary: "--calendar-primary-color",
1434
+ secondary: "--calendar-secondary-color",
1435
+ tertiary: "--calendar-tertiary-color",
1436
+ textColor: "--calendar-text-color",
1437
+ textLight: "--calendar-text-light",
1438
+ background: "--calendar-background",
1439
+ cellHover: "--calendar-cell-hover",
1440
+ borderColor: "--calendar-border-color",
1441
+ todayOutline: "--calendar-today-outline",
1442
+ selectedBg: "--calendar-selected-bg",
1443
+ headerBg: "--calendar-header-bg",
1444
+ popupBg: "--calendar-popup-bg",
1445
+ pickerBg: "--calendar-picker-bg",
1446
+ pickerShadow: "--calendar-picker-shadow",
1447
+ eventIndicator: "--calendar-event-indicator",
1448
+ onAccent: "--calendar-on-accent",
1449
+ link: "--calendar-link",
1450
+ openBg: "--calendar-open-bg",
1451
+ openFg: "--calendar-open-fg",
1452
+ conditionalBg: "--calendar-conditional-bg",
1453
+ conditionalFg: "--calendar-conditional-fg",
1454
+ blockedBg: "--calendar-blocked-bg",
1455
+ blockedFg: "--calendar-blocked-fg",
1456
+ rangeBg: "--calendar-range-bg",
1457
+ rangeOutline: "--calendar-range-outline",
1458
+ inRangeBg: "--calendar-in-range-bg",
1459
+ inRangeOutline: "--calendar-in-range-outline",
1460
+ badgeBg: "--calendar-badge-bg",
1461
+ badgeText: "--calendar-badge-text",
1462
+ badgeSuccessBg: "--calendar-badge-success-bg",
1463
+ badgeSuccessText: "--calendar-badge-success-text",
1464
+ badgeInfoBg: "--calendar-badge-info-bg",
1465
+ badgeInfoText: "--calendar-badge-info-text",
1466
+ badgeWarningBg: "--calendar-badge-warning-bg",
1467
+ badgeWarningText: "--calendar-badge-warning-text",
1468
+ badgeDangerBg: "--calendar-badge-danger-bg",
1469
+ badgeDangerText: "--calendar-badge-danger-text",
1470
+ badgeNeutralBg: "--calendar-badge-neutral-bg",
1471
+ badgeNeutralText: "--calendar-badge-neutral-text",
1472
+ badgePositiveBg: "--calendar-badge-positive-bg",
1473
+ badgePositiveText: "--calendar-badge-positive-text",
1474
+ badgeTentativeBg: "--calendar-badge-tentative-bg",
1475
+ badgeTentativeText: "--calendar-badge-tentative-text"
1476
+ };
1477
+ _CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
1478
+ _CalendarElement.titleDeprecationWarned = false;
1479
+ CalendarElement = _CalendarElement;
1275
1480
  }
1276
1481
  });
1277
1482
 
@@ -1293,6 +1498,7 @@ export {
1293
1498
  MONTHS,
1294
1499
  MONTHS_FULL,
1295
1500
  defineCalendarElement,
1501
+ escapeHtml,
1296
1502
  formatAttendees,
1297
1503
  formatDateForDisplay,
1298
1504
  formatTimeRange,
@@ -1303,13 +1509,15 @@ export {
1303
1509
  getDefaultEventColor,
1304
1510
  getEventsForDate,
1305
1511
  getMonthYearText,
1306
- getPopupPositionClass,
1307
1512
  hasEvents,
1308
1513
  isSameDay,
1309
1514
  isToday,
1310
1515
  isValidHexColor,
1311
1516
  mergeCategoryColors,
1312
1517
  normalizeDate,
1518
+ safeColor,
1519
+ safeUrl,
1520
+ slugifyToken,
1313
1521
  sortEventsByTime
1314
1522
  };
1315
1523
  //# sourceMappingURL=index.mjs.map