kalendly 0.2.1 → 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,40 +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.eventIndicator)
670
- 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;
671
789
  }
672
790
  render() {
673
791
  if (!this.engine) return;
674
792
  const viewModel = this.engine.getViewModel();
675
793
  const useShortMonths = this.hasAttribute("use-short-month-names");
676
- const title = this.getAttribute("title");
794
+ const title = this.headingText;
677
795
  const minYear = this.minYear;
678
796
  const maxYear = this.maxYear;
679
797
  const availabilityMode = this.getAttribute("availability-mode");
680
798
  const selectable = this.hasAttribute("selectable");
681
799
  const isLoading = this.loading;
800
+ if (availabilityMode) {
801
+ this.assertStatusesDeclared();
802
+ this.assertStatusesKnown();
803
+ }
682
804
  const today = /* @__PURE__ */ new Date();
683
805
  const todayMonth = today.getMonth();
684
806
  const todayYear = today.getFullYear();
@@ -687,7 +809,7 @@ var init_CalendarElement = __esm({
687
809
  const defaultRenderEvent = (event) => {
688
810
  const timeRange = formatTimeRange(event);
689
811
  const attendeesList = formatAttendees(event.attendees);
690
- let borderColor = event.color || "#3b82f6";
812
+ let borderColor = safeColor(event.color || "#3b82f6");
691
813
  if (event.category) {
692
814
  borderColor = this.engine.getCategoryColor(event.category);
693
815
  }
@@ -722,56 +844,56 @@ var init_CalendarElement = __esm({
722
844
  return `
723
845
  <div class="event-card" style="border-left-color: ${borderColor}">
724
846
  <div class="event-header">
725
- <div class="event-title">${event.name}</div>
847
+ <div class="event-title">${escapeHtml(event.name)}</div>
726
848
  <div class="event-badges">
727
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
728
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
729
- ${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>` : ""}
730
852
  </div>
731
853
  </div>
732
854
 
733
855
  ${timeRange ? `
734
856
  <div class="event-time">
735
857
  <span class="event-time-label">Time:</span>
736
- <span class="event-time-value">${timeRange}</span>
858
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
737
859
  </div>
738
860
  ` : ""}
739
861
 
740
862
  ${event.description ? `
741
- <div class="event-description">${event.description}</div>
863
+ <div class="event-description">${escapeHtml(event.description)}</div>
742
864
  ` : ""}
743
865
 
744
866
  ${event.location ? `
745
867
  <div class="event-time">
746
868
  <span class="event-time-label">Location:</span>
747
- <span class="event-time-value">${event.location}</span>
869
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
748
870
  </div>
749
871
  ` : ""}
750
872
 
751
873
  ${attendeesList ? `
752
874
  <div class="event-time">
753
875
  <span class="event-time-label">Attendees:</span>
754
- <span class="event-time-value">${attendeesList}</span>
876
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
755
877
  </div>
756
878
  ` : ""}
757
879
 
758
880
  ${event.organizer ? `
759
881
  <div class="event-time">
760
882
  <span class="event-time-label">Organizer:</span>
761
- <span class="event-time-value">${event.organizer}</span>
883
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
762
884
  </div>
763
885
  ` : ""}
764
886
 
765
887
  ${event.notes ? `
766
888
  <div class="event-time">
767
889
  <span class="event-time-label">Notes:</span>
768
- <span class="event-time-value">${event.notes}</span>
890
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
769
891
  </div>
770
892
  ` : ""}
771
893
 
772
894
  ${event.url ? `
773
895
  <div class="event-time">
774
- <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">
775
897
  View Details \u2192
776
898
  </a>
777
899
  </div>
@@ -779,7 +901,7 @@ var init_CalendarElement = __esm({
779
901
 
780
902
  ${event.tags && event.tags.length > 0 ? `
781
903
  <div class="event-tags">
782
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
904
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
783
905
  </div>
784
906
  ` : ""}
785
907
  </div>
@@ -789,11 +911,17 @@ var init_CalendarElement = __esm({
789
911
  const renderEvent = this._renderEvent || defaultRenderEvent;
790
912
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
791
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
+ };
792
919
  const isHourBooked = (hour) => events.some((event) => {
793
- if (!event.startTime || !event.endTime) return true;
794
- const [startH] = event.startTime.split(":").map(Number);
795
- const [endH] = event.endTime.split(":").map(Number);
796
- 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;
797
925
  });
798
926
  const slots = Array.from({ length: 24 }, (_, hour) => {
799
927
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -804,81 +932,156 @@ var init_CalendarElement = __esm({
804
932
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
805
933
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
806
934
  const slotClasses = [
807
- "time-grid__slot",
808
- booked ? "time-grid__slot--booked" : "time-grid__slot--free",
809
- isRangeStart ? "time-grid__slot--range-start" : "",
810
- isRangeEnd ? "time-grid__slot--range-end" : "",
811
- 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" : ""
812
940
  ].filter(Boolean).join(" ");
813
941
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
814
942
  return `
815
943
  <div class="${slotClasses}" ${slotAttrs}>
816
- <span class="time-grid__label">${startTime}</span>
817
- <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>
818
946
  </div>`;
819
947
  });
820
948
  return `<div class="time-grid">${slots.join("")}</div>`;
821
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
+ `;
822
1025
  const html = `
823
1026
  ${title ? `
824
- <div class="page--title">
825
- <h1>${title}</h1>
1027
+ <div class="calendar-title">
1028
+ <h1>${escapeHtml(title)}</h1>
826
1029
  </div>
827
1030
  ` : ""}
828
1031
 
829
- <div class="calendar--content">
830
- <div class="calendar--card">
831
- <div class="calendar--nav-header">
832
- <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">
833
1036
  &#8249;
834
1037
  </button>
835
1038
 
836
- <div class="calendar--picker-container" data-picker-container>
1039
+ <div class="calendar-picker-container" data-picker-container>
837
1040
  <button
838
1041
  type="button"
839
- class="calendar--picker-btn"
1042
+ class="calendar-picker-btn"
840
1043
  data-action="toggle-picker"
841
1044
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
842
1045
  aria-haspopup="true"
843
1046
  >
844
1047
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
845
- <span class="calendar--picker-chevron">&#9662;</span>
1048
+ <span class="calendar-picker-chevron">&#9662;</span>
846
1049
  </button>
847
1050
 
848
1051
  ${this.pickerOpen ? `
849
- <div class="calendar--picker-dropdown">
850
- <div class="calendar--picker-year-row">
1052
+ <div class="calendar-picker-dropdown">
1053
+ <div class="calendar-picker-year-row">
851
1054
  <button
852
1055
  type="button"
853
- class="calendar--picker-year-arrow"
1056
+ class="calendar-picker-year-arrow"
854
1057
  data-action="year-prev"
855
1058
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
856
1059
  aria-label="Previous year"
857
1060
  >&#8249;</button>
858
1061
  <input
859
1062
  type="text"
860
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
861
- value="${this.yearInput || viewModel.currentYear}"
1063
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1064
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
862
1065
  data-year-input
863
1066
  aria-label="Year"
864
1067
  />
865
1068
  <button
866
1069
  type="button"
867
- class="calendar--picker-year-arrow"
1070
+ class="calendar-picker-year-arrow"
868
1071
  data-action="year-next"
869
1072
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
870
1073
  aria-label="Next year"
871
1074
  >&#8250;</button>
872
1075
  </div>
873
1076
 
874
- <div class="calendar--picker-months">
1077
+ <div class="calendar-picker-months">
875
1078
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
876
1079
  const isSelected = index === viewModel.currentMonth;
877
1080
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
878
1081
  return `
879
1082
  <button
880
1083
  type="button"
881
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1084
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
882
1085
  data-action="select-month"
883
1086
  data-month="${index}"
884
1087
  >${month}</button>
@@ -891,70 +1094,22 @@ var init_CalendarElement = __esm({
891
1094
 
892
1095
  <button
893
1096
  type="button"
894
- class="calendar--today-btn"
1097
+ class="calendar-today-btn"
895
1098
  data-action="today"
896
1099
  ${isCurrentMonth ? "disabled" : ""}
897
1100
  >Today</button>
898
1101
 
899
- <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">
900
1103
  &#8250;
901
1104
  </button>
902
1105
  </div>
903
1106
 
904
- <table class="calendar--table calendar--table--bordered">
905
- <thead>
906
- <tr>
907
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
908
- </tr>
909
- </thead>
910
- <tbody data-calendar-body>
911
- ${isLoading ? Array.from(
912
- { length: 6 },
913
- () => `<tr>${Array.from(
914
- { length: 7 },
915
- () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
916
- ).join("")}</tr>`
917
- ).join("") : viewModel.calendarDates.map(
918
- (week) => `
919
- <tr>
920
- ${week.map((calendarDate, dayIndex) => {
921
- const classes = getCellClasses(calendarDate);
922
- if (availabilityMode && calendarDate.isCurrentMonth) {
923
- classes.push(
924
- calendarDate.hasEvents ? "availability--booked" : "availability--free"
925
- );
926
- }
927
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
928
- const d = calendarDate.date;
929
- if (this._rangeStart && isSameDay2(d, this._rangeStart))
930
- classes.push("availability--range-start");
931
- if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
932
- classes.push("availability--range-end");
933
- if (this._rangeStart && this._rangeEnd) {
934
- if (d > this._rangeStart && d < this._rangeEnd)
935
- classes.push("availability--in-range");
936
- }
937
- }
938
- const dateString = calendarDate.date.toISOString();
939
- return `
940
- <td
941
- class="${classes.join(" ")}"
942
- data-date="${dateString}"
943
- data-day-index="${dayIndex}"
944
- data-clickable="true"
945
- >
946
- ${calendarDate.date.getDate()}
947
- </td>
948
- `;
949
- }).join("")}
950
- </tr>
951
- `
952
- ).join("")}
953
- </tbody>
954
- </table>
1107
+ <div class="calendar-panes">
1108
+ ${viewModel.panes.map(renderPane).join("")}
1109
+ </div>
955
1110
 
956
1111
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
957
- <div class="date-popup ${viewModel.popupPositionClass}">
1112
+ <div class="date-popup">
958
1113
  <div class="popup-header">
959
1114
  <h2>${viewModel.scheduleDay}</h2>
960
1115
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -992,8 +1147,7 @@ var init_CalendarElement = __esm({
992
1147
  const availMode = this.getAttribute("availability-mode");
993
1148
  const isSelectable = this.hasAttribute("selectable");
994
1149
  if (availMode === "day" && isSelectable) {
995
- const isBooked = cell.classList.contains("availability--booked");
996
- if (!isBooked) {
1150
+ if (this.isDateSelectable(date)) {
997
1151
  let startDate;
998
1152
  let endDate;
999
1153
  if (this._rangeEnd !== null) {
@@ -1011,7 +1165,7 @@ var init_CalendarElement = __esm({
1011
1165
  cursor.setDate(cursor.getDate() + 1);
1012
1166
  let blocked = false;
1013
1167
  while (cursor < e2) {
1014
- if (this.engine.getEventsForDate(cursor).length > 0) {
1168
+ if (!this.isDateSelectable(cursor)) {
1015
1169
  blocked = true;
1016
1170
  break;
1017
1171
  }
@@ -1245,31 +1399,84 @@ var init_CalendarElement = __esm({
1245
1399
  this.theme = theme;
1246
1400
  }
1247
1401
  getCurrentDate() {
1402
+ this.rethrowInitError();
1248
1403
  return this.engine?.getViewModel().selectedDate ?? null;
1249
1404
  }
1250
1405
  goToDate(date) {
1406
+ this.rethrowInitError();
1251
1407
  const year = date.getFullYear();
1252
1408
  const month = date.getMonth();
1253
1409
  this.dispatchMonthChange(year, month);
1254
1410
  this.actions?.jump(year, month);
1255
1411
  }
1256
1412
  getEngine() {
1413
+ this.rethrowInitError();
1257
1414
  if (!this.engine)
1258
1415
  throw new Error("CalendarElement is not connected to the DOM");
1259
1416
  return this.engine;
1260
1417
  }
1261
1418
  };
1262
- CalendarElement.observedAttributes = [
1419
+ _CalendarElement.observedAttributes = [
1263
1420
  "initial-date",
1264
1421
  "min-year",
1265
1422
  "max-year",
1266
1423
  "week-starts-on",
1424
+ "heading",
1425
+ "months",
1267
1426
  "title",
1268
1427
  "use-short-month-names",
1269
1428
  "availability-mode",
1270
1429
  "selectable",
1271
1430
  "loading"
1272
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;
1273
1480
  }
1274
1481
  });
1275
1482
 
@@ -1291,6 +1498,7 @@ export {
1291
1498
  MONTHS,
1292
1499
  MONTHS_FULL,
1293
1500
  defineCalendarElement,
1501
+ escapeHtml,
1294
1502
  formatAttendees,
1295
1503
  formatDateForDisplay,
1296
1504
  formatTimeRange,
@@ -1301,13 +1509,15 @@ export {
1301
1509
  getDefaultEventColor,
1302
1510
  getEventsForDate,
1303
1511
  getMonthYearText,
1304
- getPopupPositionClass,
1305
1512
  hasEvents,
1306
1513
  isSameDay,
1307
1514
  isToday,
1308
1515
  isValidHexColor,
1309
1516
  mergeCategoryColors,
1310
1517
  normalizeDate,
1518
+ safeColor,
1519
+ safeUrl,
1520
+ slugifyToken,
1311
1521
  sortEventsByTime
1312
1522
  };
1313
1523
  //# sourceMappingURL=index.mjs.map