kalendly 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -341,6 +341,10 @@ Primitives are set as HTML attributes:
341
341
  | `months` | `"1"\|"2"` | `"1"` | Render two months side by side |
342
342
  | `min-year` | `string` | currentYear - 30 | Minimum year in picker |
343
343
  | `max-year` | `string` | currentYear + 10 | Maximum year in picker |
344
+ | `min-date` | `string` | — | Earliest bookable day, inclusive |
345
+ | `max-date` | `string` | — | Latest bookable day, inclusive |
346
+ | `available-days` | `string` | — | Bookable weekdays, e.g. `"1,2,3,4,5"` |
347
+ | `available-hours` | `string` | — | Bookable hours, e.g. `"09:00-17:00"` |
344
348
  | `week-starts-on` | `"0"\|"1"` | `"0"` | Week start: 0 = Sunday, 1 = Monday |
345
349
  | `use-short-month-names` | `string` | — | Present = use abbreviated month names |
346
350
  | `availability-mode` | `"day"\|"time"` | — | Hides event details; shows booked/free cells |
@@ -495,6 +499,40 @@ The grid renders `1440 / slot-duration` slots, and `cal-availability-select` emi
495
499
 
496
500
  Precision below `slot-duration` is not representable: a booking from 17:30 on a 60-minute grid marks 17:00–18:00 booked, because that hour cannot be sold. A vendor working in half-hours sets `slot-duration="30"` rather than expecting the grid to subdivide itself.
497
501
 
502
+ ### Booking constraints
503
+
504
+ Four optional attributes describe when a vendor is open. Omit them all and nothing is constrained.
505
+
506
+ ```html
507
+ <kal-calendar
508
+ availability-mode="time"
509
+ min-date="2026-09-01"
510
+ max-date="2026-12-31"
511
+ available-days="1,2,3,4,5"
512
+ available-hours="09:00-12:00,13:00-17:00"
513
+ ></kal-calendar>
514
+ ```
515
+
516
+ They express two different kinds of rule, which is why there are four and not two:
517
+
518
+ | | Kind | Says |
519
+ | ----------------------- | ---------------- | --------------------------------------------- |
520
+ | `min-date` / `max-date` | one-off horizon | "we take bookings from September to December" |
521
+ | `available-days` | recurring weekly | "we work weekdays" |
522
+ | `available-hours` | recurring daily | "we work 9 to 5, closed for lunch" |
523
+
524
+ None substitutes for another. A horizon cannot say "weekdays only", a weekday list cannot say "not past December", and neither says anything about the working day.
525
+
526
+ `min-date` and `max-date` are **inclusive** and parse like `initial-date`. `available-days` uses `Date.prototype.getDay()` numbering — `0` is Sunday — which is independent of `week-starts-on`, a display setting.
527
+
528
+ `available-hours` takes a comma-separated list because a working day is not always contiguous: `"09:00-12:00,13:00-17:00"` closes for lunch, and `"09:00-17:00,17:30-22:00"` runs meetings then an evening class. Each range is half-open, `[start, end)` — `"09:00-17:00"` on an hourly grid makes 16:00–17:00 the last bookable slot, the same convention events use.
529
+
530
+ Excluded **days** are not click targets at all: no hover response, and neither `cal-date-select` nor `cal-availability-select` fires. Excluded **hours** render greyed and marked `Closed`, and emit no `cal-slot-select`. They are shown rather than hidden so a booking that falls outside the window is still visible to the vendor.
531
+
532
+ Style either with `--calendar-out-of-range-bg` and `--calendar-out-of-range-fg`, or the `outOfRangeBg` / `outOfRangeFg` theme keys.
533
+
534
+ Bad input throws and names the attribute — an unreadable date, `min-date` after `max-date`, a weekday outside `0`–`6`, a malformed or inverted range, ranges that overlap, or a boundary that misses the `slot-duration` grid (`"09:30-17:00"` with hourly slots has no slot to land on).
535
+
498
536
  #### Bookings that cross midnight
499
537
 
500
538
  An `endTime` at or before its `startTime` is treated as the next day, so a booking runs as one interval rather than two half-days:
@@ -637,6 +675,14 @@ cal.getEngine();
637
675
  > `mailto:` and relative URLs; anything else becomes `#`. `color` accepts hex
638
676
  > values and CSS colour keywords.
639
677
 
678
+ > **`name` is optional.** It is rendered only as the event card's title, and
679
+ > availability mode never renders the card — so callers there need not supply one.
680
+ > An event without a name renders a card with no title rather than an empty one.
681
+ > Note this is a type-level break for anyone _reading_ `name` off
682
+ > `cal-date-select`: the field can now be absent, so `strictNullChecks` requires a
683
+ > guard. Nothing changes at runtime — events are handed back by reference, so
684
+ > whatever you supply comes back intact.
685
+
640
686
  > **Multi-day events.** `endDate` is the last day of a span and is **inclusive** —
641
687
  > `date: '2026-03-03', endDate: '2026-03-05'` covers three days. That matches the
642
688
  > `endDate` `cal-availability-select` emits, so a selection can be handed straight
@@ -658,8 +704,8 @@ cal.getEngine();
658
704
  ```typescript
659
705
  interface CalendarEvent {
660
706
  id: string | number;
661
- name: string;
662
707
  date: string | Date;
708
+ name?: string; // event card title; omit it and no title renders
663
709
 
664
710
  endDate?: string | Date; // last day of a span, inclusive
665
711
 
@@ -722,6 +768,8 @@ for. Override the `--calendar-*` layer — the primitives are internal.
722
768
  --calendar-cell-hover: #f3f4f6;
723
769
  --calendar-header-bg: #f8f9fa;
724
770
  --calendar-selected-bg: #eff6ff;
771
+ --calendar-out-of-range-bg: #f3f4f6;
772
+ --calendar-out-of-range-fg: #6b7280;
725
773
  --calendar-popup-bg: #fff;
726
774
  --calendar-picker-bg: #fff;
727
775
  --calendar-today-outline: #f7db04;
@@ -784,6 +832,8 @@ cal.theme = {
784
832
  borderColor: '#e5e7eb',
785
833
  todayOutline: '#fbbf24',
786
834
  selectedBg: '#eff6ff',
835
+ outOfRangeBg: '#f3f4f6',
836
+ outOfRangeFg: '#6b7280',
787
837
  headerBg: '#f8f9fa',
788
838
  popupBg: '#ffffff',
789
839
  pickerBg: '#ffffff',
@@ -1,8 +1,8 @@
1
1
  type Open<T extends string> = T | (string & {});
2
2
  interface CalendarEvent {
3
3
  id: string | number;
4
- name: string;
5
4
  date: string | Date;
5
+ name?: string;
6
6
  endDate?: string | Date;
7
7
  startTime?: string;
8
8
  endTime?: string;
@@ -104,6 +104,8 @@ interface CalendarTheme {
104
104
  borderColor?: string;
105
105
  todayOutline?: string;
106
106
  selectedBg?: string;
107
+ outOfRangeBg?: string;
108
+ outOfRangeFg?: string;
107
109
  headerBg?: string;
108
110
  popupBg?: string;
109
111
  pickerBg?: string;
@@ -146,6 +148,9 @@ declare function normalizeDate(date: Date): Date;
146
148
  declare function isSameDay(date1: Date, date2: Date): boolean;
147
149
  declare function isToday(date: Date): boolean;
148
150
  declare function generateYears(minYear?: number, maxYear?: number): number[];
151
+ declare function parseHourRanges(value: string, slotDuration: number): Array<[number, number]>;
152
+ declare function isDateWithinWindow(date: Date, min: Date | null, max: Date | null): boolean;
153
+ declare function isDayAllowed(date: Date, days: number[] | null): boolean;
149
154
  declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
150
155
  declare function getEventsForDate(events: CalendarEvent[], date: Date): CalendarEvent[];
151
156
  declare function hasEvents(events: CalendarEvent[], date: Date): boolean;
@@ -269,4 +274,4 @@ declare class CalendarEngine {
269
274
  destroy(): void;
270
275
  }
271
276
 
272
- export { type CalendarActions, type CalendarConfig, type CalendarDate, CalendarEngine, type CalendarEvent, type CalendarEventHandler, type CalendarPane, type CalendarProps, type CalendarState, type CalendarTheme, type CalendarViewModel, type CategoryColorMap, DAYS, DEFAULT_CATEGORY_COLORS, DEFAULT_SLOT_DURATION, MINUTES_PER_DAY, MONTHS, MONTHS_FULL, bookedSlots, escapeHtml, eventCoversDate, eventInterval, formatAttendees, formatDateForDisplay, formatMinutes, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isSameDay, isToday, isValidHexColor, mergeCategoryColors, mergeIntervals, normalizeDate, parseTimeToMinutes, safeColor, safeUrl, slugifyToken, sortEventsByTime };
277
+ export { type CalendarActions, type CalendarConfig, type CalendarDate, CalendarEngine, type CalendarEvent, type CalendarEventHandler, type CalendarPane, type CalendarProps, type CalendarState, type CalendarTheme, type CalendarViewModel, type CategoryColorMap, DAYS, DEFAULT_CATEGORY_COLORS, DEFAULT_SLOT_DURATION, MINUTES_PER_DAY, MONTHS, MONTHS_FULL, bookedSlots, escapeHtml, eventCoversDate, eventInterval, formatAttendees, formatDateForDisplay, formatMinutes, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isDateWithinWindow, isDayAllowed, isSameDay, isToday, isValidHexColor, mergeCategoryColors, mergeIntervals, normalizeDate, parseHourRanges, parseTimeToMinutes, safeColor, safeUrl, slugifyToken, sortEventsByTime };
@@ -1,8 +1,8 @@
1
1
  type Open<T extends string> = T | (string & {});
2
2
  interface CalendarEvent {
3
3
  id: string | number;
4
- name: string;
5
4
  date: string | Date;
5
+ name?: string;
6
6
  endDate?: string | Date;
7
7
  startTime?: string;
8
8
  endTime?: string;
@@ -104,6 +104,8 @@ interface CalendarTheme {
104
104
  borderColor?: string;
105
105
  todayOutline?: string;
106
106
  selectedBg?: string;
107
+ outOfRangeBg?: string;
108
+ outOfRangeFg?: string;
107
109
  headerBg?: string;
108
110
  popupBg?: string;
109
111
  pickerBg?: string;
@@ -146,6 +148,9 @@ declare function normalizeDate(date: Date): Date;
146
148
  declare function isSameDay(date1: Date, date2: Date): boolean;
147
149
  declare function isToday(date: Date): boolean;
148
150
  declare function generateYears(minYear?: number, maxYear?: number): number[];
151
+ declare function parseHourRanges(value: string, slotDuration: number): Array<[number, number]>;
152
+ declare function isDateWithinWindow(date: Date, min: Date | null, max: Date | null): boolean;
153
+ declare function isDayAllowed(date: Date, days: number[] | null): boolean;
149
154
  declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
150
155
  declare function getEventsForDate(events: CalendarEvent[], date: Date): CalendarEvent[];
151
156
  declare function hasEvents(events: CalendarEvent[], date: Date): boolean;
@@ -269,4 +274,4 @@ declare class CalendarEngine {
269
274
  destroy(): void;
270
275
  }
271
276
 
272
- export { type CalendarActions, type CalendarConfig, type CalendarDate, CalendarEngine, type CalendarEvent, type CalendarEventHandler, type CalendarPane, type CalendarProps, type CalendarState, type CalendarTheme, type CalendarViewModel, type CategoryColorMap, DAYS, DEFAULT_CATEGORY_COLORS, DEFAULT_SLOT_DURATION, MINUTES_PER_DAY, MONTHS, MONTHS_FULL, bookedSlots, escapeHtml, eventCoversDate, eventInterval, formatAttendees, formatDateForDisplay, formatMinutes, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isSameDay, isToday, isValidHexColor, mergeCategoryColors, mergeIntervals, normalizeDate, parseTimeToMinutes, safeColor, safeUrl, slugifyToken, sortEventsByTime };
277
+ export { type CalendarActions, type CalendarConfig, type CalendarDate, CalendarEngine, type CalendarEvent, type CalendarEventHandler, type CalendarPane, type CalendarProps, type CalendarState, type CalendarTheme, type CalendarViewModel, type CategoryColorMap, DAYS, DEFAULT_CATEGORY_COLORS, DEFAULT_SLOT_DURATION, MINUTES_PER_DAY, MONTHS, MONTHS_FULL, bookedSlots, escapeHtml, eventCoversDate, eventInterval, formatAttendees, formatDateForDisplay, formatMinutes, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isDateWithinWindow, isDayAllowed, isSameDay, isToday, isValidHexColor, mergeCategoryColors, mergeIntervals, normalizeDate, parseHourRanges, parseTimeToMinutes, safeColor, safeUrl, slugifyToken, sortEventsByTime };
@@ -43,12 +43,15 @@ __export(core_exports, {
43
43
  getEventsForDate: () => getEventsForDate,
44
44
  getMonthYearText: () => getMonthYearText,
45
45
  hasEvents: () => hasEvents,
46
+ isDateWithinWindow: () => isDateWithinWindow,
47
+ isDayAllowed: () => isDayAllowed,
46
48
  isSameDay: () => isSameDay,
47
49
  isToday: () => isToday,
48
50
  isValidHexColor: () => isValidHexColor,
49
51
  mergeCategoryColors: () => mergeCategoryColors,
50
52
  mergeIntervals: () => mergeIntervals,
51
53
  normalizeDate: () => normalizeDate,
54
+ parseHourRanges: () => parseHourRanges,
52
55
  parseTimeToMinutes: () => parseTimeToMinutes,
53
56
  safeColor: () => safeColor,
54
57
  safeUrl: () => safeUrl,
@@ -110,6 +113,54 @@ function generateYears(minYear, maxYear) {
110
113
  const max = maxYear ?? currentYear + 10;
111
114
  return Array.from({ length: max - min + 1 }, (_, i) => min + i);
112
115
  }
116
+ function parseHourRanges(value, slotDuration) {
117
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
118
+ if (parts.length === 0) {
119
+ throw new Error(
120
+ `<kal-calendar> available-hours is empty. Omit the attribute to allow every hour, or name at least one HH:MM-HH:MM range.`
121
+ );
122
+ }
123
+ const ranges = parts.map((part) => {
124
+ const halves = part.split("-");
125
+ if (halves.length !== 2) {
126
+ throw new Error(
127
+ `<kal-calendar> available-hours range "${part}" is not HH:MM-HH:MM.`
128
+ );
129
+ }
130
+ const [start, end] = halves.map((half) => parseTimeToMinutes(half.trim()));
131
+ if (start === null || end === null) {
132
+ throw new Error(
133
+ `<kal-calendar> available-hours range "${part}" has an unreadable time. Use 24-hour HH:MM.`
134
+ );
135
+ }
136
+ if (start >= end) {
137
+ throw new Error(
138
+ `<kal-calendar> available-hours range "${part}" starts at or after it ends.`
139
+ );
140
+ }
141
+ if (start % slotDuration !== 0 || end % slotDuration !== 0) {
142
+ throw new Error(
143
+ `<kal-calendar> available-hours range "${part}" does not land on a ${slotDuration}-minute slot boundary.`
144
+ );
145
+ }
146
+ return [start, end];
147
+ });
148
+ if (mergeIntervals(ranges).length !== ranges.length) {
149
+ throw new Error(
150
+ `<kal-calendar> available-hours ranges overlap or touch: "${value}". Write each bookable window once.`
151
+ );
152
+ }
153
+ return ranges;
154
+ }
155
+ function isDateWithinWindow(date, min, max) {
156
+ const target = normalizeDate(date).getTime();
157
+ if (min && target < normalizeDate(min).getTime()) return false;
158
+ if (max && target > normalizeDate(max).getTime()) return false;
159
+ return true;
160
+ }
161
+ function isDayAllowed(date, days) {
162
+ return days === null || days.includes(date.getDay());
163
+ }
113
164
  function eventCoversDate(event, date) {
114
165
  const start = normalizeDate(new Date(event.date)).getTime();
115
166
  const target = normalizeDate(date).getTime();
@@ -619,12 +670,15 @@ var CalendarEngine = class {
619
670
  getEventsForDate,
620
671
  getMonthYearText,
621
672
  hasEvents,
673
+ isDateWithinWindow,
674
+ isDayAllowed,
622
675
  isSameDay,
623
676
  isToday,
624
677
  isValidHexColor,
625
678
  mergeCategoryColors,
626
679
  mergeIntervals,
627
680
  normalizeDate,
681
+ parseHourRanges,
628
682
  parseTimeToMinutes,
629
683
  safeColor,
630
684
  safeUrl,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/index.ts","../../src/core/utils.ts","../../src/core/calendar-engine.ts"],"sourcesContent":["export * from './types';\nexport * from './utils';\nexport { CalendarEngine } from './calendar-engine';\n\n// Re-export commonly used utilities\nexport {\n MONTHS,\n MONTHS_FULL,\n DAYS,\n normalizeDate,\n isSameDay,\n isToday,\n generateYears,\n getEventsForDate,\n hasEvents,\n generateCalendarDates,\n getCellClasses,\n formatDateForDisplay,\n getMonthYearText,\n} from './utils';\n","import { CalendarEvent, CalendarDate, CategoryColorMap } from './types';\n\nexport const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\nexport const MONTHS_FULL = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const DAYS = [\n 'Sunday',\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n];\n\nexport function normalizeDate(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);\n}\n\nexport function isSameDay(date1: Date, date2: Date): boolean {\n return normalizeDate(date1).getTime() === normalizeDate(date2).getTime();\n}\n\nexport function isToday(date: Date): boolean {\n return isSameDay(date, new Date());\n}\n\nexport function generateYears(minYear?: number, maxYear?: number): number[] {\n const currentYear = new Date().getFullYear();\n const min = minYear ?? currentYear - 30;\n const max = maxYear ?? currentYear + 10;\n\n return Array.from({ length: max - min + 1 }, (_, i) => min + i);\n}\n\n// endDate is inclusive, matching the endDate cal-availability-select emits, so\n// a selection can be handed straight back as one event.\nexport function eventCoversDate(event: CalendarEvent, date: Date): boolean {\n const start = normalizeDate(new Date(event.date)).getTime();\n const target = normalizeDate(date).getTime();\n if (Number.isNaN(start)) return false;\n\n if (event.endDate === undefined || event.endDate === null) {\n return start === target;\n }\n\n const end = normalizeDate(new Date(event.endDate)).getTime();\n if (Number.isNaN(end)) {\n throw new Error(\n `<kal-calendar> event ${event.id} has an unreadable endDate: ` +\n `${String(event.endDate)}.`\n );\n }\n if (end < start) {\n throw new Error(\n `<kal-calendar> event ${event.id} has an endDate before its date: ` +\n `${String(event.endDate)} < ${String(event.date)}.`\n );\n }\n\n return target >= start && target <= end;\n}\n\nexport function getEventsForDate(\n events: CalendarEvent[],\n date: Date\n): CalendarEvent[] {\n return events.filter(event => eventCoversDate(event, date));\n}\n\nexport function hasEvents(events: CalendarEvent[], date: Date): boolean {\n return getEventsForDate(events, date).length > 0;\n}\n\nexport function generateCalendarDates(\n year: number,\n month: number,\n events: CalendarEvent[] = [],\n weekStartsOn: 0 | 1 = 0\n): CalendarDate[][] {\n const firstDay = new Date(year, month, 1);\n const lastDay = new Date(year, month + 1, 0);\n const daysInMonth = lastDay.getDate();\n const prevMonthLastDay = new Date(year, month, 0).getDate();\n\n let firstDayOfWeek = firstDay.getDay();\n if (weekStartsOn === 1) {\n firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;\n }\n\n const dates: CalendarDate[][] = [];\n let day = 1;\n let nextMonthDay = 1;\n\n for (let week = 0; week < 6; week++) {\n const weekDates: CalendarDate[] = [];\n\n for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {\n if (week === 0 && dayOfWeek < firstDayOfWeek) {\n // Previous month days\n const prevDay = prevMonthLastDay - firstDayOfWeek + dayOfWeek + 1;\n const prevDate = new Date(year, month - 1, prevDay);\n const dateEvents = getEventsForDate(events, prevDate);\n weekDates.push({\n date: prevDate,\n isCurrentMonth: false,\n isToday: isToday(prevDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n } else if (day > daysInMonth) {\n // Next month days\n const nextDate = new Date(year, month + 1, nextMonthDay);\n const dateEvents = getEventsForDate(events, nextDate);\n weekDates.push({\n date: nextDate,\n isCurrentMonth: false,\n isToday: isToday(nextDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n nextMonthDay++;\n } else {\n // Current month days\n const currentDate = new Date(year, month, day);\n const dateEvents = getEventsForDate(events, currentDate);\n weekDates.push({\n date: currentDate,\n isCurrentMonth: true,\n isToday: isToday(currentDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n day++;\n }\n }\n\n dates.push(weekDates);\n }\n\n return dates;\n}\n\nexport function getCellClasses(calendarDate: CalendarDate): string[] {\n const classes: string[] = [];\n\n if (!calendarDate.isCurrentMonth) {\n classes.push('calendar-cell-other-month');\n }\n\n if (calendarDate.isToday) {\n classes.push('calendar-cell-today');\n }\n\n if (calendarDate.hasEvents) {\n classes.push('calendar-cell-has-event');\n }\n\n return classes;\n}\n\nexport function formatDateForDisplay(date: Date): string {\n return `${DAYS[date.getDay()]} ${date.getDate()}`;\n}\n\nexport function getMonthYearText(year: number, month: number): string {\n return `${MONTHS_FULL[month]} ${year}`;\n}\n\nexport function formatTimeRange(event: CalendarEvent): string {\n if (event.allDay || (!event.startTime && !event.endTime)) {\n return 'All day';\n }\n\n if (event.startTime && event.endTime) {\n return `${event.startTime} - ${event.endTime}`;\n }\n\n if (event.startTime) {\n return `${event.startTime}`;\n }\n\n return '';\n}\n\nexport function formatAttendees(attendees?: string[]): string {\n if (!attendees || attendees.length === 0) return '';\n\n if (attendees.length === 1) return attendees[0];\n if (attendees.length === 2) return attendees.join(' and ');\n\n return `${attendees.slice(0, -1).join(', ')}, and ${attendees[attendees.length - 1]}`;\n}\n\nexport function sortEventsByTime(events: CalendarEvent[]): CalendarEvent[] {\n return [...events].sort((a, b) => {\n const aAllDay = a.allDay || (!a.startTime && !a.endTime);\n const bAllDay = b.allDay || (!b.startTime && !b.endTime);\n\n if (aAllDay && !bAllDay) return -1;\n if (!aAllDay && bAllDay) return 1;\n\n if (!a.startTime || !b.startTime) return 0;\n return a.startTime.localeCompare(b.startTime);\n });\n}\n\nexport const DEFAULT_CATEGORY_COLORS: CategoryColorMap = {\n work: '#3b82f6',\n personal: '#8b5cf6',\n meeting: '#10b981',\n deadline: '#ef4444',\n appointment: '#f59e0b',\n other: '#6b7280',\n};\n\n// Dynamic function that accepts custom colors\nexport function getDefaultEventColor(\n category?: string,\n customColors?: CategoryColorMap\n): string {\n const colorMap = customColors || DEFAULT_CATEGORY_COLORS;\n\n if (category && colorMap[category]) {\n return colorMap[category];\n }\n\n // Default fallback color\n return '#fc8917';\n}\n\n// Helper to merge custom colors with defaults\nexport function mergeCategoryColors(\n customColors?: CategoryColorMap\n): CategoryColorMap {\n return {\n ...DEFAULT_CATEGORY_COLORS,\n ...customColors,\n };\n}\n\n// Validate if a color is a valid hex color\nexport function isValidHexColor(color: string): boolean {\n return /^#([0-9A-F]{3}){1,2}$/i.test(color);\n}\n\n// Get color for a category with validation\nexport function getCategoryColor(\n category: string,\n customColors?: CategoryColorMap\n): string {\n const colorMap = mergeCategoryColors(customColors);\n const color = colorMap[category] || colorMap.other || '#fc8917';\n\n return isValidHexColor(color) ? color : '#fc8917';\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n// Escape a value for interpolation into HTML text or a quoted attribute\nexport function escapeHtml(value: unknown): string {\n if (value === null || value === undefined) return '';\n\n return String(value).replace(/[&<>\"']/g, char => HTML_ESCAPES[char]);\n}\n\n// Reduce a caller-supplied value to a single safe CSS class token\nexport function slugifyToken(value: string): string {\n return String(value)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n// Allow http(s), mailto and relative URLs; anything else (javascript:, data:,\n// vbscript:) collapses to a harmless anchor\nexport function safeUrl(value: string): string {\n // Browsers ignore control characters when resolving a scheme, so\n // `java\\0script:` and `java\\tscript:` both navigate. Strip them before\n // testing rather than after.\n const normalized = String(value)\n .split('')\n .filter(char => char.charCodeAt(0) > 0x20)\n .join('');\n\n if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;\n\n // Relative URLs have no scheme at all — a colon before the first slash,\n // question mark or hash means someone is naming one\n const schemeless = !/^[^/?#]*:/.test(normalized);\n\n return schemeless ? normalized : '#';\n}\n\n// Accept hex colors and bare CSS color keywords; reject anything that could\n// terminate the declaration or escape the style attribute\nexport function safeColor(value: string): string {\n const trimmed = String(value).trim();\n\n return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed)\n ? trimmed\n : '#3b82f6';\n}\n\nexport const MINUTES_PER_DAY = 1440;\nexport const DEFAULT_SLOT_DURATION = 60;\n\nexport function parseTimeToMinutes(time: unknown): number | null {\n if (typeof time !== 'string') return null;\n\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(time.trim());\n if (!match) return null;\n\n const hours = Number(match[1]);\n const mins = Number(match[2]);\n if (hours > 24 || mins > 59 || (hours === 24 && mins > 0)) return null;\n\n return hours * 60 + mins;\n}\n\nexport function formatMinutes(total: number): string {\n const hours = Math.floor(total / 60);\n const mins = total % 60;\n return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;\n}\n\n// Absolute [start, end) in minutes. An end at or before the start is on the\n// following day, so a booking that crosses midnight is one interval rather than\n// two half-days. An unreadable time occupies the whole day — a booking surface\n// that cannot parse a time must not offer the slot.\nexport function eventInterval(\n event: CalendarEvent,\n slotDuration: number,\n onDay: Date\n): [number, number] | null {\n const day = normalizeDate(onDay).getTime();\n if (Number.isNaN(day)) return null;\n\n const base = day / 60000;\n const wholeDay: [number, number] = [base, base + MINUTES_PER_DAY];\n\n if (event.allDay || !event.startTime) return wholeDay;\n\n const from = parseTimeToMinutes(event.startTime);\n if (from === null) return wholeDay;\n\n if (event.endTime === undefined || event.endTime === null) {\n return [base + from, base + from + slotDuration];\n }\n\n const to = parseTimeToMinutes(event.endTime);\n if (to === null) return wholeDay;\n\n return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];\n}\n\nexport function mergeIntervals(\n intervals: Array<[number, number]>\n): Array<[number, number]> {\n const sorted = [...intervals].sort((a, b) => a[0] - b[0]);\n\n return sorted.reduce<Array<[number, number]>>((merged, [start, end]) => {\n const last = merged[merged.length - 1];\n if (last && start <= last[1]) last[1] = Math.max(last[1], end);\n else merged.push([start, end]);\n return merged;\n }, []);\n}\n\n// Which of the day's slots any booking overlaps. Events from the previous day\n// belong in `events` so a booking crossing midnight marks the morning it runs into.\nexport function bookedSlots(\n events: CalendarEvent[],\n date: Date,\n slotDuration: number = DEFAULT_SLOT_DURATION\n): boolean[] {\n const base = normalizeDate(date).getTime() / 60000;\n\n const previousDay = new Date(date);\n previousDay.setDate(previousDay.getDate() - 1);\n\n // A span's times repeat on each day it covers, so the window is anchored to\n // the day being drawn rather than the day the span began\n const intervals: Array<[number, number]> = [];\n for (const event of new Set(events)) {\n for (const day of [previousDay, date]) {\n if (!eventCoversDate(event, day)) continue;\n const interval = eventInterval(event, slotDuration, day);\n if (interval) intervals.push(interval);\n }\n }\n\n const merged = mergeIntervals(intervals);\n\n return Array.from(\n { length: Math.floor(MINUTES_PER_DAY / slotDuration) },\n (_, index) => {\n const start = base + index * slotDuration;\n const end = start + slotDuration;\n return merged.some(([from, to]) => start < to && end > from);\n }\n );\n}\n","import {\n CalendarEvent,\n CalendarState,\n CalendarConfig,\n CalendarActions,\n CalendarViewModel,\n CalendarPane,\n CategoryColorMap,\n} from './types';\nimport {\n generateCalendarDates,\n getEventsForDate,\n generateYears,\n getMonthYearText,\n formatDateForDisplay,\n mergeCategoryColors,\n getCategoryColor,\n MONTHS,\n DAYS,\n} from './utils';\n\nexport class CalendarEngine {\n private state: CalendarState;\n private config: CalendarConfig;\n private listeners: Set<() => void> = new Set();\n private categoryColors: CategoryColorMap;\n\n constructor(config: CalendarConfig) {\n this.config = config;\n this.categoryColors = mergeCategoryColors(config.categoryColors);\n\n const initialDate = config.initialDate || new Date();\n this.state = {\n currentYear: initialDate.getFullYear(),\n currentMonth: initialDate.getMonth(),\n currentDate: initialDate.getDate(),\n selectedDate: null,\n selectedDayIndex: null,\n tasks: [],\n };\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /**\n * Notify all listeners of state changes\n */\n private notify(): void {\n this.listeners.forEach(listener => listener());\n }\n\n /**\n * Get current state\n */\n getState(): CalendarState {\n return { ...this.state };\n }\n\n /**\n * Get view model with computed properties\n */\n getViewModel(): CalendarViewModel {\n const panes: CalendarPane[] = Array.from(\n { length: Math.max(1, this.config.monthCount ?? 1) },\n (_, offset) => {\n const anchor = new Date(\n this.state.currentYear,\n this.state.currentMonth + offset,\n 1\n );\n const year = anchor.getFullYear();\n const month = anchor.getMonth();\n\n return {\n year,\n month,\n monthAndYearText: getMonthYearText(year, month),\n calendarDates: generateCalendarDates(\n year,\n month,\n this.config.events,\n this.config.weekStartsOn\n ),\n };\n }\n );\n\n return {\n ...this.state,\n months: MONTHS,\n days: DAYS,\n years: generateYears(this.config.minYear, this.config.maxYear),\n monthAndYearText: panes[0].monthAndYearText,\n scheduleDay: this.state.selectedDate\n ? formatDateForDisplay(this.state.selectedDate)\n : '',\n panes,\n calendarDates: panes[0].calendarDates,\n };\n }\n\n /**\n * Get actions object\n */\n getActions(): CalendarActions {\n return {\n next: this.next.bind(this),\n previous: this.previous.bind(this),\n jump: this.jump.bind(this),\n goToToday: this.goToToday.bind(this),\n isCurrentMonth: this.isCurrentMonth.bind(this),\n selectDate: this.selectDate.bind(this),\n updateTasks: this.updateTasks.bind(this),\n };\n }\n\n /**\n * Navigate to next month\n */\n private next(): void {\n if (this.state.currentMonth === 11) {\n this.state.currentMonth = 0;\n this.state.currentYear++;\n } else {\n this.state.currentMonth++;\n }\n\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Navigate to previous month\n */\n private previous(): void {\n if (this.state.currentMonth === 0) {\n this.state.currentMonth = 11;\n this.state.currentYear--;\n } else {\n this.state.currentMonth--;\n }\n\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Jump to specific month and year\n */\n private jump(year: number, month: number): void {\n this.state.currentYear = year;\n this.state.currentMonth = month;\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Navigate to current month (today)\n */\n private goToToday(): void {\n const today = new Date();\n this.state.currentYear = today.getFullYear();\n this.state.currentMonth = today.getMonth();\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Check if currently viewing today's month\n */\n private isCurrentMonth(): boolean {\n const today = new Date();\n return (\n this.state.currentYear === today.getFullYear() &&\n this.state.currentMonth === today.getMonth()\n );\n }\n\n /**\n * Select a specific date\n */\n private selectDate(date: Date, dayIndex?: number): void {\n this.state.selectedDate = date;\n this.state.selectedDayIndex = dayIndex ?? null;\n this.state.currentDate = date.getDate();\n this.state.currentMonth = date.getMonth();\n this.state.currentYear = date.getFullYear();\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Update tasks for the currently selected date\n */\n private updateTasks(): void {\n if (!this.state.selectedDate) {\n this.state.tasks = [];\n return;\n }\n\n this.state.tasks = getEventsForDate(\n this.config.events,\n this.state.selectedDate\n );\n }\n\n /**\n * Update events configuration\n */\n updateEvents(events: CalendarEvent[]): void {\n this.config.events = events;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Handle date cell click\n */\n handleDateClick(date: Date, dayIndex?: number): void {\n this.selectDate(date, dayIndex);\n }\n\n /**\n * Check if date has events\n */\n hasEventsForDate(date: Date): boolean {\n return getEventsForDate(this.config.events, date).length > 0;\n }\n\n /**\n * Get events for a specific date\n */\n getEventsForDate(date: Date): CalendarEvent[] {\n return getEventsForDate(this.config.events, date);\n }\n\n /**\n * Clear selected date\n */\n clearSelection(): void {\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.state.tasks = [];\n this.notify();\n }\n\n /**\n * Get category color (with custom colors support)\n */\n getCategoryColor(category?: string): string {\n if (!category) return '#fc8917';\n return getCategoryColor(category, this.categoryColors);\n }\n\n /**\n * Update category colors dynamically\n */\n updateCategoryColors(colors: CategoryColorMap): void {\n this.categoryColors = mergeCategoryColors(colors);\n this.notify();\n }\n\n /**\n * Get all category colors\n */\n getCategoryColors(): CategoryColorMap {\n return { ...this.categoryColors };\n }\n\n /**\n * Register a new category with color\n */\n registerCategory(name: string, color: string): void {\n this.categoryColors[name] = color;\n this.notify();\n }\n\n /**\n * Destroy the engine and cleanup listeners\n */\n destroy(): void {\n this.listeners.clear();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/core/index.ts","../../src/core/utils.ts","../../src/core/calendar-engine.ts"],"sourcesContent":["export * from './types';\nexport * from './utils';\nexport { CalendarEngine } from './calendar-engine';\n\n// Re-export commonly used utilities\nexport {\n MONTHS,\n MONTHS_FULL,\n DAYS,\n normalizeDate,\n isSameDay,\n isToday,\n generateYears,\n getEventsForDate,\n hasEvents,\n generateCalendarDates,\n getCellClasses,\n formatDateForDisplay,\n getMonthYearText,\n} from './utils';\n","import { CalendarEvent, CalendarDate, CategoryColorMap } from './types';\n\nexport const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\nexport const MONTHS_FULL = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nexport const DAYS = [\n 'Sunday',\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n];\n\nexport function normalizeDate(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);\n}\n\nexport function isSameDay(date1: Date, date2: Date): boolean {\n return normalizeDate(date1).getTime() === normalizeDate(date2).getTime();\n}\n\nexport function isToday(date: Date): boolean {\n return isSameDay(date, new Date());\n}\n\nexport function generateYears(minYear?: number, maxYear?: number): number[] {\n const currentYear = new Date().getFullYear();\n const min = minYear ?? currentYear - 30;\n const max = maxYear ?? currentYear + 10;\n\n return Array.from({ length: max - min + 1 }, (_, i) => min + i);\n}\n\n// endDate is inclusive, matching the endDate cal-availability-select emits, so\n// a selection can be handed straight back as one event.\n// \"09:00-12:00,13:00-17:00\" to half-open [start, end) minute intervals.\n// Throws rather than dropping a bad range: a vendor who mistypes their opening\n// hours must hear about it, not quietly stop selling the afternoon.\nexport function parseHourRanges(\n value: string,\n slotDuration: number\n): Array<[number, number]> {\n const parts = value\n .split(',')\n .map(part => part.trim())\n .filter(Boolean);\n\n if (parts.length === 0) {\n throw new Error(\n `<kal-calendar> available-hours is empty. Omit the attribute to allow ` +\n `every hour, or name at least one HH:MM-HH:MM range.`\n );\n }\n\n const ranges = parts.map((part): [number, number] => {\n const halves = part.split('-');\n if (halves.length !== 2) {\n throw new Error(\n `<kal-calendar> available-hours range \"${part}\" is not HH:MM-HH:MM.`\n );\n }\n\n const [start, end] = halves.map(half => parseTimeToMinutes(half.trim()));\n if (start === null || end === null) {\n throw new Error(\n `<kal-calendar> available-hours range \"${part}\" has an unreadable ` +\n `time. Use 24-hour HH:MM.`\n );\n }\n if (start >= end) {\n throw new Error(\n `<kal-calendar> available-hours range \"${part}\" starts at or after ` +\n `it ends.`\n );\n }\n if (start % slotDuration !== 0 || end % slotDuration !== 0) {\n throw new Error(\n `<kal-calendar> available-hours range \"${part}\" does not land on a ` +\n `${slotDuration}-minute slot boundary.`\n );\n }\n\n return [start, end];\n });\n\n if (mergeIntervals(ranges).length !== ranges.length) {\n throw new Error(\n `<kal-calendar> available-hours ranges overlap or touch: \"${value}\". ` +\n `Write each bookable window once.`\n );\n }\n\n return ranges;\n}\n\n// Both bounds inclusive; either may be null to leave that end unbounded.\nexport function isDateWithinWindow(\n date: Date,\n min: Date | null,\n max: Date | null\n): boolean {\n const target = normalizeDate(date).getTime();\n\n if (min && target < normalizeDate(min).getTime()) return false;\n if (max && target > normalizeDate(max).getTime()) return false;\n\n return true;\n}\n\n// `days` holds getDay() values, 0 = Sunday. Null means every day is allowed.\nexport function isDayAllowed(date: Date, days: number[] | null): boolean {\n return days === null || days.includes(date.getDay());\n}\n\nexport function eventCoversDate(event: CalendarEvent, date: Date): boolean {\n const start = normalizeDate(new Date(event.date)).getTime();\n const target = normalizeDate(date).getTime();\n if (Number.isNaN(start)) return false;\n\n if (event.endDate === undefined || event.endDate === null) {\n return start === target;\n }\n\n const end = normalizeDate(new Date(event.endDate)).getTime();\n if (Number.isNaN(end)) {\n throw new Error(\n `<kal-calendar> event ${event.id} has an unreadable endDate: ` +\n `${String(event.endDate)}.`\n );\n }\n if (end < start) {\n throw new Error(\n `<kal-calendar> event ${event.id} has an endDate before its date: ` +\n `${String(event.endDate)} < ${String(event.date)}.`\n );\n }\n\n return target >= start && target <= end;\n}\n\nexport function getEventsForDate(\n events: CalendarEvent[],\n date: Date\n): CalendarEvent[] {\n return events.filter(event => eventCoversDate(event, date));\n}\n\nexport function hasEvents(events: CalendarEvent[], date: Date): boolean {\n return getEventsForDate(events, date).length > 0;\n}\n\nexport function generateCalendarDates(\n year: number,\n month: number,\n events: CalendarEvent[] = [],\n weekStartsOn: 0 | 1 = 0\n): CalendarDate[][] {\n const firstDay = new Date(year, month, 1);\n const lastDay = new Date(year, month + 1, 0);\n const daysInMonth = lastDay.getDate();\n const prevMonthLastDay = new Date(year, month, 0).getDate();\n\n let firstDayOfWeek = firstDay.getDay();\n if (weekStartsOn === 1) {\n firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;\n }\n\n const dates: CalendarDate[][] = [];\n let day = 1;\n let nextMonthDay = 1;\n\n for (let week = 0; week < 6; week++) {\n const weekDates: CalendarDate[] = [];\n\n for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {\n if (week === 0 && dayOfWeek < firstDayOfWeek) {\n // Previous month days\n const prevDay = prevMonthLastDay - firstDayOfWeek + dayOfWeek + 1;\n const prevDate = new Date(year, month - 1, prevDay);\n const dateEvents = getEventsForDate(events, prevDate);\n weekDates.push({\n date: prevDate,\n isCurrentMonth: false,\n isToday: isToday(prevDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n } else if (day > daysInMonth) {\n // Next month days\n const nextDate = new Date(year, month + 1, nextMonthDay);\n const dateEvents = getEventsForDate(events, nextDate);\n weekDates.push({\n date: nextDate,\n isCurrentMonth: false,\n isToday: isToday(nextDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n nextMonthDay++;\n } else {\n // Current month days\n const currentDate = new Date(year, month, day);\n const dateEvents = getEventsForDate(events, currentDate);\n weekDates.push({\n date: currentDate,\n isCurrentMonth: true,\n isToday: isToday(currentDate),\n hasEvents: dateEvents.length > 0,\n events: dateEvents,\n });\n day++;\n }\n }\n\n dates.push(weekDates);\n }\n\n return dates;\n}\n\nexport function getCellClasses(calendarDate: CalendarDate): string[] {\n const classes: string[] = [];\n\n if (!calendarDate.isCurrentMonth) {\n classes.push('calendar-cell-other-month');\n }\n\n if (calendarDate.isToday) {\n classes.push('calendar-cell-today');\n }\n\n if (calendarDate.hasEvents) {\n classes.push('calendar-cell-has-event');\n }\n\n return classes;\n}\n\nexport function formatDateForDisplay(date: Date): string {\n return `${DAYS[date.getDay()]} ${date.getDate()}`;\n}\n\nexport function getMonthYearText(year: number, month: number): string {\n return `${MONTHS_FULL[month]} ${year}`;\n}\n\nexport function formatTimeRange(event: CalendarEvent): string {\n if (event.allDay || (!event.startTime && !event.endTime)) {\n return 'All day';\n }\n\n if (event.startTime && event.endTime) {\n return `${event.startTime} - ${event.endTime}`;\n }\n\n if (event.startTime) {\n return `${event.startTime}`;\n }\n\n return '';\n}\n\nexport function formatAttendees(attendees?: string[]): string {\n if (!attendees || attendees.length === 0) return '';\n\n if (attendees.length === 1) return attendees[0];\n if (attendees.length === 2) return attendees.join(' and ');\n\n return `${attendees.slice(0, -1).join(', ')}, and ${attendees[attendees.length - 1]}`;\n}\n\nexport function sortEventsByTime(events: CalendarEvent[]): CalendarEvent[] {\n return [...events].sort((a, b) => {\n const aAllDay = a.allDay || (!a.startTime && !a.endTime);\n const bAllDay = b.allDay || (!b.startTime && !b.endTime);\n\n if (aAllDay && !bAllDay) return -1;\n if (!aAllDay && bAllDay) return 1;\n\n if (!a.startTime || !b.startTime) return 0;\n return a.startTime.localeCompare(b.startTime);\n });\n}\n\nexport const DEFAULT_CATEGORY_COLORS: CategoryColorMap = {\n work: '#3b82f6',\n personal: '#8b5cf6',\n meeting: '#10b981',\n deadline: '#ef4444',\n appointment: '#f59e0b',\n other: '#6b7280',\n};\n\n// Dynamic function that accepts custom colors\nexport function getDefaultEventColor(\n category?: string,\n customColors?: CategoryColorMap\n): string {\n const colorMap = customColors || DEFAULT_CATEGORY_COLORS;\n\n if (category && colorMap[category]) {\n return colorMap[category];\n }\n\n // Default fallback color\n return '#fc8917';\n}\n\n// Helper to merge custom colors with defaults\nexport function mergeCategoryColors(\n customColors?: CategoryColorMap\n): CategoryColorMap {\n return {\n ...DEFAULT_CATEGORY_COLORS,\n ...customColors,\n };\n}\n\n// Validate if a color is a valid hex color\nexport function isValidHexColor(color: string): boolean {\n return /^#([0-9A-F]{3}){1,2}$/i.test(color);\n}\n\n// Get color for a category with validation\nexport function getCategoryColor(\n category: string,\n customColors?: CategoryColorMap\n): string {\n const colorMap = mergeCategoryColors(customColors);\n const color = colorMap[category] || colorMap.other || '#fc8917';\n\n return isValidHexColor(color) ? color : '#fc8917';\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n// Escape a value for interpolation into HTML text or a quoted attribute\nexport function escapeHtml(value: unknown): string {\n if (value === null || value === undefined) return '';\n\n return String(value).replace(/[&<>\"']/g, char => HTML_ESCAPES[char]);\n}\n\n// Reduce a caller-supplied value to a single safe CSS class token\nexport function slugifyToken(value: string): string {\n return String(value)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n// Allow http(s), mailto and relative URLs; anything else (javascript:, data:,\n// vbscript:) collapses to a harmless anchor\nexport function safeUrl(value: string): string {\n // Browsers ignore control characters when resolving a scheme, so\n // `java\\0script:` and `java\\tscript:` both navigate. Strip them before\n // testing rather than after.\n const normalized = String(value)\n .split('')\n .filter(char => char.charCodeAt(0) > 0x20)\n .join('');\n\n if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;\n\n // Relative URLs have no scheme at all — a colon before the first slash,\n // question mark or hash means someone is naming one\n const schemeless = !/^[^/?#]*:/.test(normalized);\n\n return schemeless ? normalized : '#';\n}\n\n// Accept hex colors and bare CSS color keywords; reject anything that could\n// terminate the declaration or escape the style attribute\nexport function safeColor(value: string): string {\n const trimmed = String(value).trim();\n\n return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed)\n ? trimmed\n : '#3b82f6';\n}\n\nexport const MINUTES_PER_DAY = 1440;\nexport const DEFAULT_SLOT_DURATION = 60;\n\nexport function parseTimeToMinutes(time: unknown): number | null {\n if (typeof time !== 'string') return null;\n\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(time.trim());\n if (!match) return null;\n\n const hours = Number(match[1]);\n const mins = Number(match[2]);\n if (hours > 24 || mins > 59 || (hours === 24 && mins > 0)) return null;\n\n return hours * 60 + mins;\n}\n\nexport function formatMinutes(total: number): string {\n const hours = Math.floor(total / 60);\n const mins = total % 60;\n return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;\n}\n\n// Absolute [start, end) in minutes. An end at or before the start is on the\n// following day, so a booking that crosses midnight is one interval rather than\n// two half-days. An unreadable time occupies the whole day — a booking surface\n// that cannot parse a time must not offer the slot.\nexport function eventInterval(\n event: CalendarEvent,\n slotDuration: number,\n onDay: Date\n): [number, number] | null {\n const day = normalizeDate(onDay).getTime();\n if (Number.isNaN(day)) return null;\n\n const base = day / 60000;\n const wholeDay: [number, number] = [base, base + MINUTES_PER_DAY];\n\n if (event.allDay || !event.startTime) return wholeDay;\n\n const from = parseTimeToMinutes(event.startTime);\n if (from === null) return wholeDay;\n\n if (event.endTime === undefined || event.endTime === null) {\n return [base + from, base + from + slotDuration];\n }\n\n const to = parseTimeToMinutes(event.endTime);\n if (to === null) return wholeDay;\n\n return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];\n}\n\nexport function mergeIntervals(\n intervals: Array<[number, number]>\n): Array<[number, number]> {\n const sorted = [...intervals].sort((a, b) => a[0] - b[0]);\n\n return sorted.reduce<Array<[number, number]>>((merged, [start, end]) => {\n const last = merged[merged.length - 1];\n if (last && start <= last[1]) last[1] = Math.max(last[1], end);\n else merged.push([start, end]);\n return merged;\n }, []);\n}\n\n// Which of the day's slots any booking overlaps. Events from the previous day\n// belong in `events` so a booking crossing midnight marks the morning it runs into.\nexport function bookedSlots(\n events: CalendarEvent[],\n date: Date,\n slotDuration: number = DEFAULT_SLOT_DURATION\n): boolean[] {\n const base = normalizeDate(date).getTime() / 60000;\n\n const previousDay = new Date(date);\n previousDay.setDate(previousDay.getDate() - 1);\n\n // A span's times repeat on each day it covers, so the window is anchored to\n // the day being drawn rather than the day the span began\n const intervals: Array<[number, number]> = [];\n for (const event of new Set(events)) {\n for (const day of [previousDay, date]) {\n if (!eventCoversDate(event, day)) continue;\n const interval = eventInterval(event, slotDuration, day);\n if (interval) intervals.push(interval);\n }\n }\n\n const merged = mergeIntervals(intervals);\n\n return Array.from(\n { length: Math.floor(MINUTES_PER_DAY / slotDuration) },\n (_, index) => {\n const start = base + index * slotDuration;\n const end = start + slotDuration;\n return merged.some(([from, to]) => start < to && end > from);\n }\n );\n}\n","import {\n CalendarEvent,\n CalendarState,\n CalendarConfig,\n CalendarActions,\n CalendarViewModel,\n CalendarPane,\n CategoryColorMap,\n} from './types';\nimport {\n generateCalendarDates,\n getEventsForDate,\n generateYears,\n getMonthYearText,\n formatDateForDisplay,\n mergeCategoryColors,\n getCategoryColor,\n MONTHS,\n DAYS,\n} from './utils';\n\nexport class CalendarEngine {\n private state: CalendarState;\n private config: CalendarConfig;\n private listeners: Set<() => void> = new Set();\n private categoryColors: CategoryColorMap;\n\n constructor(config: CalendarConfig) {\n this.config = config;\n this.categoryColors = mergeCategoryColors(config.categoryColors);\n\n const initialDate = config.initialDate || new Date();\n this.state = {\n currentYear: initialDate.getFullYear(),\n currentMonth: initialDate.getMonth(),\n currentDate: initialDate.getDate(),\n selectedDate: null,\n selectedDayIndex: null,\n tasks: [],\n };\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /**\n * Notify all listeners of state changes\n */\n private notify(): void {\n this.listeners.forEach(listener => listener());\n }\n\n /**\n * Get current state\n */\n getState(): CalendarState {\n return { ...this.state };\n }\n\n /**\n * Get view model with computed properties\n */\n getViewModel(): CalendarViewModel {\n const panes: CalendarPane[] = Array.from(\n { length: Math.max(1, this.config.monthCount ?? 1) },\n (_, offset) => {\n const anchor = new Date(\n this.state.currentYear,\n this.state.currentMonth + offset,\n 1\n );\n const year = anchor.getFullYear();\n const month = anchor.getMonth();\n\n return {\n year,\n month,\n monthAndYearText: getMonthYearText(year, month),\n calendarDates: generateCalendarDates(\n year,\n month,\n this.config.events,\n this.config.weekStartsOn\n ),\n };\n }\n );\n\n return {\n ...this.state,\n months: MONTHS,\n days: DAYS,\n years: generateYears(this.config.minYear, this.config.maxYear),\n monthAndYearText: panes[0].monthAndYearText,\n scheduleDay: this.state.selectedDate\n ? formatDateForDisplay(this.state.selectedDate)\n : '',\n panes,\n calendarDates: panes[0].calendarDates,\n };\n }\n\n /**\n * Get actions object\n */\n getActions(): CalendarActions {\n return {\n next: this.next.bind(this),\n previous: this.previous.bind(this),\n jump: this.jump.bind(this),\n goToToday: this.goToToday.bind(this),\n isCurrentMonth: this.isCurrentMonth.bind(this),\n selectDate: this.selectDate.bind(this),\n updateTasks: this.updateTasks.bind(this),\n };\n }\n\n /**\n * Navigate to next month\n */\n private next(): void {\n if (this.state.currentMonth === 11) {\n this.state.currentMonth = 0;\n this.state.currentYear++;\n } else {\n this.state.currentMonth++;\n }\n\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Navigate to previous month\n */\n private previous(): void {\n if (this.state.currentMonth === 0) {\n this.state.currentMonth = 11;\n this.state.currentYear--;\n } else {\n this.state.currentMonth--;\n }\n\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Jump to specific month and year\n */\n private jump(year: number, month: number): void {\n this.state.currentYear = year;\n this.state.currentMonth = month;\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Navigate to current month (today)\n */\n private goToToday(): void {\n const today = new Date();\n this.state.currentYear = today.getFullYear();\n this.state.currentMonth = today.getMonth();\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Check if currently viewing today's month\n */\n private isCurrentMonth(): boolean {\n const today = new Date();\n return (\n this.state.currentYear === today.getFullYear() &&\n this.state.currentMonth === today.getMonth()\n );\n }\n\n /**\n * Select a specific date\n */\n private selectDate(date: Date, dayIndex?: number): void {\n this.state.selectedDate = date;\n this.state.selectedDayIndex = dayIndex ?? null;\n this.state.currentDate = date.getDate();\n this.state.currentMonth = date.getMonth();\n this.state.currentYear = date.getFullYear();\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Update tasks for the currently selected date\n */\n private updateTasks(): void {\n if (!this.state.selectedDate) {\n this.state.tasks = [];\n return;\n }\n\n this.state.tasks = getEventsForDate(\n this.config.events,\n this.state.selectedDate\n );\n }\n\n /**\n * Update events configuration\n */\n updateEvents(events: CalendarEvent[]): void {\n this.config.events = events;\n this.updateTasks();\n this.notify();\n }\n\n /**\n * Handle date cell click\n */\n handleDateClick(date: Date, dayIndex?: number): void {\n this.selectDate(date, dayIndex);\n }\n\n /**\n * Check if date has events\n */\n hasEventsForDate(date: Date): boolean {\n return getEventsForDate(this.config.events, date).length > 0;\n }\n\n /**\n * Get events for a specific date\n */\n getEventsForDate(date: Date): CalendarEvent[] {\n return getEventsForDate(this.config.events, date);\n }\n\n /**\n * Clear selected date\n */\n clearSelection(): void {\n this.state.selectedDate = null;\n this.state.selectedDayIndex = null;\n this.state.tasks = [];\n this.notify();\n }\n\n /**\n * Get category color (with custom colors support)\n */\n getCategoryColor(category?: string): string {\n if (!category) return '#fc8917';\n return getCategoryColor(category, this.categoryColors);\n }\n\n /**\n * Update category colors dynamically\n */\n updateCategoryColors(colors: CategoryColorMap): void {\n this.categoryColors = mergeCategoryColors(colors);\n this.notify();\n }\n\n /**\n * Get all category colors\n */\n getCategoryColors(): CategoryColorMap {\n return { ...this.categoryColors };\n }\n\n /**\n * Register a new category with color\n */\n registerCategory(name: string, color: string): void {\n this.categoryColors[name] = color;\n this.notify();\n }\n\n /**\n * Destroy the engine and cleanup listeners\n */\n destroy(): void {\n this.listeners.clear();\n }\n}\n"],"mappings":"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","names":[]}
@@ -51,6 +51,54 @@ function generateYears(minYear, maxYear) {
51
51
  const max = maxYear ?? currentYear + 10;
52
52
  return Array.from({ length: max - min + 1 }, (_, i) => min + i);
53
53
  }
54
+ function parseHourRanges(value, slotDuration) {
55
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
56
+ if (parts.length === 0) {
57
+ throw new Error(
58
+ `<kal-calendar> available-hours is empty. Omit the attribute to allow every hour, or name at least one HH:MM-HH:MM range.`
59
+ );
60
+ }
61
+ const ranges = parts.map((part) => {
62
+ const halves = part.split("-");
63
+ if (halves.length !== 2) {
64
+ throw new Error(
65
+ `<kal-calendar> available-hours range "${part}" is not HH:MM-HH:MM.`
66
+ );
67
+ }
68
+ const [start, end] = halves.map((half) => parseTimeToMinutes(half.trim()));
69
+ if (start === null || end === null) {
70
+ throw new Error(
71
+ `<kal-calendar> available-hours range "${part}" has an unreadable time. Use 24-hour HH:MM.`
72
+ );
73
+ }
74
+ if (start >= end) {
75
+ throw new Error(
76
+ `<kal-calendar> available-hours range "${part}" starts at or after it ends.`
77
+ );
78
+ }
79
+ if (start % slotDuration !== 0 || end % slotDuration !== 0) {
80
+ throw new Error(
81
+ `<kal-calendar> available-hours range "${part}" does not land on a ${slotDuration}-minute slot boundary.`
82
+ );
83
+ }
84
+ return [start, end];
85
+ });
86
+ if (mergeIntervals(ranges).length !== ranges.length) {
87
+ throw new Error(
88
+ `<kal-calendar> available-hours ranges overlap or touch: "${value}". Write each bookable window once.`
89
+ );
90
+ }
91
+ return ranges;
92
+ }
93
+ function isDateWithinWindow(date, min, max) {
94
+ const target = normalizeDate(date).getTime();
95
+ if (min && target < normalizeDate(min).getTime()) return false;
96
+ if (max && target > normalizeDate(max).getTime()) return false;
97
+ return true;
98
+ }
99
+ function isDayAllowed(date, days) {
100
+ return days === null || days.includes(date.getDay());
101
+ }
54
102
  function eventCoversDate(event, date) {
55
103
  const start = normalizeDate(new Date(event.date)).getTime();
56
104
  const target = normalizeDate(date).getTime();
@@ -559,12 +607,15 @@ export {
559
607
  getEventsForDate,
560
608
  getMonthYearText,
561
609
  hasEvents,
610
+ isDateWithinWindow,
611
+ isDayAllowed,
562
612
  isSameDay,
563
613
  isToday,
564
614
  isValidHexColor,
565
615
  mergeCategoryColors,
566
616
  mergeIntervals,
567
617
  normalizeDate,
618
+ parseHourRanges,
568
619
  parseTimeToMinutes,
569
620
  safeColor,
570
621
  safeUrl,