kalendly 0.2.2 → 0.3.1

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
@@ -62,7 +62,7 @@ npm install kalendly
62
62
  />
63
63
  <script src="https://unpkg.com/kalendly/dist/index.umd.js"></script>
64
64
 
65
- <kal-calendar id="cal" title="My Calendar"></kal-calendar>
65
+ <kal-calendar id="cal" heading="My Calendar"></kal-calendar>
66
66
 
67
67
  <script>
68
68
  const cal = document.getElementById('cal');
@@ -104,7 +104,7 @@ import 'kalendly/styles';
104
104
  function App() {
105
105
  return (
106
106
  <kal-calendar
107
- title="My Calendar"
107
+ heading="My Calendar"
108
108
  events={events}
109
109
  oncal-date-select={e => console.log(e.detail.date)}
110
110
  oncal-month-change={e => console.log(e.detail.year, e.detail.month)}
@@ -198,7 +198,7 @@ Without this the component still renders correctly — Vue falls back to a nativ
198
198
  ```vue
199
199
  <template>
200
200
  <kal-calendar
201
- title="My Calendar"
201
+ heading="My Calendar"
202
202
  :events="events"
203
203
  @cal-date-select="onDateSelect"
204
204
  @cal-month-change="onMonthChange"
@@ -236,7 +236,7 @@ import 'kalendly/styles';
236
236
  ```html
237
237
  <!-- app.component.html -->
238
238
  <kal-calendar
239
- title="My Calendar"
239
+ heading="My Calendar"
240
240
  [events]="events"
241
241
  (cal-date-select)="onDateSelect($event)"
242
242
  (cal-month-change)="onMonthChange($event)"
@@ -280,7 +280,7 @@ import 'kalendly/styles';
280
280
  function App() {
281
281
  return (
282
282
  <kal-calendar
283
- title="My Calendar"
283
+ heading="My Calendar"
284
284
  prop:events={events}
285
285
  on:cal-date-select={e => console.log(e.detail.date)}
286
286
  />
@@ -316,7 +316,7 @@ kalendly uses Light DOM — all standard CSS techniques work:
316
316
  }
317
317
 
318
318
  /* 2. Direct class overrides */
319
- .kalendly-calendar .calendar--card {
319
+ .kalendly-calendar .calendar-card {
320
320
  border-radius: 12px;
321
321
  }
322
322
  ```
@@ -333,40 +333,50 @@ document.querySelector('kal-calendar').theme = {
333
333
 
334
334
  Primitives are set as HTML attributes:
335
335
 
336
- | Attribute | Type | Default | Description |
337
- | ----------------------- | ---------------- | ---------------- | ------------------------------------------------ |
338
- | `title` | `string` | — | Calendar title |
339
- | `initial-date` | `string` | today | ISO date string for initial view |
340
- | `min-year` | `string` | currentYear - 30 | Minimum year in picker |
341
- | `max-year` | `string` | currentYear + 10 | Maximum year in picker |
342
- | `week-starts-on` | `"0"\|"1"` | `"0"` | Week start: 0 = Sunday, 1 = Monday |
343
- | `use-short-month-names` | `string` | — | Present = use abbreviated month names |
344
- | `availability-mode` | `"day"\|"time"` | | Hides event details; shows booked/free cells |
345
- | `selectable` | `"range"` | — | Enables day/slot selection (requires avail mode) |
346
- | `loading` | `boolean` (flag) | — | Present = render skeleton shimmer cells |
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
+ | `week-starts-on` | `"0"\|"1"` | `"0"` | Week start: 0 = Sunday, 1 = Monday |
345
+ | `use-short-month-names` | `string` | — | Present = use abbreviated month names |
346
+ | `availability-mode` | `"day"\|"time"` | — | Hides event details; shows booked/free cells |
347
+ | `slot-duration` | `string` (number) | `"60"` | Time-grid granularity in minutes; must divide 1440 |
348
+ | `selectable` | `"range"` | — | Enables day/slot selection (requires avail mode) |
349
+ | `loading` | `boolean` (flag) | — | Present = render skeleton shimmer cells |
347
350
 
348
351
  ## Properties
349
352
 
350
353
  Rich objects are set as JS properties (not attributes):
351
354
 
352
- | Property | Type | Description |
353
- | ---------------- | ---------------------------------- | ------------------------------------------------- |
354
- | `events` | `CalendarEvent[]` | Events to display |
355
- | `loading` | `boolean` | `true` = render skeleton cells; `false` = restore |
356
- | `theme` | `CalendarTheme` | Custom theme colors |
357
- | `categoryColors` | `CategoryColorMap` | Per-category color overrides |
358
- | `renderEvent` | `(event: CalendarEvent) => string` | Custom event HTML renderer |
359
- | `renderNoEvents` | `() => string` | Custom empty-state HTML renderer |
355
+ | Property | Type | Description |
356
+ | -------------------- | ---------------------------------- | ------------------------------------------------- |
357
+ | `events` | `CalendarEvent[]` | Events to display |
358
+ | `loading` | `boolean` | `true` = render skeleton cells; `false` = restore |
359
+ | `theme` | `CalendarTheme` | Custom theme colors |
360
+ | `categoryColors` | `CategoryColorMap` | Per-category color overrides |
361
+ | `renderEvent` | `(event: CalendarEvent) => string` | Custom event HTML renderer |
362
+ | `renderNoEvents` | `() => string` | Custom empty-state HTML renderer |
363
+ | `availabilityColors` | `Record<string, string>` | Colour per availability bucket |
364
+ | `selectableStatuses` | `string[]` | Buckets a range may start, end or span |
360
365
 
361
366
  > `renderEvent` and `renderNoEvents` are ignored when `availability-mode` is set.
362
367
 
368
+ ### Why `heading` and not `title`
369
+
370
+ `title` is a global HTML attribute, so the browser renders it as a tooltip floating over the whole calendar as well as using it as the heading. `heading` does the same job without the tooltip. `title` still works and warns once per page; it will be removed in a future release.
371
+
363
372
  ## Custom Events
364
373
 
365
- | Event | `detail` shape | Description |
366
- | ------------------------- | ---------------------------------------------------------------------- | -------------------------------------- |
367
- | `cal-date-select` | `{ date: Date, events: CalendarEvent[] }` | User clicked a date (normal mode) |
368
- | `cal-month-change` | `{ year: number, month: number }` | Fires **before** the new month renders |
369
- | `cal-availability-select` | `{ startDate: Date, endDate: Date }` or `{ date, startTime, endTime }` | Day/slot selected in availability mode |
374
+ | Event | `detail` shape | Description |
375
+ | ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- |
376
+ | `cal-date-select` | `{ date: Date, events: CalendarEvent[] }` | User clicked a date (normal mode) |
377
+ | `cal-month-change` | `{ year: number, month: number }` | Fires **before** the new month renders |
378
+ | `cal-availability-select` | `{ startDate: Date, endDate: Date }` or `{ date, startTime, endTime }` | Day/slot selected in availability mode |
379
+ | `cal-slot-select` | `{ date: Date, startTime: string, endTime: string, booked: boolean }` | Any time slot clicked, whether or not `selectable` is set |
370
380
 
371
381
  All events bubble and are composed (cross Shadow DOM boundaries).
372
382
 
@@ -385,7 +395,70 @@ Hides all event details from the end user — only booked/free state is shown. D
385
395
  <kal-calendar availability-mode="day"></kal-calendar>
386
396
  ```
387
397
 
388
- Days with events are tinted red (booked); days without events are tinted green (free). Only cells in the current month are colour-coded — other-month cells remain grayed out. Clicking a day fires no popup and reveals no event details.
398
+ Every event must declare `availabilityStatus`. Three buckets ship built in, coloured as a traffic light:
399
+
400
+ | Bucket | Colour | Meaning |
401
+ | ------------- | ------ | ----------------------------------------- |
402
+ | `open` | green | nothing claims this day |
403
+ | `conditional` | amber | claimed, but not necessarily hard-blocked |
404
+ | `blocked` | red | not available |
405
+
406
+ ```js
407
+ cal.events = [
408
+ { id: 1, date: '2026-03-02', availabilityStatus: 'blocked' },
409
+ { id: 2, date: '2026-03-05', availabilityStatus: 'conditional' },
410
+ ];
411
+ ```
412
+
413
+ Only cells in the current month are colour-coded — other-month cells remain grayed out. Clicking a day fires no popup and reveals no event details.
414
+
415
+ A day holding several events resolves by severity: `blocked` beats `conditional` beats `open`, so a day with both a conditional and a blocked booking reads blocked. Precedence never depends on the order events arrive in.
416
+
417
+ #### Your own buckets
418
+
419
+ `availabilityColors` merges over the built-in three — override one, or add your own:
420
+
421
+ ```js
422
+ cal.availabilityColors = {
423
+ conditional: '#7c3aed', // recolour a built-in
424
+ maintenance: '#0891b2', // add a bucket
425
+ };
426
+ cal.events = [{ id: 3, date: '2026-03-09', availabilityStatus: 'maintenance' }];
427
+ ```
428
+
429
+ A bucket named in `availabilityColors` paints from an inline colour, which takes precedence over the same colour set through `theme`. The built-in three paint from CSS variables and are themeable the usual way.
430
+
431
+ Caller-defined buckets resolve by the order their keys appear in `availabilityColors`.
432
+
433
+ #### Which days are selectable
434
+
435
+ By default only `open` days can start, end or span a range. `selectableStatuses` widens that:
436
+
437
+ ```js
438
+ cal.selectableStatuses = ['open', 'conditional'];
439
+ ```
440
+
441
+ #### Misconfiguration throws
442
+
443
+ An event with no `availabilityStatus`, or one naming a bucket that is neither built in nor declared in `availabilityColors`, throws and names the offending events:
444
+
445
+ ```
446
+ <kal-calendar> availability-mode requires availabilityStatus on every event. Missing on: 7, 9.
447
+ ```
448
+
449
+ Setting `events` throws where you set it. One case cannot: markup parsed before the module defines the element configures the calendar inside a custom element upgrade, and the browser reports exceptions there as uncaught rather than passing them to your code. The failure is kept, so the next call to `getEngine()`, `getCurrentDate()` or `goToDate()` throws it, and correcting `events` or `availabilityColors` clears it.
450
+
451
+ #### Two months side by side
452
+
453
+ ```html
454
+ <kal-calendar
455
+ availability-mode="day"
456
+ months="2"
457
+ selectable="range"
458
+ ></kal-calendar>
459
+ ```
460
+
461
+ Navigation advances one month at a time, so a range spanning a month boundary stays visible. Ranges cross panes freely, and it works in standard and time modes too. Panes stack vertically on narrow screens.
389
462
 
390
463
  <div align="center">
391
464
  <img src="./docs/images/day.png" alt="Availability day view — month grid with red booked cells and green free cells"/>
@@ -408,13 +481,81 @@ cal.events = [
408
481
  <kal-calendar availability-mode="time"></kal-calendar>
409
482
  ```
410
483
 
411
- Clicking a day opens a popup with a 24-slot hourly grid (00:00 – 23:00). Each slot shows only "Booked" or "Available" — no event name or organiser is ever rendered. A slot is booked if any event's time window overlaps that hour; the rest are free.
484
+ 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.
485
+
486
+ #### Slot length
487
+
488
+ `slot-duration` sets the grid granularity in minutes. It must divide 1440 evenly; anything else falls back to 60 with a console warning.
489
+
490
+ ```html
491
+ <kal-calendar availability-mode="time" slot-duration="30"></kal-calendar>
492
+ ```
493
+
494
+ The grid renders `1440 / slot-duration` slots, and `cal-availability-select` emits times on that granularity — half-hour slots produce half-hour selections.
495
+
496
+ 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
+
498
+ #### Bookings that cross midnight
499
+
500
+ 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:
501
+
502
+ ```js
503
+ { id: 1, date: '2026-03-15', startTime: '22:00', endTime: '06:00' }
504
+ ```
505
+
506
+ 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.
507
+
508
+ #### A booking with no end time
509
+
510
+ 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.
511
+
512
+ 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.
412
513
 
413
514
  <div align="center">
414
515
  <img src="./docs/images/time.png" alt="Availability time view — day popup showing 24 hourly slots coloured red (booked) or green (available)"/>
415
516
  <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>
416
517
  </div>
417
518
 
519
+ #### Reacting to a slot click
520
+
521
+ `cal-slot-select` fires on every slot click, the way `cal-date-select` fires for
522
+ every day — including booked slots, and whether or not `selectable` is set. Use it
523
+ to drive your own booking flow without turning on range selection:
524
+
525
+ ```js
526
+ cal.addEventListener('cal-slot-select', e => {
527
+ const { date, startTime, endTime, booked } = e.detail;
528
+ if (booked) return showTakenMessage(startTime);
529
+ openBookingForm(date, startTime, endTime);
530
+ });
531
+ ```
532
+
533
+ `selectable="range"` still governs `cal-availability-select`, the three-click
534
+ range machine and the range highlighting. It also controls the cursor: slots only
535
+ show a pointer when the grid can actually be booked from.
536
+
537
+ #### Round-tripping a selection
538
+
539
+ `cal-availability-select` emits an inclusive `endDate`, so a saved selection goes
540
+ straight back as a single event — no expanding into one event per day:
541
+
542
+ ```js
543
+ cal.addEventListener('cal-availability-select', async e => {
544
+ const { startDate, endDate } = e.detail;
545
+ const booking = await save({ startDate, endDate });
546
+
547
+ cal.events = [
548
+ ...cal.events,
549
+ {
550
+ id: booking.id,
551
+ date: startDate,
552
+ endDate,
553
+ availabilityStatus: 'blocked',
554
+ },
555
+ ];
556
+ });
557
+ ```
558
+
418
559
  ### Selectable range
419
560
 
420
561
  Add `selectable="range"` to let the user pick a free day or time slot:
@@ -489,30 +630,57 @@ cal.getEngine();
489
630
 
490
631
  ## CalendarEvent Interface
491
632
 
633
+ > **Event text renders as text.** `name`, `description`, `location`,
634
+ > `organizer`, `notes`, `tags` and `attendees` are HTML-escaped, so markup in
635
+ > those fields shows as characters rather than being parsed. Use `renderEvent`
636
+ > if you need to emit your own markup. `url` accepts `http:`, `https:`,
637
+ > `mailto:` and relative URLs; anything else becomes `#`. `color` accepts hex
638
+ > values and CSS colour keywords.
639
+
640
+ > **Multi-day events.** `endDate` is the last day of a span and is **inclusive** —
641
+ > `date: '2026-03-03', endDate: '2026-03-05'` covers three days. That matches the
642
+ > `endDate` `cal-availability-select` emits, so a selection can be handed straight
643
+ > back as one event. It deliberately differs from RFC 5545, whose all-day `DTEND`
644
+ > is exclusive; adjust if you map to iCalendar. An `endDate` before `date`, or one
645
+ > that cannot be read, throws and names the event.
646
+ >
647
+ > Times on a span repeat daily: `09:00`–`17:00` across three days means that
648
+ > window on each of the three, not one continuous block. A hall can hold a
649
+ > 09:00–17:00 meeting and a 17:30–22:00 class on overlapping days.
650
+ >
651
+ > `recurring` is declared on the type but not implemented — nothing reads it.
652
+
653
+ > **Custom values.** `status`, `category` and `priority` accept any string. An
654
+ > unrecognised value renders as an uppercased badge with a neutral fill, which
655
+ > you can style via `.badge.status-<your-value>` or recolour through
656
+ > `--calendar-badge-bg` / `--calendar-badge-text`.
657
+
492
658
  ```typescript
493
659
  interface CalendarEvent {
494
660
  id: string | number;
495
661
  name: string;
496
662
  date: string | Date;
497
663
 
664
+ endDate?: string | Date; // last day of a span, inclusive
665
+
498
666
  startTime?: string; // e.g. "09:00"
499
667
  endTime?: string; // e.g. "10:00"
500
668
  allDay?: boolean;
501
669
 
502
670
  description?: string;
503
671
  color?: string;
504
- category?:
505
- | 'work'
506
- | 'personal'
507
- | 'meeting'
508
- | 'deadline'
509
- | 'appointment'
510
- | 'other';
672
+ // Known values keep autocomplete; any other string is accepted
673
+ category?: Open<
674
+ 'work' | 'personal' | 'meeting' | 'deadline' | 'appointment' | 'other'
675
+ >;
511
676
  location?: string;
512
677
  url?: string;
513
678
 
514
- status?: 'scheduled' | 'completed' | 'cancelled' | 'tentative';
515
- priority?: 'low' | 'medium' | 'high';
679
+ status?: Open<'scheduled' | 'completed' | 'cancelled' | 'tentative'>;
680
+ priority?: Open<'low' | 'medium' | 'high'>;
681
+
682
+ // Required under availability-mode; see Availability Mode below
683
+ availabilityStatus?: Open<'open' | 'conditional' | 'blocked'>;
516
684
 
517
685
  attendees?: string[];
518
686
  organizer?: string;
@@ -532,26 +700,77 @@ interface CalendarEvent {
532
700
 
533
701
  ## Theming
534
702
 
535
- ### All CSS variables
703
+ ### Design tokens
704
+
705
+ Every colour, size, radius, shadow and spacing step is a custom property. Two
706
+ tiers: `--kal-*` holds the raw palette, `--calendar-*` names what each value is
707
+ for. Override the `--calendar-*` layer — the primitives are internal.
536
708
 
537
709
  ```css
538
710
  :root {
711
+ /* Brand */
539
712
  --calendar-primary-color: #fc8917;
540
713
  --calendar-secondary-color: #fca045;
541
714
  --calendar-tertiary-color: #fdb873;
715
+
716
+ /* Surfaces and text */
542
717
  --calendar-text-color: #2c3e50;
543
718
  --calendar-text-light: #6b7280;
544
- --calendar-border-color: #dee2e6;
545
- --calendar-today-outline: #f7db04;
546
- --calendar-event-indicator: #1890ff;
719
+ --calendar-on-accent: #fff;
547
720
  --calendar-background: #fff;
721
+ --calendar-border-color: #dee2e6;
548
722
  --calendar-cell-hover: #f3f4f6;
723
+ --calendar-header-bg: #f8f9fa;
549
724
  --calendar-selected-bg: #eff6ff;
725
+ --calendar-popup-bg: #fff;
726
+ --calendar-picker-bg: #fff;
727
+ --calendar-today-outline: #f7db04;
728
+ --calendar-event-indicator: #1890ff;
729
+ --calendar-input-invalid: #ef4444;
730
+ --calendar-link: #2563eb;
731
+ --calendar-skeleton-base: #f0f0f0;
732
+ --calendar-skeleton-highlight: #e8e8e8;
733
+
734
+ /* Availability */
735
+ --calendar-open-bg: #dcfce7;
736
+ --calendar-open-fg: #16a34a;
737
+ --calendar-conditional-bg: #fef3c7;
738
+ --calendar-conditional-fg: #d97706;
739
+ --calendar-blocked-bg: #fee2e2;
740
+ --calendar-blocked-fg: #dc2626;
741
+ --calendar-range-bg: #16a34a;
742
+ --calendar-range-outline: #15803d;
743
+ --calendar-in-range-bg: #bbf7d0;
744
+ --calendar-in-range-outline: #86efac;
745
+
746
+ /* Badges — bg/text is the fallback for caller-defined values */
747
+ --calendar-badge-bg: #f3f4f6;
748
+ --calendar-badge-text: #4b5563;
749
+ --calendar-badge-success-bg: #d1fae5;
750
+ --calendar-badge-success-text: #059669;
751
+ --calendar-badge-info-bg: #dbeafe;
752
+ --calendar-badge-info-text: #2563eb;
753
+ --calendar-badge-warning-bg: #fef3c7;
754
+ --calendar-badge-warning-text: #d97706;
755
+ --calendar-badge-danger-bg: #fee2e2;
756
+ --calendar-badge-danger-text: #dc2626;
757
+ --calendar-badge-neutral-bg: #f3f4f6;
758
+ --calendar-badge-neutral-text: #6b7280;
759
+ --calendar-badge-positive-bg: #dcfce7;
760
+ --calendar-badge-positive-text: #16a34a;
761
+ --calendar-badge-tentative-bg: #e0e7ff;
762
+ --calendar-badge-tentative-text: #4f46e5;
550
763
  }
551
764
  ```
552
765
 
766
+ Type, radius, elevation and spacing scales are exposed the same way —
767
+ `--calendar-font-*`, `--calendar-radius-*`, `--calendar-shadow-*` and
768
+ `--calendar-space-*`. See `dist/styles/calendar.css` for the full set.
769
+
553
770
  ### JS theme property (full reference)
554
771
 
772
+ Every `--calendar-*` colour token has a matching camelCase theme key.
773
+
555
774
  ```js
556
775
  cal.theme = {
557
776
  primary: '#3b82f6',
@@ -559,15 +778,54 @@ cal.theme = {
559
778
  tertiary: '#93c5fd',
560
779
  textColor: '#111827',
561
780
  textLight: '#6b7280',
781
+ onAccent: '#ffffff',
562
782
  background: '#ffffff',
563
783
  cellHover: '#f3f4f6',
564
784
  borderColor: '#e5e7eb',
565
785
  todayOutline: '#fbbf24',
566
786
  selectedBg: '#eff6ff',
787
+ headerBg: '#f8f9fa',
788
+ popupBg: '#ffffff',
789
+ pickerBg: '#ffffff',
790
+ pickerShadow: '0 4px 20px rgba(0, 0, 0, 0.15)',
567
791
  eventIndicator: '#10b981',
792
+ link: '#2563eb',
793
+
794
+ // Availability
795
+ openBg: '#dcfce7',
796
+ openFg: '#16a34a',
797
+ conditionalBg: '#fef3c7',
798
+ conditionalFg: '#d97706',
799
+ blockedBg: '#fee2e2',
800
+ blockedFg: '#dc2626',
801
+ rangeBg: '#16a34a',
802
+ rangeOutline: '#15803d',
803
+ inRangeBg: '#bbf7d0',
804
+ inRangeOutline: '#86efac',
805
+
806
+ // Badges
807
+ badgeBg: '#f3f4f6',
808
+ badgeText: '#4b5563',
809
+ badgeSuccessBg: '#d1fae5',
810
+ badgeSuccessText: '#059669',
811
+ badgeInfoBg: '#dbeafe',
812
+ badgeInfoText: '#2563eb',
813
+ badgeWarningBg: '#fef3c7',
814
+ badgeWarningText: '#d97706',
815
+ badgeDangerBg: '#fee2e2',
816
+ badgeDangerText: '#dc2626',
817
+ badgeNeutralBg: '#f3f4f6',
818
+ badgeNeutralText: '#6b7280',
819
+ badgePositiveBg: '#dcfce7',
820
+ badgePositiveText: '#16a34a',
821
+ badgeTentativeBg: '#e0e7ff',
822
+ badgeTentativeText: '#4f46e5',
568
823
  };
569
824
  ```
570
825
 
826
+ > `availabilityColors` writes an inline colour on the cell, so for any bucket it
827
+ > names it takes precedence over the matching `theme` key.
828
+
571
829
  ### Dark theme example
572
830
 
573
831
  ```js
@@ -1,17 +1,20 @@
1
+ type Open<T extends string> = T | (string & {});
1
2
  interface CalendarEvent {
2
3
  id: string | number;
3
4
  name: string;
4
5
  date: string | Date;
6
+ endDate?: string | Date;
5
7
  startTime?: string;
6
8
  endTime?: string;
7
9
  allDay?: boolean;
8
10
  description?: string;
9
11
  color?: string;
10
- category?: 'work' | 'personal' | 'meeting' | 'deadline' | 'appointment' | 'other';
12
+ category?: Open<'work' | 'personal' | 'meeting' | 'deadline' | 'appointment' | 'other'>;
11
13
  location?: string;
12
14
  url?: string;
13
- status?: 'scheduled' | 'completed' | 'cancelled' | 'tentative';
14
- priority?: 'low' | 'medium' | 'high';
15
+ status?: Open<'scheduled' | 'completed' | 'cancelled' | 'tentative'>;
16
+ priority?: Open<'low' | 'medium' | 'high'>;
17
+ availabilityStatus?: Open<'open' | 'conditional' | 'blocked'>;
15
18
  attendees?: string[];
16
19
  organizer?: string;
17
20
  reminders?: number[];
@@ -52,6 +55,13 @@ interface CalendarConfig {
52
55
  maxYear?: number;
53
56
  weekStartsOn?: 0 | 1;
54
57
  categoryColors?: CategoryColorMap;
58
+ monthCount?: number;
59
+ }
60
+ interface CalendarPane {
61
+ year: number;
62
+ month: number;
63
+ monthAndYearText: string;
64
+ calendarDates: CalendarDate[][];
55
65
  }
56
66
  interface CalendarActions {
57
67
  next: () => void;
@@ -62,18 +72,14 @@ interface CalendarActions {
62
72
  selectDate: (date: Date) => void;
63
73
  updateTasks: () => void;
64
74
  }
65
- interface PopupPosition {
66
- class: 'popup-left' | 'popup-right' | 'popup-center-top' | 'popup-center-bottom';
67
- style?: Record<string, string | number>;
68
- }
69
75
  interface CalendarViewModel extends CalendarState {
70
76
  months: string[];
71
77
  days: string[];
72
78
  years: number[];
73
79
  monthAndYearText: string;
74
80
  scheduleDay: string;
81
+ panes: CalendarPane[];
75
82
  calendarDates: CalendarDate[][];
76
- popupPositionClass: string;
77
83
  }
78
84
  type CalendarEventHandler = (event: CalendarEvent) => void;
79
85
  interface CalendarProps {
@@ -100,7 +106,37 @@ interface CalendarTheme {
100
106
  selectedBg?: string;
101
107
  headerBg?: string;
102
108
  popupBg?: string;
109
+ pickerBg?: string;
110
+ pickerShadow?: string;
103
111
  eventIndicator?: string;
112
+ onAccent?: string;
113
+ link?: string;
114
+ openBg?: string;
115
+ openFg?: string;
116
+ conditionalBg?: string;
117
+ conditionalFg?: string;
118
+ blockedBg?: string;
119
+ blockedFg?: string;
120
+ rangeBg?: string;
121
+ rangeOutline?: string;
122
+ inRangeBg?: string;
123
+ inRangeOutline?: string;
124
+ badgeBg?: string;
125
+ badgeText?: string;
126
+ badgeSuccessBg?: string;
127
+ badgeSuccessText?: string;
128
+ badgeInfoBg?: string;
129
+ badgeInfoText?: string;
130
+ badgeWarningBg?: string;
131
+ badgeWarningText?: string;
132
+ badgeDangerBg?: string;
133
+ badgeDangerText?: string;
134
+ badgeNeutralBg?: string;
135
+ badgeNeutralText?: string;
136
+ badgePositiveBg?: string;
137
+ badgePositiveText?: string;
138
+ badgeTentativeBg?: string;
139
+ badgeTentativeText?: string;
104
140
  }
105
141
 
106
142
  declare const MONTHS: string[];
@@ -110,10 +146,10 @@ declare function normalizeDate(date: Date): Date;
110
146
  declare function isSameDay(date1: Date, date2: Date): boolean;
111
147
  declare function isToday(date: Date): boolean;
112
148
  declare function generateYears(minYear?: number, maxYear?: number): number[];
149
+ declare function eventCoversDate(event: CalendarEvent, date: Date): boolean;
113
150
  declare function getEventsForDate(events: CalendarEvent[], date: Date): CalendarEvent[];
114
151
  declare function hasEvents(events: CalendarEvent[], date: Date): boolean;
115
152
  declare function generateCalendarDates(year: number, month: number, events?: CalendarEvent[], weekStartsOn?: 0 | 1): CalendarDate[][];
116
- declare function getPopupPositionClass(selectedDayIndex: number | null): string;
117
153
  declare function getCellClasses(calendarDate: CalendarDate): string[];
118
154
  declare function formatDateForDisplay(date: Date): string;
119
155
  declare function getMonthYearText(year: number, month: number): string;
@@ -125,6 +161,17 @@ declare function getDefaultEventColor(category?: string, customColors?: Category
125
161
  declare function mergeCategoryColors(customColors?: CategoryColorMap): CategoryColorMap;
126
162
  declare function isValidHexColor(color: string): boolean;
127
163
  declare function getCategoryColor(category: string, customColors?: CategoryColorMap): string;
164
+ declare function escapeHtml(value: unknown): string;
165
+ declare function slugifyToken(value: string): string;
166
+ declare function safeUrl(value: string): string;
167
+ declare function safeColor(value: string): string;
168
+ declare const MINUTES_PER_DAY = 1440;
169
+ declare const DEFAULT_SLOT_DURATION = 60;
170
+ declare function parseTimeToMinutes(time: unknown): number | null;
171
+ declare function formatMinutes(total: number): string;
172
+ declare function eventInterval(event: CalendarEvent, slotDuration: number, onDay: Date): [number, number] | null;
173
+ declare function mergeIntervals(intervals: Array<[number, number]>): Array<[number, number]>;
174
+ declare function bookedSlots(events: CalendarEvent[], date: Date, slotDuration?: number): boolean[];
128
175
 
129
176
  declare class CalendarEngine {
130
177
  private state;
@@ -222,4 +269,4 @@ declare class CalendarEngine {
222
269
  destroy(): void;
223
270
  }
224
271
 
225
- export { type CalendarActions, type CalendarConfig, type CalendarDate, CalendarEngine, type CalendarEvent, type CalendarEventHandler, type CalendarProps, type CalendarState, type CalendarTheme, type CalendarViewModel, type CategoryColorMap, DAYS, DEFAULT_CATEGORY_COLORS, MONTHS, MONTHS_FULL, type PopupPosition, formatAttendees, formatDateForDisplay, formatTimeRange, generateCalendarDates, generateYears, getCategoryColor, getCellClasses, getDefaultEventColor, getEventsForDate, getMonthYearText, getPopupPositionClass, hasEvents, isSameDay, isToday, isValidHexColor, mergeCategoryColors, normalizeDate, sortEventsByTime };
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 };