kalendly 0.3.0 → 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 +155 -20
- package/dist/core/index.d.mts +16 -2
- package/dist/core/index.d.ts +16 -2
- package/dist/core/index.js +153 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +142 -5
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.d.mts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +297 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +286 -31
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +274 -30
- package/dist/index.umd.js.map +1 -1
- package/dist/styles/calendar.css +33 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -333,19 +333,24 @@ document.querySelector('kal-calendar').theme = {
|
|
|
333
333
|
|
|
334
334
|
Primitives are set as HTML attributes:
|
|
335
335
|
|
|
336
|
-
| Attribute | Type
|
|
337
|
-
| ----------------------- |
|
|
338
|
-
| `heading` | `string`
|
|
339
|
-
| `title` | `string`
|
|
340
|
-
| `initial-date` | `string`
|
|
341
|
-
| `months` | `"1"\|"2"`
|
|
342
|
-
| `min-year` | `string`
|
|
343
|
-
| `max-year` | `string`
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
336
|
+
| Attribute | Type | Default | Description |
|
|
337
|
+
| ----------------------- | ----------------- | ---------------- | -------------------------------------------------- |
|
|
338
|
+
| `heading` | `string` | — | Calendar heading |
|
|
339
|
+
| `title` | `string` | — | **Deprecated** — use `heading` |
|
|
340
|
+
| `initial-date` | `string` | today | ISO date string for initial view |
|
|
341
|
+
| `months` | `"1"\|"2"` | `"1"` | Render two months side by side |
|
|
342
|
+
| `min-year` | `string` | currentYear - 30 | Minimum year in picker |
|
|
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"` |
|
|
348
|
+
| `week-starts-on` | `"0"\|"1"` | `"0"` | Week start: 0 = Sunday, 1 = Monday |
|
|
349
|
+
| `use-short-month-names` | `string` | — | Present = use abbreviated month names |
|
|
350
|
+
| `availability-mode` | `"day"\|"time"` | — | Hides event details; shows booked/free cells |
|
|
351
|
+
| `slot-duration` | `string` (number) | `"60"` | Time-grid granularity in minutes; must divide 1440 |
|
|
352
|
+
| `selectable` | `"range"` | — | Enables day/slot selection (requires avail mode) |
|
|
353
|
+
| `loading` | `boolean` (flag) | — | Present = render skeleton shimmer cells |
|
|
349
354
|
|
|
350
355
|
## Properties
|
|
351
356
|
|
|
@@ -370,11 +375,12 @@ Rich objects are set as JS properties (not attributes):
|
|
|
370
375
|
|
|
371
376
|
## Custom Events
|
|
372
377
|
|
|
373
|
-
| Event | `detail` shape | Description
|
|
374
|
-
| ------------------------- | ---------------------------------------------------------------------- |
|
|
375
|
-
| `cal-date-select` | `{ date: Date, events: CalendarEvent[] }` | User clicked a date (normal mode)
|
|
376
|
-
| `cal-month-change` | `{ year: number, month: number }` | Fires **before** the new month renders
|
|
377
|
-
| `cal-availability-select` | `{ startDate: Date, endDate: Date }` or `{ date, startTime, endTime }` | Day/slot selected in availability mode
|
|
378
|
+
| Event | `detail` shape | Description |
|
|
379
|
+
| ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
380
|
+
| `cal-date-select` | `{ date: Date, events: CalendarEvent[] }` | User clicked a date (normal mode) |
|
|
381
|
+
| `cal-month-change` | `{ year: number, month: number }` | Fires **before** the new month renders |
|
|
382
|
+
| `cal-availability-select` | `{ startDate: Date, endDate: Date }` or `{ date, startTime, endTime }` | Day/slot selected in availability mode |
|
|
383
|
+
| `cal-slot-select` | `{ date: Date, startTime: string, endTime: string, booked: boolean }` | Any time slot clicked, whether or not `selectable` is set |
|
|
378
384
|
|
|
379
385
|
All events bubble and are composed (cross Shadow DOM boundaries).
|
|
380
386
|
|
|
@@ -479,13 +485,115 @@ cal.events = [
|
|
|
479
485
|
<kal-calendar availability-mode="time"></kal-calendar>
|
|
480
486
|
```
|
|
481
487
|
|
|
482
|
-
Clicking a day opens a popup with a
|
|
488
|
+
Clicking a day opens a popup with a grid of slots. Each shows only "Booked" or "Available" — no event name or organiser is ever rendered. A slot is booked when any booking overlaps it.
|
|
489
|
+
|
|
490
|
+
#### Slot length
|
|
491
|
+
|
|
492
|
+
`slot-duration` sets the grid granularity in minutes. It must divide 1440 evenly; anything else falls back to 60 with a console warning.
|
|
493
|
+
|
|
494
|
+
```html
|
|
495
|
+
<kal-calendar availability-mode="time" slot-duration="30"></kal-calendar>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The grid renders `1440 / slot-duration` slots, and `cal-availability-select` emits times on that granularity — half-hour slots produce half-hour selections.
|
|
499
|
+
|
|
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.
|
|
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
|
+
|
|
536
|
+
#### Bookings that cross midnight
|
|
537
|
+
|
|
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:
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
{ id: 1, date: '2026-03-15', startTime: '22:00', endTime: '06:00' }
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
That marks 22:00–24:00 on 15 March and 00:00–06:00 on the 16th. Each day's grid shows the portion of any booking falling on that day.
|
|
545
|
+
|
|
546
|
+
#### A booking with no end time
|
|
547
|
+
|
|
548
|
+
An event with a `startTime` and no `endTime` occupies **one slot**, and the library warns once naming the event. The supported fix is an end time in your data — the duration is a fallback, not a feature.
|
|
549
|
+
|
|
550
|
+
Overlapping bookings merge rather than double-count, so a 09:00–17:00 meeting and a 17:30–22:00 class on the same day mark 09:00–22:00 booked between them.
|
|
483
551
|
|
|
484
552
|
<div align="center">
|
|
485
553
|
<img src="./docs/images/time.png" alt="Availability time view — day popup showing 24 hourly slots coloured red (booked) or green (available)"/>
|
|
486
554
|
<p><em>Time view: clicking a day opens an hourly grid — booked slots in red, available slots in green; no event details exposed</em></p>
|
|
487
555
|
</div>
|
|
488
556
|
|
|
557
|
+
#### Reacting to a slot click
|
|
558
|
+
|
|
559
|
+
`cal-slot-select` fires on every slot click, the way `cal-date-select` fires for
|
|
560
|
+
every day — including booked slots, and whether or not `selectable` is set. Use it
|
|
561
|
+
to drive your own booking flow without turning on range selection:
|
|
562
|
+
|
|
563
|
+
```js
|
|
564
|
+
cal.addEventListener('cal-slot-select', e => {
|
|
565
|
+
const { date, startTime, endTime, booked } = e.detail;
|
|
566
|
+
if (booked) return showTakenMessage(startTime);
|
|
567
|
+
openBookingForm(date, startTime, endTime);
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`selectable="range"` still governs `cal-availability-select`, the three-click
|
|
572
|
+
range machine and the range highlighting. It also controls the cursor: slots only
|
|
573
|
+
show a pointer when the grid can actually be booked from.
|
|
574
|
+
|
|
575
|
+
#### Round-tripping a selection
|
|
576
|
+
|
|
577
|
+
`cal-availability-select` emits an inclusive `endDate`, so a saved selection goes
|
|
578
|
+
straight back as a single event — no expanding into one event per day:
|
|
579
|
+
|
|
580
|
+
```js
|
|
581
|
+
cal.addEventListener('cal-availability-select', async e => {
|
|
582
|
+
const { startDate, endDate } = e.detail;
|
|
583
|
+
const booking = await save({ startDate, endDate });
|
|
584
|
+
|
|
585
|
+
cal.events = [
|
|
586
|
+
...cal.events,
|
|
587
|
+
{
|
|
588
|
+
id: booking.id,
|
|
589
|
+
date: startDate,
|
|
590
|
+
endDate,
|
|
591
|
+
availabilityStatus: 'blocked',
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
489
597
|
### Selectable range
|
|
490
598
|
|
|
491
599
|
Add `selectable="range"` to let the user pick a free day or time slot:
|
|
@@ -567,6 +675,27 @@ cal.getEngine();
|
|
|
567
675
|
> `mailto:` and relative URLs; anything else becomes `#`. `color` accepts hex
|
|
568
676
|
> values and CSS colour keywords.
|
|
569
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
|
+
|
|
686
|
+
> **Multi-day events.** `endDate` is the last day of a span and is **inclusive** —
|
|
687
|
+
> `date: '2026-03-03', endDate: '2026-03-05'` covers three days. That matches the
|
|
688
|
+
> `endDate` `cal-availability-select` emits, so a selection can be handed straight
|
|
689
|
+
> back as one event. It deliberately differs from RFC 5545, whose all-day `DTEND`
|
|
690
|
+
> is exclusive; adjust if you map to iCalendar. An `endDate` before `date`, or one
|
|
691
|
+
> that cannot be read, throws and names the event.
|
|
692
|
+
>
|
|
693
|
+
> Times on a span repeat daily: `09:00`–`17:00` across three days means that
|
|
694
|
+
> window on each of the three, not one continuous block. A hall can hold a
|
|
695
|
+
> 09:00–17:00 meeting and a 17:30–22:00 class on overlapping days.
|
|
696
|
+
>
|
|
697
|
+
> `recurring` is declared on the type but not implemented — nothing reads it.
|
|
698
|
+
|
|
570
699
|
> **Custom values.** `status`, `category` and `priority` accept any string. An
|
|
571
700
|
> unrecognised value renders as an uppercased badge with a neutral fill, which
|
|
572
701
|
> you can style via `.badge.status-<your-value>` or recolour through
|
|
@@ -575,8 +704,10 @@ cal.getEngine();
|
|
|
575
704
|
```typescript
|
|
576
705
|
interface CalendarEvent {
|
|
577
706
|
id: string | number;
|
|
578
|
-
name: string;
|
|
579
707
|
date: string | Date;
|
|
708
|
+
name?: string; // event card title; omit it and no title renders
|
|
709
|
+
|
|
710
|
+
endDate?: string | Date; // last day of a span, inclusive
|
|
580
711
|
|
|
581
712
|
startTime?: string; // e.g. "09:00"
|
|
582
713
|
endTime?: string; // e.g. "10:00"
|
|
@@ -637,6 +768,8 @@ for. Override the `--calendar-*` layer — the primitives are internal.
|
|
|
637
768
|
--calendar-cell-hover: #f3f4f6;
|
|
638
769
|
--calendar-header-bg: #f8f9fa;
|
|
639
770
|
--calendar-selected-bg: #eff6ff;
|
|
771
|
+
--calendar-out-of-range-bg: #f3f4f6;
|
|
772
|
+
--calendar-out-of-range-fg: #6b7280;
|
|
640
773
|
--calendar-popup-bg: #fff;
|
|
641
774
|
--calendar-picker-bg: #fff;
|
|
642
775
|
--calendar-today-outline: #f7db04;
|
|
@@ -699,6 +832,8 @@ cal.theme = {
|
|
|
699
832
|
borderColor: '#e5e7eb',
|
|
700
833
|
todayOutline: '#fbbf24',
|
|
701
834
|
selectedBg: '#eff6ff',
|
|
835
|
+
outOfRangeBg: '#f3f4f6',
|
|
836
|
+
outOfRangeFg: '#6b7280',
|
|
702
837
|
headerBg: '#f8f9fa',
|
|
703
838
|
popupBg: '#ffffff',
|
|
704
839
|
pickerBg: '#ffffff',
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
+
endDate?: string | Date;
|
|
6
7
|
startTime?: string;
|
|
7
8
|
endTime?: string;
|
|
8
9
|
allDay?: boolean;
|
|
@@ -103,6 +104,8 @@ interface CalendarTheme {
|
|
|
103
104
|
borderColor?: string;
|
|
104
105
|
todayOutline?: string;
|
|
105
106
|
selectedBg?: string;
|
|
107
|
+
outOfRangeBg?: string;
|
|
108
|
+
outOfRangeFg?: string;
|
|
106
109
|
headerBg?: string;
|
|
107
110
|
popupBg?: string;
|
|
108
111
|
pickerBg?: string;
|
|
@@ -145,6 +148,10 @@ declare function normalizeDate(date: Date): Date;
|
|
|
145
148
|
declare function isSameDay(date1: Date, date2: Date): boolean;
|
|
146
149
|
declare function isToday(date: Date): boolean;
|
|
147
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;
|
|
154
|
+
declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
|
|
148
155
|
declare function getEventsForDate(events: CalendarEvent[], date: Date): CalendarEvent[];
|
|
149
156
|
declare function hasEvents(events: CalendarEvent[], date: Date): boolean;
|
|
150
157
|
declare function generateCalendarDates(year: number, month: number, events?: CalendarEvent[], weekStartsOn?: 0 | 1): CalendarDate[][];
|
|
@@ -163,6 +170,13 @@ declare function escapeHtml(value: unknown): string;
|
|
|
163
170
|
declare function slugifyToken(value: string): string;
|
|
164
171
|
declare function safeUrl(value: string): string;
|
|
165
172
|
declare function safeColor(value: string): string;
|
|
173
|
+
declare const MINUTES_PER_DAY = 1440;
|
|
174
|
+
declare const DEFAULT_SLOT_DURATION = 60;
|
|
175
|
+
declare function parseTimeToMinutes(time: unknown): number | null;
|
|
176
|
+
declare function formatMinutes(total: number): string;
|
|
177
|
+
declare function eventInterval(event: CalendarEvent, slotDuration: number, onDay: Date): [number, number] | null;
|
|
178
|
+
declare function mergeIntervals(intervals: Array<[number, number]>): Array<[number, number]>;
|
|
179
|
+
declare function bookedSlots(events: CalendarEvent[], date: Date, slotDuration?: number): boolean[];
|
|
166
180
|
|
|
167
181
|
declare class CalendarEngine {
|
|
168
182
|
private state;
|
|
@@ -260,4 +274,4 @@ declare class CalendarEngine {
|
|
|
260
274
|
destroy(): void;
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
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, MONTHS, MONTHS_FULL, escapeHtml, formatAttendees, formatDateForDisplay, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isSameDay, isToday, isValidHexColor, mergeCategoryColors, normalizeDate, 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 };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
+
endDate?: string | Date;
|
|
6
7
|
startTime?: string;
|
|
7
8
|
endTime?: string;
|
|
8
9
|
allDay?: boolean;
|
|
@@ -103,6 +104,8 @@ interface CalendarTheme {
|
|
|
103
104
|
borderColor?: string;
|
|
104
105
|
todayOutline?: string;
|
|
105
106
|
selectedBg?: string;
|
|
107
|
+
outOfRangeBg?: string;
|
|
108
|
+
outOfRangeFg?: string;
|
|
106
109
|
headerBg?: string;
|
|
107
110
|
popupBg?: string;
|
|
108
111
|
pickerBg?: string;
|
|
@@ -145,6 +148,10 @@ declare function normalizeDate(date: Date): Date;
|
|
|
145
148
|
declare function isSameDay(date1: Date, date2: Date): boolean;
|
|
146
149
|
declare function isToday(date: Date): boolean;
|
|
147
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;
|
|
154
|
+
declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
|
|
148
155
|
declare function getEventsForDate(events: CalendarEvent[], date: Date): CalendarEvent[];
|
|
149
156
|
declare function hasEvents(events: CalendarEvent[], date: Date): boolean;
|
|
150
157
|
declare function generateCalendarDates(year: number, month: number, events?: CalendarEvent[], weekStartsOn?: 0 | 1): CalendarDate[][];
|
|
@@ -163,6 +170,13 @@ declare function escapeHtml(value: unknown): string;
|
|
|
163
170
|
declare function slugifyToken(value: string): string;
|
|
164
171
|
declare function safeUrl(value: string): string;
|
|
165
172
|
declare function safeColor(value: string): string;
|
|
173
|
+
declare const MINUTES_PER_DAY = 1440;
|
|
174
|
+
declare const DEFAULT_SLOT_DURATION = 60;
|
|
175
|
+
declare function parseTimeToMinutes(time: unknown): number | null;
|
|
176
|
+
declare function formatMinutes(total: number): string;
|
|
177
|
+
declare function eventInterval(event: CalendarEvent, slotDuration: number, onDay: Date): [number, number] | null;
|
|
178
|
+
declare function mergeIntervals(intervals: Array<[number, number]>): Array<[number, number]>;
|
|
179
|
+
declare function bookedSlots(events: CalendarEvent[], date: Date, slotDuration?: number): boolean[];
|
|
166
180
|
|
|
167
181
|
declare class CalendarEngine {
|
|
168
182
|
private state;
|
|
@@ -260,4 +274,4 @@ declare class CalendarEngine {
|
|
|
260
274
|
destroy(): void;
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
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, MONTHS, MONTHS_FULL, escapeHtml, formatAttendees, formatDateForDisplay, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, hasEvents, isSameDay, isToday, isValidHexColor, mergeCategoryColors, normalizeDate, 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 };
|
package/dist/core/index.js
CHANGED
|
@@ -23,11 +23,17 @@ __export(core_exports, {
|
|
|
23
23
|
CalendarEngine: () => CalendarEngine,
|
|
24
24
|
DAYS: () => DAYS,
|
|
25
25
|
DEFAULT_CATEGORY_COLORS: () => DEFAULT_CATEGORY_COLORS,
|
|
26
|
+
DEFAULT_SLOT_DURATION: () => DEFAULT_SLOT_DURATION,
|
|
27
|
+
MINUTES_PER_DAY: () => MINUTES_PER_DAY,
|
|
26
28
|
MONTHS: () => MONTHS,
|
|
27
29
|
MONTHS_FULL: () => MONTHS_FULL,
|
|
30
|
+
bookedSlots: () => bookedSlots,
|
|
28
31
|
escapeHtml: () => escapeHtml,
|
|
32
|
+
eventCoversDate: () => eventCoversDate,
|
|
33
|
+
eventInterval: () => eventInterval,
|
|
29
34
|
formatAttendees: () => formatAttendees,
|
|
30
35
|
formatDateForDisplay: () => formatDateForDisplay,
|
|
36
|
+
formatMinutes: () => formatMinutes,
|
|
31
37
|
formatTimeRange: () => formatTimeRange,
|
|
32
38
|
generateCalendarDates: () => generateCalendarDates,
|
|
33
39
|
generateYears: () => generateYears,
|
|
@@ -37,11 +43,16 @@ __export(core_exports, {
|
|
|
37
43
|
getEventsForDate: () => getEventsForDate,
|
|
38
44
|
getMonthYearText: () => getMonthYearText,
|
|
39
45
|
hasEvents: () => hasEvents,
|
|
46
|
+
isDateWithinWindow: () => isDateWithinWindow,
|
|
47
|
+
isDayAllowed: () => isDayAllowed,
|
|
40
48
|
isSameDay: () => isSameDay,
|
|
41
49
|
isToday: () => isToday,
|
|
42
50
|
isValidHexColor: () => isValidHexColor,
|
|
43
51
|
mergeCategoryColors: () => mergeCategoryColors,
|
|
52
|
+
mergeIntervals: () => mergeIntervals,
|
|
44
53
|
normalizeDate: () => normalizeDate,
|
|
54
|
+
parseHourRanges: () => parseHourRanges,
|
|
55
|
+
parseTimeToMinutes: () => parseTimeToMinutes,
|
|
45
56
|
safeColor: () => safeColor,
|
|
46
57
|
safeUrl: () => safeUrl,
|
|
47
58
|
slugifyToken: () => slugifyToken,
|
|
@@ -102,12 +113,76 @@ function generateYears(minYear, maxYear) {
|
|
|
102
113
|
const max = maxYear ?? currentYear + 10;
|
|
103
114
|
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
104
115
|
}
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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];
|
|
110
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
|
+
}
|
|
164
|
+
function eventCoversDate(event, date) {
|
|
165
|
+
const start = normalizeDate(new Date(event.date)).getTime();
|
|
166
|
+
const target = normalizeDate(date).getTime();
|
|
167
|
+
if (Number.isNaN(start)) return false;
|
|
168
|
+
if (event.endDate === void 0 || event.endDate === null) {
|
|
169
|
+
return start === target;
|
|
170
|
+
}
|
|
171
|
+
const end = normalizeDate(new Date(event.endDate)).getTime();
|
|
172
|
+
if (Number.isNaN(end)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`<kal-calendar> event ${event.id} has an unreadable endDate: ${String(event.endDate)}.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (end < start) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`<kal-calendar> event ${event.id} has an endDate before its date: ${String(event.endDate)} < ${String(event.date)}.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return target >= start && target <= end;
|
|
183
|
+
}
|
|
184
|
+
function getEventsForDate(events, date) {
|
|
185
|
+
return events.filter((event) => eventCoversDate(event, date));
|
|
111
186
|
}
|
|
112
187
|
function hasEvents(events, date) {
|
|
113
188
|
return getEventsForDate(events, date).length > 0;
|
|
@@ -266,6 +341,68 @@ function safeColor(value) {
|
|
|
266
341
|
const trimmed = String(value).trim();
|
|
267
342
|
return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
|
|
268
343
|
}
|
|
344
|
+
var MINUTES_PER_DAY = 1440;
|
|
345
|
+
var DEFAULT_SLOT_DURATION = 60;
|
|
346
|
+
function parseTimeToMinutes(time) {
|
|
347
|
+
if (typeof time !== "string") return null;
|
|
348
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
349
|
+
if (!match) return null;
|
|
350
|
+
const hours = Number(match[1]);
|
|
351
|
+
const mins = Number(match[2]);
|
|
352
|
+
if (hours > 24 || mins > 59 || hours === 24 && mins > 0) return null;
|
|
353
|
+
return hours * 60 + mins;
|
|
354
|
+
}
|
|
355
|
+
function formatMinutes(total) {
|
|
356
|
+
const hours = Math.floor(total / 60);
|
|
357
|
+
const mins = total % 60;
|
|
358
|
+
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
359
|
+
}
|
|
360
|
+
function eventInterval(event, slotDuration, onDay) {
|
|
361
|
+
const day = normalizeDate(onDay).getTime();
|
|
362
|
+
if (Number.isNaN(day)) return null;
|
|
363
|
+
const base = day / 6e4;
|
|
364
|
+
const wholeDay = [base, base + MINUTES_PER_DAY];
|
|
365
|
+
if (event.allDay || !event.startTime) return wholeDay;
|
|
366
|
+
const from = parseTimeToMinutes(event.startTime);
|
|
367
|
+
if (from === null) return wholeDay;
|
|
368
|
+
if (event.endTime === void 0 || event.endTime === null) {
|
|
369
|
+
return [base + from, base + from + slotDuration];
|
|
370
|
+
}
|
|
371
|
+
const to = parseTimeToMinutes(event.endTime);
|
|
372
|
+
if (to === null) return wholeDay;
|
|
373
|
+
return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];
|
|
374
|
+
}
|
|
375
|
+
function mergeIntervals(intervals) {
|
|
376
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
377
|
+
return sorted.reduce((merged, [start, end]) => {
|
|
378
|
+
const last = merged[merged.length - 1];
|
|
379
|
+
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
|
380
|
+
else merged.push([start, end]);
|
|
381
|
+
return merged;
|
|
382
|
+
}, []);
|
|
383
|
+
}
|
|
384
|
+
function bookedSlots(events, date, slotDuration = DEFAULT_SLOT_DURATION) {
|
|
385
|
+
const base = normalizeDate(date).getTime() / 6e4;
|
|
386
|
+
const previousDay = new Date(date);
|
|
387
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
388
|
+
const intervals = [];
|
|
389
|
+
for (const event of new Set(events)) {
|
|
390
|
+
for (const day of [previousDay, date]) {
|
|
391
|
+
if (!eventCoversDate(event, day)) continue;
|
|
392
|
+
const interval = eventInterval(event, slotDuration, day);
|
|
393
|
+
if (interval) intervals.push(interval);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const merged = mergeIntervals(intervals);
|
|
397
|
+
return Array.from(
|
|
398
|
+
{ length: Math.floor(MINUTES_PER_DAY / slotDuration) },
|
|
399
|
+
(_, index) => {
|
|
400
|
+
const start = base + index * slotDuration;
|
|
401
|
+
const end = start + slotDuration;
|
|
402
|
+
return merged.some(([from, to]) => start < to && end > from);
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
}
|
|
269
406
|
|
|
270
407
|
// src/core/calendar-engine.ts
|
|
271
408
|
var CalendarEngine = class {
|
|
@@ -513,11 +650,17 @@ var CalendarEngine = class {
|
|
|
513
650
|
CalendarEngine,
|
|
514
651
|
DAYS,
|
|
515
652
|
DEFAULT_CATEGORY_COLORS,
|
|
653
|
+
DEFAULT_SLOT_DURATION,
|
|
654
|
+
MINUTES_PER_DAY,
|
|
516
655
|
MONTHS,
|
|
517
656
|
MONTHS_FULL,
|
|
657
|
+
bookedSlots,
|
|
518
658
|
escapeHtml,
|
|
659
|
+
eventCoversDate,
|
|
660
|
+
eventInterval,
|
|
519
661
|
formatAttendees,
|
|
520
662
|
formatDateForDisplay,
|
|
663
|
+
formatMinutes,
|
|
521
664
|
formatTimeRange,
|
|
522
665
|
generateCalendarDates,
|
|
523
666
|
generateYears,
|
|
@@ -527,11 +670,16 @@ var CalendarEngine = class {
|
|
|
527
670
|
getEventsForDate,
|
|
528
671
|
getMonthYearText,
|
|
529
672
|
hasEvents,
|
|
673
|
+
isDateWithinWindow,
|
|
674
|
+
isDayAllowed,
|
|
530
675
|
isSameDay,
|
|
531
676
|
isToday,
|
|
532
677
|
isValidHexColor,
|
|
533
678
|
mergeCategoryColors,
|
|
679
|
+
mergeIntervals,
|
|
534
680
|
normalizeDate,
|
|
681
|
+
parseHourRanges,
|
|
682
|
+
parseTimeToMinutes,
|
|
535
683
|
safeColor,
|
|
536
684
|
safeUrl,
|
|
537
685
|
slugifyToken,
|