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.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,40 +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.eventIndicator)
682
- 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;
683
801
  }
684
802
  render() {
685
803
  if (!this.engine) return;
686
804
  const viewModel = this.engine.getViewModel();
687
805
  const useShortMonths = this.hasAttribute("use-short-month-names");
688
- const title = this.getAttribute("title");
806
+ const title = this.headingText;
689
807
  const minYear = this.minYear;
690
808
  const maxYear = this.maxYear;
691
809
  const availabilityMode = this.getAttribute("availability-mode");
692
810
  const selectable = this.hasAttribute("selectable");
693
811
  const isLoading = this.loading;
812
+ if (availabilityMode) {
813
+ this.assertStatusesDeclared();
814
+ this.assertStatusesKnown();
815
+ }
694
816
  const today = /* @__PURE__ */ new Date();
695
817
  const todayMonth = today.getMonth();
696
818
  const todayYear = today.getFullYear();
@@ -699,7 +821,7 @@ var init_CalendarElement = __esm({
699
821
  const defaultRenderEvent = (event) => {
700
822
  const timeRange = formatTimeRange(event);
701
823
  const attendeesList = formatAttendees(event.attendees);
702
- let borderColor = event.color || "#3b82f6";
824
+ let borderColor = safeColor(event.color || "#3b82f6");
703
825
  if (event.category) {
704
826
  borderColor = this.engine.getCategoryColor(event.category);
705
827
  }
@@ -734,56 +856,56 @@ var init_CalendarElement = __esm({
734
856
  return `
735
857
  <div class="event-card" style="border-left-color: ${borderColor}">
736
858
  <div class="event-header">
737
- <div class="event-title">${event.name}</div>
859
+ <div class="event-title">${escapeHtml(event.name)}</div>
738
860
  <div class="event-badges">
739
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
740
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
741
- ${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>` : ""}
742
864
  </div>
743
865
  </div>
744
866
 
745
867
  ${timeRange ? `
746
868
  <div class="event-time">
747
869
  <span class="event-time-label">Time:</span>
748
- <span class="event-time-value">${timeRange}</span>
870
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
749
871
  </div>
750
872
  ` : ""}
751
873
 
752
874
  ${event.description ? `
753
- <div class="event-description">${event.description}</div>
875
+ <div class="event-description">${escapeHtml(event.description)}</div>
754
876
  ` : ""}
755
877
 
756
878
  ${event.location ? `
757
879
  <div class="event-time">
758
880
  <span class="event-time-label">Location:</span>
759
- <span class="event-time-value">${event.location}</span>
881
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
760
882
  </div>
761
883
  ` : ""}
762
884
 
763
885
  ${attendeesList ? `
764
886
  <div class="event-time">
765
887
  <span class="event-time-label">Attendees:</span>
766
- <span class="event-time-value">${attendeesList}</span>
888
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
767
889
  </div>
768
890
  ` : ""}
769
891
 
770
892
  ${event.organizer ? `
771
893
  <div class="event-time">
772
894
  <span class="event-time-label">Organizer:</span>
773
- <span class="event-time-value">${event.organizer}</span>
895
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
774
896
  </div>
775
897
  ` : ""}
776
898
 
777
899
  ${event.notes ? `
778
900
  <div class="event-time">
779
901
  <span class="event-time-label">Notes:</span>
780
- <span class="event-time-value">${event.notes}</span>
902
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
781
903
  </div>
782
904
  ` : ""}
783
905
 
784
906
  ${event.url ? `
785
907
  <div class="event-time">
786
- <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">
787
909
  View Details \u2192
788
910
  </a>
789
911
  </div>
@@ -791,7 +913,7 @@ var init_CalendarElement = __esm({
791
913
 
792
914
  ${event.tags && event.tags.length > 0 ? `
793
915
  <div class="event-tags">
794
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
916
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
795
917
  </div>
796
918
  ` : ""}
797
919
  </div>
@@ -801,11 +923,17 @@ var init_CalendarElement = __esm({
801
923
  const renderEvent = this._renderEvent || defaultRenderEvent;
802
924
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
803
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
+ };
804
931
  const isHourBooked = (hour) => events.some((event) => {
805
- if (!event.startTime || !event.endTime) return true;
806
- const [startH] = event.startTime.split(":").map(Number);
807
- const [endH] = event.endTime.split(":").map(Number);
808
- 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;
809
937
  });
810
938
  const slots = Array.from({ length: 24 }, (_, hour) => {
811
939
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -816,81 +944,156 @@ var init_CalendarElement = __esm({
816
944
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
817
945
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
818
946
  const slotClasses = [
819
- "time-grid__slot",
820
- booked ? "time-grid__slot--booked" : "time-grid__slot--free",
821
- isRangeStart ? "time-grid__slot--range-start" : "",
822
- isRangeEnd ? "time-grid__slot--range-end" : "",
823
- 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" : ""
824
952
  ].filter(Boolean).join(" ");
825
953
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
826
954
  return `
827
955
  <div class="${slotClasses}" ${slotAttrs}>
828
- <span class="time-grid__label">${startTime}</span>
829
- <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>
830
958
  </div>`;
831
959
  });
832
960
  return `<div class="time-grid">${slots.join("")}</div>`;
833
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
+ `;
834
1037
  const html = `
835
1038
  ${title ? `
836
- <div class="page--title">
837
- <h1>${title}</h1>
1039
+ <div class="calendar-title">
1040
+ <h1>${escapeHtml(title)}</h1>
838
1041
  </div>
839
1042
  ` : ""}
840
1043
 
841
- <div class="calendar--content">
842
- <div class="calendar--card">
843
- <div class="calendar--nav-header">
844
- <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">
845
1048
  &#8249;
846
1049
  </button>
847
1050
 
848
- <div class="calendar--picker-container" data-picker-container>
1051
+ <div class="calendar-picker-container" data-picker-container>
849
1052
  <button
850
1053
  type="button"
851
- class="calendar--picker-btn"
1054
+ class="calendar-picker-btn"
852
1055
  data-action="toggle-picker"
853
1056
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
854
1057
  aria-haspopup="true"
855
1058
  >
856
1059
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
857
- <span class="calendar--picker-chevron">&#9662;</span>
1060
+ <span class="calendar-picker-chevron">&#9662;</span>
858
1061
  </button>
859
1062
 
860
1063
  ${this.pickerOpen ? `
861
- <div class="calendar--picker-dropdown">
862
- <div class="calendar--picker-year-row">
1064
+ <div class="calendar-picker-dropdown">
1065
+ <div class="calendar-picker-year-row">
863
1066
  <button
864
1067
  type="button"
865
- class="calendar--picker-year-arrow"
1068
+ class="calendar-picker-year-arrow"
866
1069
  data-action="year-prev"
867
1070
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
868
1071
  aria-label="Previous year"
869
1072
  >&#8249;</button>
870
1073
  <input
871
1074
  type="text"
872
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
873
- value="${this.yearInput || viewModel.currentYear}"
1075
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1076
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
874
1077
  data-year-input
875
1078
  aria-label="Year"
876
1079
  />
877
1080
  <button
878
1081
  type="button"
879
- class="calendar--picker-year-arrow"
1082
+ class="calendar-picker-year-arrow"
880
1083
  data-action="year-next"
881
1084
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
882
1085
  aria-label="Next year"
883
1086
  >&#8250;</button>
884
1087
  </div>
885
1088
 
886
- <div class="calendar--picker-months">
1089
+ <div class="calendar-picker-months">
887
1090
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
888
1091
  const isSelected = index === viewModel.currentMonth;
889
1092
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
890
1093
  return `
891
1094
  <button
892
1095
  type="button"
893
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1096
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
894
1097
  data-action="select-month"
895
1098
  data-month="${index}"
896
1099
  >${month}</button>
@@ -903,70 +1106,22 @@ var init_CalendarElement = __esm({
903
1106
 
904
1107
  <button
905
1108
  type="button"
906
- class="calendar--today-btn"
1109
+ class="calendar-today-btn"
907
1110
  data-action="today"
908
1111
  ${isCurrentMonth ? "disabled" : ""}
909
1112
  >Today</button>
910
1113
 
911
- <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">
912
1115
  &#8250;
913
1116
  </button>
914
1117
  </div>
915
1118
 
916
- <table class="calendar--table calendar--table--bordered">
917
- <thead>
918
- <tr>
919
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
920
- </tr>
921
- </thead>
922
- <tbody data-calendar-body>
923
- ${isLoading ? Array.from(
924
- { length: 6 },
925
- () => `<tr>${Array.from(
926
- { length: 7 },
927
- () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
928
- ).join("")}</tr>`
929
- ).join("") : viewModel.calendarDates.map(
930
- (week) => `
931
- <tr>
932
- ${week.map((calendarDate, dayIndex) => {
933
- const classes = getCellClasses(calendarDate);
934
- if (availabilityMode && calendarDate.isCurrentMonth) {
935
- classes.push(
936
- calendarDate.hasEvents ? "availability--booked" : "availability--free"
937
- );
938
- }
939
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
940
- const d = calendarDate.date;
941
- if (this._rangeStart && isSameDay2(d, this._rangeStart))
942
- classes.push("availability--range-start");
943
- if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
944
- classes.push("availability--range-end");
945
- if (this._rangeStart && this._rangeEnd) {
946
- if (d > this._rangeStart && d < this._rangeEnd)
947
- classes.push("availability--in-range");
948
- }
949
- }
950
- const dateString = calendarDate.date.toISOString();
951
- return `
952
- <td
953
- class="${classes.join(" ")}"
954
- data-date="${dateString}"
955
- data-day-index="${dayIndex}"
956
- data-clickable="true"
957
- >
958
- ${calendarDate.date.getDate()}
959
- </td>
960
- `;
961
- }).join("")}
962
- </tr>
963
- `
964
- ).join("")}
965
- </tbody>
966
- </table>
1119
+ <div class="calendar-panes">
1120
+ ${viewModel.panes.map(renderPane).join("")}
1121
+ </div>
967
1122
 
968
1123
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
969
- <div class="date-popup ${viewModel.popupPositionClass}">
1124
+ <div class="date-popup">
970
1125
  <div class="popup-header">
971
1126
  <h2>${viewModel.scheduleDay}</h2>
972
1127
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -1004,8 +1159,7 @@ var init_CalendarElement = __esm({
1004
1159
  const availMode = this.getAttribute("availability-mode");
1005
1160
  const isSelectable = this.hasAttribute("selectable");
1006
1161
  if (availMode === "day" && isSelectable) {
1007
- const isBooked = cell.classList.contains("availability--booked");
1008
- if (!isBooked) {
1162
+ if (this.isDateSelectable(date)) {
1009
1163
  let startDate;
1010
1164
  let endDate;
1011
1165
  if (this._rangeEnd !== null) {
@@ -1023,7 +1177,7 @@ var init_CalendarElement = __esm({
1023
1177
  cursor.setDate(cursor.getDate() + 1);
1024
1178
  let blocked = false;
1025
1179
  while (cursor < e2) {
1026
- if (this.engine.getEventsForDate(cursor).length > 0) {
1180
+ if (!this.isDateSelectable(cursor)) {
1027
1181
  blocked = true;
1028
1182
  break;
1029
1183
  }
@@ -1257,31 +1411,84 @@ var init_CalendarElement = __esm({
1257
1411
  this.theme = theme;
1258
1412
  }
1259
1413
  getCurrentDate() {
1414
+ this.rethrowInitError();
1260
1415
  return this.engine?.getViewModel().selectedDate ?? null;
1261
1416
  }
1262
1417
  goToDate(date) {
1418
+ this.rethrowInitError();
1263
1419
  const year = date.getFullYear();
1264
1420
  const month = date.getMonth();
1265
1421
  this.dispatchMonthChange(year, month);
1266
1422
  this.actions?.jump(year, month);
1267
1423
  }
1268
1424
  getEngine() {
1425
+ this.rethrowInitError();
1269
1426
  if (!this.engine)
1270
1427
  throw new Error("CalendarElement is not connected to the DOM");
1271
1428
  return this.engine;
1272
1429
  }
1273
1430
  };
1274
- CalendarElement.observedAttributes = [
1431
+ _CalendarElement.observedAttributes = [
1275
1432
  "initial-date",
1276
1433
  "min-year",
1277
1434
  "max-year",
1278
1435
  "week-starts-on",
1436
+ "heading",
1437
+ "months",
1279
1438
  "title",
1280
1439
  "use-short-month-names",
1281
1440
  "availability-mode",
1282
1441
  "selectable",
1283
1442
  "loading"
1284
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;
1285
1492
  }
1286
1493
  });
1287
1494
 
@@ -1295,6 +1502,7 @@ __export(web_components_exports, {
1295
1502
  MONTHS: () => MONTHS,
1296
1503
  MONTHS_FULL: () => MONTHS_FULL,
1297
1504
  defineCalendarElement: () => defineCalendarElement,
1505
+ escapeHtml: () => escapeHtml,
1298
1506
  formatAttendees: () => formatAttendees,
1299
1507
  formatDateForDisplay: () => formatDateForDisplay,
1300
1508
  formatTimeRange: () => formatTimeRange,
@@ -1305,13 +1513,15 @@ __export(web_components_exports, {
1305
1513
  getDefaultEventColor: () => getDefaultEventColor,
1306
1514
  getEventsForDate: () => getEventsForDate,
1307
1515
  getMonthYearText: () => getMonthYearText,
1308
- getPopupPositionClass: () => getPopupPositionClass,
1309
1516
  hasEvents: () => hasEvents,
1310
1517
  isSameDay: () => isSameDay,
1311
1518
  isToday: () => isToday,
1312
1519
  isValidHexColor: () => isValidHexColor,
1313
1520
  mergeCategoryColors: () => mergeCategoryColors,
1314
1521
  normalizeDate: () => normalizeDate,
1522
+ safeColor: () => safeColor,
1523
+ safeUrl: () => safeUrl,
1524
+ slugifyToken: () => slugifyToken,
1315
1525
  sortEventsByTime: () => sortEventsByTime
1316
1526
  });
1317
1527
  module.exports = __toCommonJS(web_components_exports);
@@ -1333,6 +1543,7 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1333
1543
  MONTHS,
1334
1544
  MONTHS_FULL,
1335
1545
  defineCalendarElement,
1546
+ escapeHtml,
1336
1547
  formatAttendees,
1337
1548
  formatDateForDisplay,
1338
1549
  formatTimeRange,
@@ -1343,13 +1554,15 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
1343
1554
  getDefaultEventColor,
1344
1555
  getEventsForDate,
1345
1556
  getMonthYearText,
1346
- getPopupPositionClass,
1347
1557
  hasEvents,
1348
1558
  isSameDay,
1349
1559
  isToday,
1350
1560
  isValidHexColor,
1351
1561
  mergeCategoryColors,
1352
1562
  normalizeDate,
1563
+ safeColor,
1564
+ safeUrl,
1565
+ slugifyToken,
1353
1566
  sortEventsByTime
1354
1567
  });
1355
1568
  //# sourceMappingURL=index.js.map