@nomideusz/svelte-calendar 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +408 -178
  2. package/dist/adapters/memory.d.ts +7 -9
  3. package/dist/adapters/memory.js +8 -23
  4. package/dist/adapters/recurring.d.ts +3 -8
  5. package/dist/adapters/recurring.js +27 -30
  6. package/dist/calendar/Calendar.svelte +457 -46
  7. package/dist/calendar/Calendar.svelte.d.ts +55 -4
  8. package/dist/core/index.d.ts +2 -2
  9. package/dist/core/index.js +1 -1
  10. package/dist/core/locale.js +2 -2
  11. package/dist/core/palette.d.ts +5 -0
  12. package/dist/core/palette.js +8 -0
  13. package/dist/core/types.d.ts +14 -0
  14. package/dist/engine/event-store.svelte.d.ts +0 -17
  15. package/dist/engine/event-store.svelte.js +30 -17
  16. package/dist/engine/index.d.ts +1 -1
  17. package/dist/engine/view-state.svelte.d.ts +10 -5
  18. package/dist/engine/view-state.svelte.js +32 -16
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.js +3 -4
  21. package/dist/theme/auto.d.ts +53 -0
  22. package/dist/theme/auto.js +534 -0
  23. package/dist/theme/index.d.ts +3 -1
  24. package/dist/theme/index.js +3 -1
  25. package/dist/theme/presets.d.ts +20 -6
  26. package/dist/theme/presets.js +19 -6
  27. package/dist/views/agenda/AgendaDay.svelte +116 -37
  28. package/dist/views/agenda/AgendaWeek.svelte +219 -70
  29. package/dist/views/agenda/index.d.ts +0 -2
  30. package/dist/views/agenda/index.js +0 -2
  31. package/dist/views/index.d.ts +3 -2
  32. package/dist/views/index.js +3 -2
  33. package/dist/views/mobile/Mobile.svelte +17 -0
  34. package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
  35. package/dist/views/mobile/MobileDay.svelte +665 -0
  36. package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
  37. package/dist/views/mobile/MobileWeek.svelte +456 -0
  38. package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
  39. package/dist/views/mobile/index.d.ts +1 -0
  40. package/dist/views/mobile/index.js +1 -0
  41. package/dist/views/planner/PlannerDay.svelte +144 -10
  42. package/dist/views/planner/PlannerWeek.svelte +147 -28
  43. package/dist/widget/CalendarWidget.svelte +2 -17
  44. package/package.json +2 -1
@@ -6,16 +6,16 @@
6
6
  Events are positioned as horizontal cards with overlap support.
7
7
  -->
8
8
  <script lang="ts">
9
- import { getContext, onMount, tick, untrack } from 'svelte';
9
+ import { getContext, onMount, tick, untrack, type Snippet } from 'svelte';
10
10
  import { fly } from 'svelte/transition';
11
11
  import { createClock } from '../../core/clock.svelte.js';
12
- import type { TimelineEvent } from '../../core/types.js';
12
+ import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
13
13
  import type { DragState } from '../../engine/drag.svelte.js';
14
14
  import type { ViewState } from '../../engine/view-state.svelte.js';
15
15
  import { DAY_MS, HOUR_MS, sod } from '../../core/time.js';
16
16
  import { isAllDay, isMultiDay, segmentForDay } from '../../core/time.js';
17
17
  import type { DaySegment } from '../../core/time.js';
18
- import { fmtH, fmtTime } from '../../core/locale.js';
18
+ import { fmtH, fmtTime, weekdayShort } from '../../core/locale.js';
19
19
  import { getLabels } from '../../core/locale.js';
20
20
 
21
21
  const L = $derived(getLabels());
@@ -62,8 +62,27 @@
62
62
  const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
63
63
  const snapIntervalCtx = getContext<{ current: number }>('calendar:snapInterval');
64
64
  const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
65
+ const loadRangeCtx = getContext<{ current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void }>('calendar:loadRange') as { current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void } | undefined;
65
66
 
66
67
  const clock = createClock();
68
+ const showNavCtx = getContext<{ current: boolean }>('calendar:showNavigation') as { current: boolean } | undefined;
69
+ const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | undefined;
70
+ const showNav = $derived(showNavCtx?.current ?? true);
71
+ const showDates = $derived(showDatesCtx?.current ?? true);
72
+
73
+ // ── New feature contexts ──
74
+ const blockedSlotsCtx = getContext<{ current: BlockedSlot[] | undefined }>('calendar:blockedSlots') as { current: BlockedSlot[] | undefined } | undefined;
75
+ const dayHeaderSnippetCtx = getContext<{ current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined }>('calendar:dayHeaderSnippet') as { current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined } | undefined;
76
+ const minDurationCtx = getContext<{ current: number | undefined }>('calendar:minDuration') as { current: number | undefined } | undefined;
77
+ const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
78
+ const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
79
+
80
+ const blockedSlots = $derived(blockedSlotsCtx?.current);
81
+ const dayHeaderSnippet = $derived(dayHeaderSnippetCtx?.current);
82
+ const minDuration = $derived(minDurationCtx?.current);
83
+ const oneventhover = $derived(callbacksCtx?.oneventhover);
84
+ const disabledDates = $derived(disabledDatesCtx?.current);
85
+ const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
67
86
 
68
87
  // ─── State ──────────────────────────────────────────
69
88
  let following = $state(true);
@@ -96,16 +115,29 @@
96
115
  const DAY_GAP = 2;
97
116
 
98
117
  const dateLabel = $derived(
99
- new Date(visibleDayMs).toLocaleDateString(locale ?? 'en-US', {
100
- weekday: 'long',
101
- month: 'long',
102
- day: 'numeric',
103
- })
118
+ showDates
119
+ ? new Date(visibleDayMs).toLocaleDateString(locale ?? 'en-US', {
120
+ weekday: 'long',
121
+ month: 'long',
122
+ day: 'numeric',
123
+ })
124
+ : new Date(visibleDayMs).toLocaleDateString(locale ?? 'en-US', {
125
+ weekday: 'long',
126
+ })
104
127
  );
105
128
 
106
129
  const count = 1 + 2 * BUFFER_DAYS;
107
130
  const origin = $derived(internalCenterMs - BUFFER_DAYS * DAY_MS);
108
131
 
132
+ // ─── Declare load range for entire visible buffer ────────
133
+ $effect(() => {
134
+ if (!loadRangeCtx) return;
135
+ const rangeStart = new Date(internalCenterMs - BUFFER_DAYS * DAY_MS);
136
+ const rangeEnd = new Date(internalCenterMs + (BUFFER_DAYS + 1) * DAY_MS);
137
+ loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
138
+ return () => loadRangeCtx.set(null);
139
+ });
140
+
109
141
  // Auto-calculate hourWidth — minimum 60px so hours stay readable.
110
142
  // When 24h × 60px exceeds the container, the timeline scrolls horizontally.
111
143
  const MIN_HOUR_W = 60;
@@ -398,6 +430,15 @@
398
430
  }
399
431
 
400
432
  // ─── Click-to-create ────────────────────────────────
433
+ function isBlockedAt(dayMs: number, hour: number): boolean {
434
+ if (!blockedSlots?.length) return false;
435
+ const jsDay = new Date(dayMs).getDay();
436
+ const isoDay = jsDay === 0 ? 7 : jsDay;
437
+ return blockedSlots.some(slot => {
438
+ if (slot.day && slot.day !== isoDay) return false;
439
+ return hour >= slot.start && hour < slot.end;
440
+ });
441
+ }
401
442
  function handleTrackClick(e: MouseEvent) {
402
443
  if (wasDragging) { wasDragging = false; return; }
403
444
  if (!oneventcreate || readOnly) return;
@@ -408,10 +449,16 @@
408
449
 
409
450
  for (const d of days) {
410
451
  if (clickX >= d.x && clickX < d.x + dayWidth) {
452
+ // Check disabled dates
453
+ if (disabledSet.has(d.ms)) return;
411
454
  const frac = (clickX - d.x) / dayWidth;
412
- const hour = Math.floor(startHour + frac * hourCount);
455
+ const clickHour = startHour + frac * hourCount;
456
+ // Check blocked slots
457
+ if (isBlockedAt(d.ms, clickHour)) return;
458
+ const hour = Math.floor(clickHour);
459
+ const durMin = minDuration ? Math.max(60, minDuration) : 60;
413
460
  const start = new Date(d.ms + hour * HOUR_MS);
414
- const end = new Date(d.ms + (hour + 1) * HOUR_MS);
461
+ const end = new Date(start.getTime() + durMin * 60_000);
415
462
  oneventcreate({ start, end });
416
463
  return;
417
464
  }
@@ -498,6 +545,7 @@
498
545
  class="fs-day"
499
546
  class:fs-today={d.today}
500
547
  class:fs-past={d.past}
548
+ class:fs-disabled={disabledSet.has(d.ms)}
501
549
  style:left="{d.x}px"
502
550
  style:width="{dayWidth}px"
503
551
  >
@@ -509,6 +557,37 @@
509
557
  </div>
510
558
  <div class="fs-tick fs-tick--half" style:left="{x + hourWidth * 0.5}px"></div>
511
559
  {/each}
560
+
561
+ <!-- Blocked slot overlays -->
562
+ {#if blockedSlots?.length}
563
+ {@const jsDay = new Date(d.ms).getDay()}
564
+ {@const isoDay = jsDay === 0 ? 7 : jsDay}
565
+ {#each blockedSlots as slot}
566
+ {#if !slot.day || slot.day === isoDay}
567
+ {@const s = Math.max(slot.start, startHour)}
568
+ {@const e = Math.min(slot.end, endHour)}
569
+ {#if e > s}
570
+ <div
571
+ class="fs-blocked"
572
+ style:left="{(s - startHour) * hourWidth}px"
573
+ style:width="{(e - s) * hourWidth}px"
574
+ aria-label={slot.label || 'Unavailable'}
575
+ >
576
+ {#if slot.label}
577
+ <span class="fs-blocked-label">{slot.label}</span>
578
+ {/if}
579
+ </div>
580
+ {/if}
581
+ {/if}
582
+ {/each}
583
+ {/if}
584
+
585
+ <!-- Custom day header snippet -->
586
+ {#if dayHeaderSnippet}
587
+ <div class="fs-day-header-custom">
588
+ {@render dayHeaderSnippet({ date: new Date(d.ms), isToday: d.today, dayName: weekdayShort(d.ms, locale) })}
589
+ </div>
590
+ {/if}
512
591
  </div>
513
592
  {/each}
514
593
 
@@ -534,6 +613,7 @@
534
613
  tabindex="0"
535
614
  aria-label="{p.ev.title}{p.isCurrent ? ` (${L.inProgress})` : ''}"
536
615
  onpointerdown={(e) => onEventPointerDown(e, p.ev)}
616
+ onpointerenter={() => oneventhover?.(p.ev)}
537
617
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(p.ev); } }}
538
618
  >
539
619
  <div class="fs-ev-inner">
@@ -589,6 +669,7 @@
589
669
 
590
670
  <div class="fs-date-label">{dateLabel}</div>
591
671
 
672
+ {#if showNav}
592
673
  <nav class="fs-nav" aria-label={L.dayNavigation}>
593
674
  <button
594
675
  class="fs-nav-pill fs-nav-today"
@@ -614,6 +695,7 @@
614
695
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
615
696
  </button>
616
697
  </nav>
698
+ {/if}
617
699
  </div>
618
700
 
619
701
  <style>
@@ -662,6 +744,58 @@
662
744
  .fs-today { background: var(--dt-today-bg, rgba(239, 68, 68, 0.025)); }
663
745
  .fs-past { opacity: 0.7; }
664
746
 
747
+ /* ─── Disabled day ───────────────────────────────── */
748
+ .fs-disabled {
749
+ opacity: 0.35;
750
+ background: repeating-linear-gradient(
751
+ 45deg,
752
+ transparent,
753
+ transparent 6px,
754
+ var(--dt-border, rgba(148, 163, 184, 0.07)) 6px,
755
+ var(--dt-border, rgba(148, 163, 184, 0.07)) 7px
756
+ ) !important;
757
+ }
758
+
759
+ /* ─── Blocked slot overlay ───────────────────────── */
760
+ .fs-blocked {
761
+ position: absolute;
762
+ top: 18px;
763
+ bottom: 0;
764
+ z-index: 3;
765
+ background: repeating-linear-gradient(
766
+ -45deg,
767
+ color-mix(in srgb, var(--dt-text, rgba(148, 163, 184, 0.85)) 4%, transparent),
768
+ color-mix(in srgb, var(--dt-text, rgba(148, 163, 184, 0.85)) 4%, transparent) 4px,
769
+ transparent 4px,
770
+ transparent 8px
771
+ );
772
+ border-radius: 4px;
773
+ pointer-events: none;
774
+ display: flex;
775
+ align-items: flex-end;
776
+ justify-content: center;
777
+ padding-bottom: 6px;
778
+ }
779
+
780
+ .fs-blocked-label {
781
+ font: 500 9px/1 var(--dt-sans, system-ui, sans-serif);
782
+ color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
783
+ text-transform: uppercase;
784
+ letter-spacing: 0.04em;
785
+ white-space: nowrap;
786
+ }
787
+
788
+ /* ─── Custom day header ──────────────────────────── */
789
+ .fs-day-header-custom {
790
+ position: absolute;
791
+ top: 16px;
792
+ left: 50%;
793
+ transform: translateX(-50%);
794
+ z-index: 4;
795
+ pointer-events: auto;
796
+ white-space: nowrap;
797
+ }
798
+
665
799
  /* ─── Hour ticks ─────────────────────────────────── */
666
800
  .fs-tick {
667
801
  position: absolute;
@@ -9,9 +9,9 @@
9
9
  • Generous whitespace, thin dividers, minimal chrome.
10
10
  -->
11
11
  <script lang="ts">
12
- import { getContext, onMount, tick, untrack } from 'svelte';
12
+ import { getContext, onMount, tick, untrack, type Snippet } from 'svelte';
13
13
  import { createClock } from '../../core/clock.svelte.js';
14
- import type { TimelineEvent } from '../../core/types.js';
14
+ import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
15
15
  import type { DragState } from '../../engine/drag.svelte.js';
16
16
  import type { ViewState } from '../../engine/view-state.svelte.js';
17
17
  import { DAY_MS, HOUR_MS, sod } from '../../core/time.js';
@@ -52,8 +52,31 @@
52
52
  const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
53
53
  const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
54
54
  const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
55
+ const loadRangeCtx = getContext<{ current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void }>('calendar:loadRange') as { current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void } | undefined;
55
56
 
56
57
  const clock = createClock();
58
+ const showNavCtx = getContext<{ current: boolean }>('calendar:showNavigation') as { current: boolean } | undefined;
59
+ const equalDaysCtx = getContext<{ current: boolean }>('calendar:equalDays') as { current: boolean } | undefined;
60
+ const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | undefined;
61
+ const hideDaysCtx = getContext<{ current: number[] | undefined }>('calendar:hideDays') as { current: number[] | undefined } | undefined;
62
+ const showNav = $derived(showNavCtx?.current ?? true);
63
+ const equalDays = $derived(equalDaysCtx?.current ?? false);
64
+ const showDates = $derived(showDatesCtx?.current ?? true);
65
+ const hideDays = $derived(hideDaysCtx?.current);
66
+
67
+ // ── New feature contexts ──
68
+ const blockedSlotsCtx = getContext<{ current: BlockedSlot[] | undefined }>('calendar:blockedSlots') as { current: BlockedSlot[] | undefined } | undefined;
69
+ const dayHeaderSnippetCtx = getContext<{ current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined }>('calendar:dayHeaderSnippet') as { current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined } | undefined;
70
+ const minDurationCtx = getContext<{ current: number | undefined }>('calendar:minDuration') as { current: number | undefined } | undefined;
71
+ const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
72
+ const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
73
+
74
+ const blockedSlots = $derived(blockedSlotsCtx?.current);
75
+ const dayHeaderSnippet = $derived(dayHeaderSnippetCtx?.current);
76
+ const minDuration = $derived(minDurationCtx?.current);
77
+ const oneventhover = $derived(callbacksCtx?.oneventhover);
78
+ const disabledDates = $derived(disabledDatesCtx?.current);
79
+ const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
57
80
 
58
81
  // ─── Infinite-scroll config ────────────────────────
59
82
  const BUFFER_WEEKS = 6; // weeks rendered on each side of anchor
@@ -80,7 +103,23 @@
80
103
 
81
104
  // ─── Derived ────────────────────────────────────────
82
105
  const todayMs = $derived(clock.today);
83
- const anchorWeekStart = $derived(sowFn(internalFocusMs, mondayStart));
106
+ const customDays = $derived(viewState?.dayCount ?? 7);
107
+ const anchorPeriodStart = $derived(
108
+ customDays === 7
109
+ ? sowFn(internalFocusMs, mondayStart)
110
+ : sod(internalFocusMs)
111
+ );
112
+
113
+ // ─── Declare load range for entire visible buffer ────────
114
+ // Instead of calling store.load() directly, we tell Calendar
115
+ // what range we need. Calendar's single $effect handles loading.
116
+ $effect(() => {
117
+ if (!loadRangeCtx) return;
118
+ const rangeStart = new Date(anchorPeriodStart - BUFFER_WEEKS * customDays * DAY_MS);
119
+ const rangeEnd = new Date(anchorPeriodStart + (BUFFER_WEEKS + 1) * customDays * DAY_MS);
120
+ loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
121
+ return () => loadRangeCtx.set(null);
122
+ });
84
123
 
85
124
  // ─── Week data ──────────────────────────────────────
86
125
  interface WeekRow {
@@ -105,24 +144,20 @@
105
144
 
106
145
  const weeks = $derived.by(() => {
107
146
  const result: WeekRow[] = [];
108
- const currentWeekStart = sowFn(todayMs, mondayStart);
109
147
 
110
148
  for (let w = -BUFFER_WEEKS; w <= BUFFER_WEEKS; w++) {
111
- const weekStart = anchorWeekStart + w * 7 * DAY_MS;
112
- const isCurrent = weekStart === currentWeekStart;
149
+ const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
150
+ const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
113
151
  const days: DayCell[] = [];
114
152
 
115
- const firstMonth = new Date(weekStart).getMonth();
116
- const lastMonth = new Date(weekStart + 6 * DAY_MS).getMonth();
117
-
118
- for (let d = 0; d < 7; d++) {
119
- const ms = weekStart + d * DAY_MS;
153
+ for (let d = 0; d < customDays; d++) {
154
+ const ms = periodStart + d * DAY_MS;
120
155
  const date = new Date(ms);
121
156
  const dayNum = date.getDate();
122
157
  const dow = date.getDay();
123
158
  const isWeekend = dow === 0 || dow === 6;
124
159
  const isToday = ms === todayMs;
125
- const isPast = ms < todayMs;
160
+ const isPast = equalDays ? false : ms < todayMs;
126
161
  const isFirstOfMonth = dayNum === 1;
127
162
  const monthLabel = (d === 0 || isFirstOfMonth)
128
163
  ? monthLong(ms, locale).toUpperCase()
@@ -148,12 +183,24 @@
148
183
  days.push({ ms, dayNum, isToday, isPast, isWeekend, isFirstOfMonth, monthLabel, events: timedEvents, allDaySegments });
149
184
  }
150
185
 
151
- // Month label: show when first day of week is day 1-7
152
- const startDate = new Date(weekStart);
153
- const showMonth = startDate.getDate() <= 7;
154
- const monthLabel = showMonth ? monthLong(weekStart, locale).toUpperCase() : null;
186
+ // Month label: show when first day of period is day 1-7 (for 7-day), or first day of period (for custom)
187
+ const startDate = new Date(periodStart);
188
+ const showMonth = customDays === 7 ? startDate.getDate() <= 7 : startDate.getDate() <= customDays;
189
+ const monthLabel = showMonth ? monthLong(periodStart, locale).toUpperCase() : null;
155
190
 
156
- result.push({ weekStart, isCurrent, monthLabel, days });
191
+ result.push({ weekStart: periodStart, isCurrent, monthLabel, days });
192
+ }
193
+
194
+ // Filter hidden days if hideDays is set
195
+ if (hideDays?.length) {
196
+ for (const row of result) {
197
+ row.days = row.days.filter((d) => {
198
+ const isoDay = new Date(d.ms).getDay();
199
+ // Convert JS day (0=Sun) to ISO (7=Sun)
200
+ const iso = isoDay === 0 ? 7 : isoDay;
201
+ return !hideDays.includes(iso);
202
+ });
203
+ }
157
204
  }
158
205
 
159
206
  return result;
@@ -225,7 +272,7 @@
225
272
  }
226
273
 
227
274
  const shift = SHIFT_WEEKS * direction;
228
- internalFocusMs += shift * 7 * DAY_MS;
275
+ internalFocusMs += shift * customDays * DAY_MS;
229
276
  lastExternalMs = internalFocusMs;
230
277
  viewState?.setFocusDate(new Date(internalFocusMs));
231
278
 
@@ -296,8 +343,12 @@
296
343
  const target = e.target as HTMLElement;
297
344
  if (target.closest('.wg-ev')) return;
298
345
  if (readOnly || !oneventcreate) return;
346
+ // Check disabled dates
347
+ if (disabledSet.has(ms)) return;
348
+ // Check blocked slots (all-day block check: if entire day is blocked, prevent)
349
+ const durMin = minDuration ? Math.max(60, minDuration) : 60;
299
350
  const start = new Date(ms + 9 * HOUR_MS);
300
- const end = new Date(ms + 10 * HOUR_MS);
351
+ const end = new Date(start.getTime() + durMin * 60_000);
301
352
  oneventcreate({ start, end });
302
353
  }
303
354
 
@@ -402,8 +453,7 @@
402
453
  class="wg-cell"
403
454
  class:wg-cell--today={day.isToday}
404
455
  class:wg-cell--past={day.isPast}
405
- class:wg-cell--weekend={day.isWeekend}
406
- role="gridcell"
456
+ class:wg-cell--weekend={day.isWeekend} class:wg-cell--disabled={disabledSet.has(day.ms)} role="gridcell"
407
457
  tabindex="0"
408
458
  aria-label="{new Date(day.ms).toLocaleDateString(locale ?? 'en-US', { weekday: 'long', month: 'short', day: 'numeric' })}{day.isToday ? ` (${L.today.toLowerCase()})` : ''}, {L.nEvents(day.events.length)}"
409
459
  onclick={(e) => handleDayCellClick(day.ms, e)}
@@ -411,16 +461,40 @@
411
461
  >
412
462
  <!-- Day label in top-right corner -->
413
463
  <div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
414
- <span class="wg-day-num" class:wg-day-num--today={day.isToday}>
415
- {day.dayNum}
416
- </span>
464
+ {#if showDates}
465
+ <span class="wg-day-num" class:wg-day-num--today={day.isToday}>
466
+ {day.dayNum}
467
+ </span>
468
+ {/if}
417
469
  <span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
418
470
  </div>
419
471
 
420
- {#if day.monthLabel}
472
+ {#if day.monthLabel && showDates}
421
473
  <span class="wg-cell-month">{day.monthLabel}</span>
422
474
  {/if}
423
475
 
476
+ <!-- Custom day header snippet -->
477
+ {#if dayHeaderSnippet}
478
+ <div class="wg-cell-custom-header">
479
+ {@render dayHeaderSnippet({ date: new Date(day.ms), isToday: day.isToday, dayName: weekdayShort(day.ms, locale) })}
480
+ </div>
481
+ {/if}
482
+
483
+ <!-- Blocked slots indicator -->
484
+ {#if blockedSlots?.length}
485
+ {@const jsDay = new Date(day.ms).getDay()}
486
+ {@const isoDay = jsDay === 0 ? 7 : jsDay}
487
+ {#each blockedSlots as slot}
488
+ {#if !slot.day || slot.day === isoDay}
489
+ <div class="wg-blocked" aria-label={slot.label || 'Unavailable'}>
490
+ {#if slot.label}
491
+ <span class="wg-blocked-label">{slot.label}</span>
492
+ {/if}
493
+ </div>
494
+ {/if}
495
+ {/each}
496
+ {/if}
497
+
424
498
  <!-- All-day / multi-day events -->
425
499
  {#if day.allDaySegments.length > 0}
426
500
  <div class="wg-allday">
@@ -464,8 +538,7 @@
464
538
  role="button"
465
539
  tabindex="0"
466
540
  aria-label="{ev.title}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
467
- onpointerdown={(e) => onEventPointerDown(e, ev)}
468
-
541
+ onpointerdown={(e) => onEventPointerDown(e, ev)} onpointerenter={() => oneventhover?.(ev)}
469
542
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
470
543
  >
471
544
  <span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
@@ -484,7 +557,7 @@
484
557
  {/each}
485
558
  </div>
486
559
 
487
- {#if scrolled}
560
+ {#if showNav && scrolled}
488
561
  <nav class="wg-nav" aria-label={L.weekNavigation}>
489
562
  <button
490
563
  class="wg-nav-pill"
@@ -576,8 +649,54 @@
576
649
  .wg-week--current .wg-cell--past { opacity: 0.8; }
577
650
  .wg-week--current .wg-cell--past:hover { opacity: 0.9; }
578
651
 
652
+ /* equalDays: when no cells are marked past, all are full brightness */
653
+
579
654
  .wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
580
655
 
656
+ /* ─── Disabled cell ──────────────────────────────── */
657
+ .wg-cell--disabled {
658
+ opacity: 0.35;
659
+ pointer-events: none;
660
+ background: repeating-linear-gradient(
661
+ 45deg,
662
+ transparent,
663
+ transparent 6px,
664
+ var(--dt-border, rgba(0, 0, 0, 0.06)) 6px,
665
+ var(--dt-border, rgba(0, 0, 0, 0.06)) 7px
666
+ ) !important;
667
+ }
668
+
669
+ /* ─── Blocked slot indicator ─────────────────────── */
670
+ .wg-blocked {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 3px;
674
+ padding: 2px 4px;
675
+ border-radius: 3px;
676
+ background: repeating-linear-gradient(
677
+ -45deg,
678
+ color-mix(in srgb, var(--dt-text, rgba(0,0,0,0.85)) 4%, transparent),
679
+ color-mix(in srgb, var(--dt-text, rgba(0,0,0,0.85)) 4%, transparent) 3px,
680
+ transparent 3px,
681
+ transparent 6px
682
+ );
683
+ margin-bottom: 2px;
684
+ min-height: 14px;
685
+ }
686
+
687
+ .wg-blocked-label {
688
+ font: 500 8px/1 var(--dt-sans, system-ui, sans-serif);
689
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
690
+ text-transform: uppercase;
691
+ letter-spacing: 0.04em;
692
+ white-space: nowrap;
693
+ }
694
+
695
+ /* ─── Custom day header ──────────────────────────── */
696
+ .wg-cell-custom-header {
697
+ padding: 0 4px 2px;
698
+ }
699
+
581
700
  /* ─── Cell header (day label top-right) ──────────── */
582
701
  .wg-cell-hd {
583
702
  display: flex;
@@ -15,17 +15,11 @@
15
15
  -->
16
16
  <script lang="ts">
17
17
  import Calendar from '../calendar/Calendar.svelte';
18
- import type { CalendarView } from '../calendar/Calendar.svelte';
19
- import { Planner } from '../views/planner/index.js';
20
- import { Agenda } from '../views/agenda/index.js';
21
18
  import { createRestAdapter } from '../adapters/rest.js';
22
19
  import { createMemoryAdapter } from '../adapters/memory.js';
23
20
  import { presets } from '../theme/presets.js';
24
21
  import type { PresetName } from '../theme/presets.js';
25
22
  import type { TimelineEvent } from '../core/types.js';
26
- import { getLabels } from '../core/locale.js';
27
-
28
- const L = $derived(getLabels());
29
23
 
30
24
  interface Props {
31
25
  /** REST API base URL — if provided, fetches events from this endpoint */
@@ -64,7 +58,7 @@
64
58
  const heightPx = $derived(parseInt(height, 10) || 600);
65
59
  const isMondayStart = $derived(mondaystart !== 'false');
66
60
  const themeStyle = $derived(
67
- (presets as Record<string, string>)[theme] ?? presets.neutral
61
+ (presets as Record<string, string>)[theme] || presets.neutral
68
62
  );
69
63
  const dirValue = $derived(
70
64
  (dir === 'rtl' || dir === 'ltr' || dir === 'auto') ? dir as 'ltr' | 'rtl' | 'auto' : undefined
@@ -135,20 +129,11 @@
135
129
  }
136
130
  return createMemoryAdapter(parseEvents(events));
137
131
  });
138
-
139
- // ── Default views ──
140
- const defaultViews: CalendarView[] = $derived([
141
- { id: 'day-planner', label: L.planner, granularity: 'day', component: Planner, props: { mode: 'day' } },
142
- { id: 'week-planner', label: L.planner, granularity: 'week', component: Planner, props: { mode: 'week' } },
143
- { id: 'day-agenda', label: L.agenda, granularity: 'day', component: Agenda, props: { mode: 'day' } },
144
- { id: 'week-agenda', label: L.agenda, granularity: 'week', component: Agenda, props: { mode: 'week' } },
145
- ]);
146
132
  </script>
147
133
 
148
134
  <Calendar
149
135
  {adapter}
150
- views={defaultViews}
151
- defaultView={view}
136
+ {view}
152
137
  theme={themeStyle}
153
138
  height={heightPx}
154
139
  mondayStart={isMondayStart}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A themeable Svelte 5 calendar with Day and Week views — Planner and Agenda.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,6 +43,7 @@
43
43
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
44
44
  "@types/node": "^22.0.0",
45
45
  "@vercel/analytics": "^1.6.1",
46
+ "jsdom": "^28.1.0",
46
47
  "marked": "^17.0.3",
47
48
  "svelte": "^5.51.0",
48
49
  "svelte-check": "^4.3.6",