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.js CHANGED
@@ -107,26 +107,16 @@ function generateCalendarDates(year, month, events = [], weekStartsOn = 0) {
107
107
  }
108
108
  return dates;
109
109
  }
110
- function getPopupPositionClass(selectedDayIndex) {
111
- if (selectedDayIndex === null) return "popup-center-bottom";
112
- if (selectedDayIndex < 3) {
113
- return "popup-right";
114
- } else if (selectedDayIndex > 4) {
115
- return "popup-left";
116
- } else {
117
- return "popup-center-bottom";
118
- }
119
- }
120
110
  function getCellClasses(calendarDate) {
121
111
  const classes = [];
122
112
  if (!calendarDate.isCurrentMonth) {
123
- classes.push("other-month");
113
+ classes.push("calendar-cell-other-month");
124
114
  }
125
115
  if (calendarDate.isToday) {
126
- classes.push("schedule--current--exam");
116
+ classes.push("calendar-cell-today");
127
117
  }
128
118
  if (calendarDate.hasEvents) {
129
- classes.push("has--event");
119
+ classes.push("calendar-cell-has-event");
130
120
  }
131
121
  return classes;
132
122
  }
@@ -185,7 +175,24 @@ function getCategoryColor(category, customColors) {
185
175
  const color = colorMap[category] || colorMap.other || "#fc8917";
186
176
  return isValidHexColor(color) ? color : "#fc8917";
187
177
  }
188
- var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS;
178
+ function escapeHtml(value) {
179
+ if (value === null || value === void 0) return "";
180
+ return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
181
+ }
182
+ function slugifyToken(value) {
183
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
184
+ }
185
+ function safeUrl(value) {
186
+ const normalized = String(value).split("").filter((char) => char.charCodeAt(0) > 32).join("");
187
+ if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;
188
+ const schemeless = !/^[^/?#]*:/.test(normalized);
189
+ return schemeless ? normalized : "#";
190
+ }
191
+ function safeColor(value) {
192
+ const trimmed = String(value).trim();
193
+ return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
194
+ }
195
+ var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES;
189
196
  var init_utils = __esm({
190
197
  "src/core/utils.ts"() {
191
198
  "use strict";
@@ -234,6 +241,13 @@ var init_utils = __esm({
234
241
  appointment: "#f59e0b",
235
242
  other: "#6b7280"
236
243
  };
244
+ HTML_ESCAPES = {
245
+ "&": "&amp;",
246
+ "<": "&lt;",
247
+ ">": "&gt;",
248
+ '"': "&quot;",
249
+ "'": "&#39;"
250
+ };
237
251
  }
238
252
  });
239
253
 
@@ -281,24 +295,38 @@ var init_calendar_engine = __esm({
281
295
  * Get view model with computed properties
282
296
  */
283
297
  getViewModel() {
284
- const calendarDates = generateCalendarDates(
285
- this.state.currentYear,
286
- this.state.currentMonth,
287
- this.config.events,
288
- this.config.weekStartsOn
298
+ const panes = Array.from(
299
+ { length: Math.max(1, this.config.monthCount ?? 1) },
300
+ (_, offset) => {
301
+ const anchor = new Date(
302
+ this.state.currentYear,
303
+ this.state.currentMonth + offset,
304
+ 1
305
+ );
306
+ const year = anchor.getFullYear();
307
+ const month = anchor.getMonth();
308
+ return {
309
+ year,
310
+ month,
311
+ monthAndYearText: getMonthYearText(year, month),
312
+ calendarDates: generateCalendarDates(
313
+ year,
314
+ month,
315
+ this.config.events,
316
+ this.config.weekStartsOn
317
+ )
318
+ };
319
+ }
289
320
  );
290
321
  return {
291
322
  ...this.state,
292
323
  months: MONTHS,
293
324
  days: DAYS,
294
325
  years: generateYears(this.config.minYear, this.config.maxYear),
295
- monthAndYearText: getMonthYearText(
296
- this.state.currentYear,
297
- this.state.currentMonth
298
- ),
326
+ monthAndYearText: panes[0].monthAndYearText,
299
327
  scheduleDay: this.state.selectedDate ? formatDateForDisplay(this.state.selectedDate) : "",
300
- calendarDates,
301
- popupPositionClass: getPopupPositionClass(this.state.selectedDayIndex)
328
+ panes,
329
+ calendarDates: panes[0].calendarDates
302
330
  };
303
331
  }
304
332
  /**
@@ -494,13 +522,13 @@ function defineCalendarElement(tagName = "kal-calendar") {
494
522
  customElements.define(tagName, CalendarElement);
495
523
  }
496
524
  }
497
- var isSameDay2, CalendarElement;
525
+ var isSameDay2, _CalendarElement, CalendarElement;
498
526
  var init_CalendarElement = __esm({
499
527
  "src/web-components/CalendarElement.ts"() {
500
528
  "use strict";
501
529
  init_core();
502
530
  isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
503
- CalendarElement = class extends HTMLElement {
531
+ _CalendarElement = class _CalendarElement extends HTMLElement {
504
532
  constructor() {
505
533
  super(...arguments);
506
534
  this.engine = null;
@@ -521,6 +549,10 @@ var init_CalendarElement = __esm({
521
549
  this._categoryColors = null;
522
550
  this._renderEvent = null;
523
551
  this._renderNoEvents = null;
552
+ this._availabilityColors = null;
553
+ this._selectableStatuses = null;
554
+ this._initError = null;
555
+ this.pendingUpdate = null;
524
556
  // Selection state (availability mode — range)
525
557
  this._rangeStart = null;
526
558
  this._rangeEnd = null;
@@ -533,6 +565,7 @@ var init_CalendarElement = __esm({
533
565
  return this._events;
534
566
  }
535
567
  set events(val) {
568
+ this._initError = null;
536
569
  this._events = val;
537
570
  if (this.engine) {
538
571
  this.engine.updateEvents(val);
@@ -548,13 +581,28 @@ var init_CalendarElement = __esm({
548
581
  this.engine.updateCategoryColors(val);
549
582
  }
550
583
  }
584
+ get availabilityColors() {
585
+ return this._availabilityColors ?? {};
586
+ }
587
+ set availabilityColors(val) {
588
+ this._initError = null;
589
+ this._availabilityColors = val;
590
+ if (this.engine) this.scheduleRender();
591
+ }
592
+ get selectableStatuses() {
593
+ return this._selectableStatuses ?? [];
594
+ }
595
+ set selectableStatuses(val) {
596
+ this._selectableStatuses = val;
597
+ if (this.engine) this.scheduleRender();
598
+ }
551
599
  set renderEvent(val) {
552
600
  this._renderEvent = val;
553
- if (this.engine) this.render();
601
+ if (this.engine) this.scheduleRender();
554
602
  }
555
603
  set renderNoEvents(val) {
556
604
  this._renderNoEvents = val;
557
- if (this.engine) this.render();
605
+ if (this.engine) this.scheduleRender();
558
606
  }
559
607
  get loading() {
560
608
  return this.hasAttribute("loading");
@@ -563,10 +611,22 @@ var init_CalendarElement = __esm({
563
611
  if (val) this.setAttribute("loading", "");
564
612
  else this.removeAttribute("loading");
565
613
  }
614
+ // Custom element reactions report rather than propagate, so a failure here
615
+ // is kept and resurfaced at the next call the integrator makes
616
+ reaction(run) {
617
+ try {
618
+ run();
619
+ } catch (error) {
620
+ this._initError = error;
621
+ throw error;
622
+ }
623
+ }
566
624
  connectedCallback() {
567
625
  this.classList.add("kalendly-calendar");
568
- this.initEngine();
569
- this.render();
626
+ this.reaction(() => {
627
+ this.initEngine();
628
+ this.render();
629
+ });
570
630
  }
571
631
  disconnectedCallback() {
572
632
  this.cleanup();
@@ -576,7 +636,7 @@ var init_CalendarElement = __esm({
576
636
  attributeChangedCallback(name, oldVal, newVal) {
577
637
  if (oldVal === newVal) return;
578
638
  if (name === "loading") {
579
- this.render();
639
+ this.scheduleRender();
580
640
  return;
581
641
  }
582
642
  if (name === "selectable" && newVal === null) {
@@ -589,6 +649,23 @@ var init_CalendarElement = __esm({
589
649
  }
590
650
  this.reinit();
591
651
  }
652
+ get headingText() {
653
+ const heading = this.getAttribute("heading");
654
+ if (heading !== null) return heading;
655
+ const title = this.getAttribute("title");
656
+ if (title !== null && !_CalendarElement.titleDeprecationWarned) {
657
+ _CalendarElement.titleDeprecationWarned = true;
658
+ console.warn(
659
+ `<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.`
660
+ );
661
+ }
662
+ return title;
663
+ }
664
+ get monthCount() {
665
+ const requested = Number(this.getAttribute("months") ?? 1);
666
+ if (!Number.isInteger(requested) || requested < 1) return 1;
667
+ return Math.min(requested, 2);
668
+ }
592
669
  get minYear() {
593
670
  const val = this.getAttribute("min-year");
594
671
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() - 30;
@@ -608,19 +685,20 @@ var init_CalendarElement = __esm({
608
685
  minYear: this.minYear,
609
686
  maxYear: this.maxYear,
610
687
  weekStartsOn,
611
- categoryColors: this._categoryColors ?? void 0
688
+ categoryColors: this._categoryColors ?? void 0,
689
+ monthCount: this.monthCount
612
690
  });
613
691
  this.actions = this.engine.getActions();
614
692
  this.applyTheme();
615
693
  this.unsubscribe = this.engine.subscribe(() => {
616
- this.render();
694
+ this.scheduleRender();
617
695
  });
618
696
  }
619
697
  reinit() {
620
698
  if (!this.engine) return;
621
699
  this.cleanup();
622
700
  this.initEngine();
623
- this.render();
701
+ this.scheduleRender();
624
702
  }
625
703
  cleanup() {
626
704
  if (this.unsubscribe) {
@@ -657,42 +735,84 @@ var init_CalendarElement = __esm({
657
735
  applyTheme() {
658
736
  if (!this._theme) return;
659
737
  const root = document.documentElement;
660
- const t = this._theme;
661
- if (t.primary)
662
- root.style.setProperty("--calendar-primary-color", t.primary);
663
- if (t.secondary)
664
- root.style.setProperty("--calendar-secondary-color", t.secondary);
665
- if (t.tertiary)
666
- root.style.setProperty("--calendar-tertiary-color", t.tertiary);
667
- if (t.textColor)
668
- root.style.setProperty("--calendar-text-color", t.textColor);
669
- if (t.textLight)
670
- root.style.setProperty("--calendar-text-light", t.textLight);
671
- if (t.background)
672
- root.style.setProperty("--calendar-background", t.background);
673
- if (t.cellHover)
674
- root.style.setProperty("--calendar-cell-hover", t.cellHover);
675
- if (t.borderColor)
676
- root.style.setProperty("--calendar-border-color", t.borderColor);
677
- if (t.todayOutline)
678
- root.style.setProperty("--calendar-today-outline", t.todayOutline);
679
- if (t.selectedBg)
680
- root.style.setProperty("--calendar-selected-bg", t.selectedBg);
681
- if (t.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
682
- if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
683
- if (t.eventIndicator)
684
- root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
738
+ for (const key of Object.keys(_CalendarElement.themeMap)) {
739
+ const value = this._theme[key];
740
+ if (value) root.style.setProperty(_CalendarElement.themeMap[key], value);
741
+ }
742
+ }
743
+ get knownBuckets() {
744
+ return [
745
+ ..._CalendarElement.BUILT_IN_BUCKETS,
746
+ ...Object.keys(this._availabilityColors ?? {})
747
+ ];
748
+ }
749
+ get updateComplete() {
750
+ return this.pendingUpdate ?? Promise.resolve();
751
+ }
752
+ scheduleRender() {
753
+ if (this.pendingUpdate) return;
754
+ this.pendingUpdate = Promise.resolve().then(() => {
755
+ this.pendingUpdate = null;
756
+ try {
757
+ this.render();
758
+ } catch (error) {
759
+ this._initError = error;
760
+ throw error;
761
+ }
762
+ });
763
+ }
764
+ rethrowInitError() {
765
+ if (this._initError) throw this._initError;
766
+ }
767
+ assertStatusesDeclared() {
768
+ if (!this.getAttribute("availability-mode")) return;
769
+ const missing = this._events.filter(
770
+ (event) => typeof event.availabilityStatus !== "string" || event.availabilityStatus === ""
771
+ ).map((event) => String(event.id));
772
+ if (missing.length) {
773
+ throw new Error(
774
+ `<kal-calendar> availability-mode requires availabilityStatus on every event. Missing on: ${missing.join(", ")}.`
775
+ );
776
+ }
777
+ }
778
+ assertStatusesKnown() {
779
+ const known = new Set(this.knownBuckets);
780
+ const unknown = this._events.filter((event) => !known.has(event.availabilityStatus)).map((event) => `${event.id} (${event.availabilityStatus})`);
781
+ if (unknown.length) {
782
+ throw new Error(
783
+ `<kal-calendar> availabilityStatus must name a built-in bucket (${_CalendarElement.BUILT_IN_BUCKETS.join(", ")}) or a key of availabilityColors. Unrecognised on: ${unknown.join(", ")}.`
784
+ );
785
+ }
786
+ }
787
+ resolveBucket(date) {
788
+ if (!this.engine) return "open";
789
+ const declared = new Set(
790
+ this.engine.getEventsForDate(date).map((event) => event.availabilityStatus)
791
+ );
792
+ if (declared.size === 0) return "open";
793
+ return this.knownBuckets.find((bucket) => declared.has(bucket));
794
+ }
795
+ isDateSelectable(date) {
796
+ if (!this.engine) return false;
797
+ if (this._selectableStatuses) {
798
+ return this._selectableStatuses.includes(this.resolveBucket(date));
799
+ }
800
+ return this.engine.getEventsForDate(date).length === 0;
685
801
  }
686
802
  render() {
687
803
  if (!this.engine) return;
688
804
  const viewModel = this.engine.getViewModel();
689
805
  const useShortMonths = this.hasAttribute("use-short-month-names");
690
- const title = this.getAttribute("title");
806
+ const title = this.headingText;
691
807
  const minYear = this.minYear;
692
808
  const maxYear = this.maxYear;
693
809
  const availabilityMode = this.getAttribute("availability-mode");
694
810
  const selectable = this.hasAttribute("selectable");
695
811
  const isLoading = this.loading;
812
+ if (availabilityMode) {
813
+ this.assertStatusesDeclared();
814
+ this.assertStatusesKnown();
815
+ }
696
816
  const today = /* @__PURE__ */ new Date();
697
817
  const todayMonth = today.getMonth();
698
818
  const todayYear = today.getFullYear();
@@ -701,7 +821,7 @@ var init_CalendarElement = __esm({
701
821
  const defaultRenderEvent = (event) => {
702
822
  const timeRange = formatTimeRange(event);
703
823
  const attendeesList = formatAttendees(event.attendees);
704
- let borderColor = event.color || "#3b82f6";
824
+ let borderColor = safeColor(event.color || "#3b82f6");
705
825
  if (event.category) {
706
826
  borderColor = this.engine.getCategoryColor(event.category);
707
827
  }
@@ -736,56 +856,56 @@ var init_CalendarElement = __esm({
736
856
  return `
737
857
  <div class="event-card" style="border-left-color: ${borderColor}">
738
858
  <div class="event-header">
739
- <div class="event-title">${event.name}</div>
859
+ <div class="event-title">${escapeHtml(event.name)}</div>
740
860
  <div class="event-badges">
741
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
742
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
743
- ${event.status && event.status !== "scheduled" ? `<span class="badge status-${event.status}">${getStatusLabel(event.status)}</span>` : ""}
861
+ ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
862
+ ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
863
+ ${event.status && event.status !== "scheduled" ? `<span class="badge status status-${slugifyToken(event.status)}">${escapeHtml(getStatusLabel(event.status))}</span>` : ""}
744
864
  </div>
745
865
  </div>
746
866
 
747
867
  ${timeRange ? `
748
868
  <div class="event-time">
749
869
  <span class="event-time-label">Time:</span>
750
- <span class="event-time-value">${timeRange}</span>
870
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
751
871
  </div>
752
872
  ` : ""}
753
873
 
754
874
  ${event.description ? `
755
- <div class="event-description">${event.description}</div>
875
+ <div class="event-description">${escapeHtml(event.description)}</div>
756
876
  ` : ""}
757
877
 
758
878
  ${event.location ? `
759
879
  <div class="event-time">
760
880
  <span class="event-time-label">Location:</span>
761
- <span class="event-time-value">${event.location}</span>
881
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
762
882
  </div>
763
883
  ` : ""}
764
884
 
765
885
  ${attendeesList ? `
766
886
  <div class="event-time">
767
887
  <span class="event-time-label">Attendees:</span>
768
- <span class="event-time-value">${attendeesList}</span>
888
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
769
889
  </div>
770
890
  ` : ""}
771
891
 
772
892
  ${event.organizer ? `
773
893
  <div class="event-time">
774
894
  <span class="event-time-label">Organizer:</span>
775
- <span class="event-time-value">${event.organizer}</span>
895
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
776
896
  </div>
777
897
  ` : ""}
778
898
 
779
899
  ${event.notes ? `
780
900
  <div class="event-time">
781
901
  <span class="event-time-label">Notes:</span>
782
- <span class="event-time-value">${event.notes}</span>
902
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
783
903
  </div>
784
904
  ` : ""}
785
905
 
786
906
  ${event.url ? `
787
907
  <div class="event-time">
788
- <a href="${event.url}" target="_blank" rel="noopener noreferrer" class="event-link">
908
+ <a href="${escapeHtml(safeUrl(event.url))}" target="_blank" rel="noopener noreferrer" class="event-link">
789
909
  View Details \u2192
790
910
  </a>
791
911
  </div>
@@ -793,7 +913,7 @@ var init_CalendarElement = __esm({
793
913
 
794
914
  ${event.tags && event.tags.length > 0 ? `
795
915
  <div class="event-tags">
796
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
916
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
797
917
  </div>
798
918
  ` : ""}
799
919
  </div>
@@ -803,11 +923,17 @@ var init_CalendarElement = __esm({
803
923
  const renderEvent = this._renderEvent || defaultRenderEvent;
804
924
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
805
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
+ };
806
931
  const isHourBooked = (hour) => events.some((event) => {
807
- if (!event.startTime || !event.endTime) return true;
808
- const [startH] = event.startTime.split(":").map(Number);
809
- const [endH] = event.endTime.split(":").map(Number);
810
- return hour >= startH && hour < endH;
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;
811
937
  });
812
938
  const slots = Array.from({ length: 24 }, (_, hour) => {
813
939
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -818,81 +944,156 @@ var init_CalendarElement = __esm({
818
944
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
819
945
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
820
946
  const slotClasses = [
821
- "time-grid__slot",
822
- booked ? "time-grid__slot--booked" : "time-grid__slot--free",
823
- isRangeStart ? "time-grid__slot--range-start" : "",
824
- isRangeEnd ? "time-grid__slot--range-end" : "",
825
- isInRange ? "time-grid__slot--in-range" : ""
947
+ "time-grid-slot",
948
+ booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
949
+ isRangeStart ? "time-grid-slot-range-start" : "",
950
+ isRangeEnd ? "time-grid-slot-range-end" : "",
951
+ isInRange ? "time-grid-slot-in-range" : ""
826
952
  ].filter(Boolean).join(" ");
827
953
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
828
954
  return `
829
955
  <div class="${slotClasses}" ${slotAttrs}>
830
- <span class="time-grid__label">${startTime}</span>
831
- <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
956
+ <span class="time-grid-label">${startTime}</span>
957
+ <span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
832
958
  </div>`;
833
959
  });
834
960
  return `<div class="time-grid">${slots.join("")}</div>`;
835
961
  };
962
+ const multiMonth = viewModel.panes.length > 1;
963
+ const renderPane = (pane) => `
964
+ <div class="calendar-pane">
965
+ ${multiMonth ? `<div class="calendar-pane-caption">${escapeHtml(pane.monthAndYearText)}</div>` : ""}
966
+ <table class="calendar-table calendar-table-bordered">
967
+ <thead>
968
+ <tr>
969
+ ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
970
+ </tr>
971
+ </thead>
972
+ <tbody data-calendar-body>
973
+ ${isLoading ? Array.from(
974
+ { length: 6 },
975
+ () => `<tr>${Array.from(
976
+ { length: 7 },
977
+ () => `<td class="calendar-skeleton" aria-hidden="true"></td>`
978
+ ).join("")}</tr>`
979
+ ).join("") : pane.calendarDates.map(
980
+ (week) => `
981
+ <tr>
982
+ ${week.map((calendarDate, dayIndex) => {
983
+ const classes = getCellClasses(calendarDate);
984
+ const cellAttrs = [];
985
+ if (availabilityMode && calendarDate.isCurrentMonth) {
986
+ const bucket = this.resolveBucket(
987
+ calendarDate.date
988
+ );
989
+ const custom = (this._availabilityColors ?? {})[bucket];
990
+ classes.push(
991
+ `availability-${slugifyToken(bucket)}`
992
+ );
993
+ if (custom) {
994
+ classes.push("availability-status");
995
+ cellAttrs.push(
996
+ `style="--availability-color: ${escapeHtml(safeColor(custom))}"`
997
+ );
998
+ }
999
+ if (!this.isDateSelectable(calendarDate.date)) {
1000
+ classes.push("availability-unselectable");
1001
+ }
1002
+ cellAttrs.push(
1003
+ `aria-label="${escapeHtml(bucket)}"`
1004
+ );
1005
+ }
1006
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
1007
+ const d = calendarDate.date;
1008
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
1009
+ classes.push("availability-range-start");
1010
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
1011
+ classes.push("availability-range-end");
1012
+ if (this._rangeStart && this._rangeEnd) {
1013
+ if (d > this._rangeStart && d < this._rangeEnd)
1014
+ classes.push("availability-in-range");
1015
+ }
1016
+ }
1017
+ const dateString = calendarDate.date.toISOString();
1018
+ return `
1019
+ <td
1020
+ class="${classes.join(" ")}"
1021
+ data-date="${dateString}"
1022
+ data-day-index="${dayIndex}"
1023
+ data-clickable="true"
1024
+ ${cellAttrs.join(" ")}
1025
+ >
1026
+ ${calendarDate.date.getDate()}
1027
+ </td>
1028
+ `;
1029
+ }).join("")}
1030
+ </tr>
1031
+ `
1032
+ ).join("")}
1033
+ </tbody>
1034
+ </table>
1035
+ </div>
1036
+ `;
836
1037
  const html = `
837
1038
  ${title ? `
838
- <div class="page--title">
839
- <h1>${title}</h1>
1039
+ <div class="calendar-title">
1040
+ <h1>${escapeHtml(title)}</h1>
840
1041
  </div>
841
1042
  ` : ""}
842
1043
 
843
- <div class="calendar--content">
844
- <div class="calendar--card">
845
- <div class="calendar--nav-header">
846
- <button type="button" class="calendar--nav-arrow" data-action="previous" aria-label="Previous month">
1044
+ <div class="calendar-content">
1045
+ <div class="calendar-card">
1046
+ <div class="calendar-nav-header">
1047
+ <button type="button" class="calendar-nav-arrow" data-action="previous" aria-label="Previous month">
847
1048
  &#8249;
848
1049
  </button>
849
1050
 
850
- <div class="calendar--picker-container" data-picker-container>
1051
+ <div class="calendar-picker-container" data-picker-container>
851
1052
  <button
852
1053
  type="button"
853
- class="calendar--picker-btn"
1054
+ class="calendar-picker-btn"
854
1055
  data-action="toggle-picker"
855
1056
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
856
1057
  aria-haspopup="true"
857
1058
  >
858
1059
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
859
- <span class="calendar--picker-chevron">&#9662;</span>
1060
+ <span class="calendar-picker-chevron">&#9662;</span>
860
1061
  </button>
861
1062
 
862
1063
  ${this.pickerOpen ? `
863
- <div class="calendar--picker-dropdown">
864
- <div class="calendar--picker-year-row">
1064
+ <div class="calendar-picker-dropdown">
1065
+ <div class="calendar-picker-year-row">
865
1066
  <button
866
1067
  type="button"
867
- class="calendar--picker-year-arrow"
1068
+ class="calendar-picker-year-arrow"
868
1069
  data-action="year-prev"
869
1070
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
870
1071
  aria-label="Previous year"
871
1072
  >&#8249;</button>
872
1073
  <input
873
1074
  type="text"
874
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
875
- value="${this.yearInput || viewModel.currentYear}"
1075
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1076
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
876
1077
  data-year-input
877
1078
  aria-label="Year"
878
1079
  />
879
1080
  <button
880
1081
  type="button"
881
- class="calendar--picker-year-arrow"
1082
+ class="calendar-picker-year-arrow"
882
1083
  data-action="year-next"
883
1084
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
884
1085
  aria-label="Next year"
885
1086
  >&#8250;</button>
886
1087
  </div>
887
1088
 
888
- <div class="calendar--picker-months">
1089
+ <div class="calendar-picker-months">
889
1090
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
890
1091
  const isSelected = index === viewModel.currentMonth;
891
1092
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
892
1093
  return `
893
1094
  <button
894
1095
  type="button"
895
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1096
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
896
1097
  data-action="select-month"
897
1098
  data-month="${index}"
898
1099
  >${month}</button>
@@ -905,70 +1106,22 @@ var init_CalendarElement = __esm({
905
1106
 
906
1107
  <button
907
1108
  type="button"
908
- class="calendar--today-btn"
1109
+ class="calendar-today-btn"
909
1110
  data-action="today"
910
1111
  ${isCurrentMonth ? "disabled" : ""}
911
1112
  >Today</button>
912
1113
 
913
- <button type="button" class="calendar--nav-arrow" data-action="next" aria-label="Next month">
1114
+ <button type="button" class="calendar-nav-arrow" data-action="next" aria-label="Next month">
914
1115
  &#8250;
915
1116
  </button>
916
1117
  </div>
917
1118
 
918
- <table class="calendar--table calendar--table--bordered">
919
- <thead>
920
- <tr>
921
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
922
- </tr>
923
- </thead>
924
- <tbody data-calendar-body>
925
- ${isLoading ? Array.from(
926
- { length: 6 },
927
- () => `<tr>${Array.from(
928
- { length: 7 },
929
- () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
930
- ).join("")}</tr>`
931
- ).join("") : viewModel.calendarDates.map(
932
- (week) => `
933
- <tr>
934
- ${week.map((calendarDate, dayIndex) => {
935
- const classes = getCellClasses(calendarDate);
936
- if (availabilityMode && calendarDate.isCurrentMonth) {
937
- classes.push(
938
- calendarDate.hasEvents ? "availability--booked" : "availability--free"
939
- );
940
- }
941
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
942
- const d = calendarDate.date;
943
- if (this._rangeStart && isSameDay2(d, this._rangeStart))
944
- classes.push("availability--range-start");
945
- if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
946
- classes.push("availability--range-end");
947
- if (this._rangeStart && this._rangeEnd) {
948
- if (d > this._rangeStart && d < this._rangeEnd)
949
- classes.push("availability--in-range");
950
- }
951
- }
952
- const dateString = calendarDate.date.toISOString();
953
- return `
954
- <td
955
- class="${classes.join(" ")}"
956
- data-date="${dateString}"
957
- data-day-index="${dayIndex}"
958
- data-clickable="true"
959
- >
960
- ${calendarDate.date.getDate()}
961
- </td>
962
- `;
963
- }).join("")}
964
- </tr>
965
- `
966
- ).join("")}
967
- </tbody>
968
- </table>
1119
+ <div class="calendar-panes">
1120
+ ${viewModel.panes.map(renderPane).join("")}
1121
+ </div>
969
1122
 
970
1123
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
971
- <div class="date-popup ${viewModel.popupPositionClass}">
1124
+ <div class="date-popup">
972
1125
  <div class="popup-header">
973
1126
  <h2>${viewModel.scheduleDay}</h2>
974
1127
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -1006,8 +1159,7 @@ var init_CalendarElement = __esm({
1006
1159
  const availMode = this.getAttribute("availability-mode");
1007
1160
  const isSelectable = this.hasAttribute("selectable");
1008
1161
  if (availMode === "day" && isSelectable) {
1009
- const isBooked = cell.classList.contains("availability--booked");
1010
- if (!isBooked) {
1162
+ if (this.isDateSelectable(date)) {
1011
1163
  let startDate;
1012
1164
  let endDate;
1013
1165
  if (this._rangeEnd !== null) {
@@ -1025,7 +1177,7 @@ var init_CalendarElement = __esm({
1025
1177
  cursor.setDate(cursor.getDate() + 1);
1026
1178
  let blocked = false;
1027
1179
  while (cursor < e2) {
1028
- if (this.engine.getEventsForDate(cursor).length > 0) {
1180
+ if (!this.isDateSelectable(cursor)) {
1029
1181
  blocked = true;
1030
1182
  break;
1031
1183
  }
@@ -1259,31 +1411,84 @@ var init_CalendarElement = __esm({
1259
1411
  this.theme = theme;
1260
1412
  }
1261
1413
  getCurrentDate() {
1414
+ this.rethrowInitError();
1262
1415
  return this.engine?.getViewModel().selectedDate ?? null;
1263
1416
  }
1264
1417
  goToDate(date) {
1418
+ this.rethrowInitError();
1265
1419
  const year = date.getFullYear();
1266
1420
  const month = date.getMonth();
1267
1421
  this.dispatchMonthChange(year, month);
1268
1422
  this.actions?.jump(year, month);
1269
1423
  }
1270
1424
  getEngine() {
1425
+ this.rethrowInitError();
1271
1426
  if (!this.engine)
1272
1427
  throw new Error("CalendarElement is not connected to the DOM");
1273
1428
  return this.engine;
1274
1429
  }
1275
1430
  };
1276
- CalendarElement.observedAttributes = [
1431
+ _CalendarElement.observedAttributes = [
1277
1432
  "initial-date",
1278
1433
  "min-year",
1279
1434
  "max-year",
1280
1435
  "week-starts-on",
1436
+ "heading",
1437
+ "months",
1281
1438
  "title",
1282
1439
  "use-short-month-names",
1283
1440
  "availability-mode",
1284
1441
  "selectable",
1285
1442
  "loading"
1286
1443
  ];
1444
+ _CalendarElement.themeMap = {
1445
+ primary: "--calendar-primary-color",
1446
+ secondary: "--calendar-secondary-color",
1447
+ tertiary: "--calendar-tertiary-color",
1448
+ textColor: "--calendar-text-color",
1449
+ textLight: "--calendar-text-light",
1450
+ background: "--calendar-background",
1451
+ cellHover: "--calendar-cell-hover",
1452
+ borderColor: "--calendar-border-color",
1453
+ todayOutline: "--calendar-today-outline",
1454
+ selectedBg: "--calendar-selected-bg",
1455
+ headerBg: "--calendar-header-bg",
1456
+ popupBg: "--calendar-popup-bg",
1457
+ pickerBg: "--calendar-picker-bg",
1458
+ pickerShadow: "--calendar-picker-shadow",
1459
+ eventIndicator: "--calendar-event-indicator",
1460
+ onAccent: "--calendar-on-accent",
1461
+ link: "--calendar-link",
1462
+ openBg: "--calendar-open-bg",
1463
+ openFg: "--calendar-open-fg",
1464
+ conditionalBg: "--calendar-conditional-bg",
1465
+ conditionalFg: "--calendar-conditional-fg",
1466
+ blockedBg: "--calendar-blocked-bg",
1467
+ blockedFg: "--calendar-blocked-fg",
1468
+ rangeBg: "--calendar-range-bg",
1469
+ rangeOutline: "--calendar-range-outline",
1470
+ inRangeBg: "--calendar-in-range-bg",
1471
+ inRangeOutline: "--calendar-in-range-outline",
1472
+ badgeBg: "--calendar-badge-bg",
1473
+ badgeText: "--calendar-badge-text",
1474
+ badgeSuccessBg: "--calendar-badge-success-bg",
1475
+ badgeSuccessText: "--calendar-badge-success-text",
1476
+ badgeInfoBg: "--calendar-badge-info-bg",
1477
+ badgeInfoText: "--calendar-badge-info-text",
1478
+ badgeWarningBg: "--calendar-badge-warning-bg",
1479
+ badgeWarningText: "--calendar-badge-warning-text",
1480
+ badgeDangerBg: "--calendar-badge-danger-bg",
1481
+ badgeDangerText: "--calendar-badge-danger-text",
1482
+ badgeNeutralBg: "--calendar-badge-neutral-bg",
1483
+ badgeNeutralText: "--calendar-badge-neutral-text",
1484
+ badgePositiveBg: "--calendar-badge-positive-bg",
1485
+ badgePositiveText: "--calendar-badge-positive-text",
1486
+ badgeTentativeBg: "--calendar-badge-tentative-bg",
1487
+ badgeTentativeText: "--calendar-badge-tentative-text"
1488
+ };
1489
+ _CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
1490
+ _CalendarElement.titleDeprecationWarned = false;
1491
+ CalendarElement = _CalendarElement;
1287
1492
  }
1288
1493
  });
1289
1494
 
@@ -1297,6 +1502,7 @@ __export(web_components_exports, {
1297
1502
  MONTHS: () => MONTHS,
1298
1503
  MONTHS_FULL: () => MONTHS_FULL,
1299
1504
  defineCalendarElement: () => defineCalendarElement,
1505
+ escapeHtml: () => escapeHtml,
1300
1506
  formatAttendees: () => formatAttendees,
1301
1507
  formatDateForDisplay: () => formatDateForDisplay,
1302
1508
  formatTimeRange: () => formatTimeRange,
@@ -1307,13 +1513,15 @@ __export(web_components_exports, {
1307
1513
  getDefaultEventColor: () => getDefaultEventColor,
1308
1514
  getEventsForDate: () => getEventsForDate,
1309
1515
  getMonthYearText: () => getMonthYearText,
1310
- getPopupPositionClass: () => getPopupPositionClass,
1311
1516
  hasEvents: () => hasEvents,
1312
1517
  isSameDay: () => isSameDay,
1313
1518
  isToday: () => isToday,
1314
1519
  isValidHexColor: () => isValidHexColor,
1315
1520
  mergeCategoryColors: () => mergeCategoryColors,
1316
1521
  normalizeDate: () => normalizeDate,
1522
+ safeColor: () => safeColor,
1523
+ safeUrl: () => safeUrl,
1524
+ slugifyToken: () => slugifyToken,
1317
1525
  sortEventsByTime: () => sortEventsByTime
1318
1526
  });
1319
1527
  module.exports = __toCommonJS(web_components_exports);
@@ -1335,6 +1543,7 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1335
1543
  MONTHS,
1336
1544
  MONTHS_FULL,
1337
1545
  defineCalendarElement,
1546
+ escapeHtml,
1338
1547
  formatAttendees,
1339
1548
  formatDateForDisplay,
1340
1549
  formatTimeRange,
@@ -1345,13 +1554,15 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1345
1554
  getDefaultEventColor,
1346
1555
  getEventsForDate,
1347
1556
  getMonthYearText,
1348
- getPopupPositionClass,
1349
1557
  hasEvents,
1350
1558
  isSameDay,
1351
1559
  isToday,
1352
1560
  isValidHexColor,
1353
1561
  mergeCategoryColors,
1354
1562
  normalizeDate,
1563
+ safeColor,
1564
+ safeUrl,
1565
+ slugifyToken,
1355
1566
  sortEventsByTime
1356
1567
  });
1357
1568
  //# sourceMappingURL=index.js.map