@nomideusz/svelte-calendar 0.5.2 → 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.
- package/README.md +408 -178
- package/dist/adapters/memory.d.ts +7 -9
- package/dist/adapters/memory.js +8 -23
- package/dist/adapters/recurring.d.ts +3 -8
- package/dist/adapters/recurring.js +27 -30
- package/dist/calendar/Calendar.svelte +457 -46
- package/dist/calendar/Calendar.svelte.d.ts +55 -4
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/locale.js +2 -2
- package/dist/core/palette.d.ts +5 -0
- package/dist/core/palette.js +8 -0
- package/dist/core/types.d.ts +14 -0
- package/dist/engine/event-store.svelte.d.ts +0 -17
- package/dist/engine/event-store.svelte.js +30 -17
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +10 -5
- package/dist/engine/view-state.svelte.js +32 -16
- package/dist/index.d.ts +7 -7
- package/dist/index.js +3 -4
- package/dist/theme/auto.d.ts +53 -0
- package/dist/theme/auto.js +534 -0
- package/dist/theme/index.d.ts +3 -1
- package/dist/theme/index.js +3 -1
- package/dist/theme/presets.d.ts +20 -6
- package/dist/theme/presets.js +19 -6
- package/dist/views/agenda/AgendaDay.svelte +116 -37
- package/dist/views/agenda/AgendaWeek.svelte +219 -70
- package/dist/views/agenda/index.d.ts +0 -2
- package/dist/views/agenda/index.js +0 -2
- package/dist/views/index.d.ts +3 -2
- package/dist/views/index.js +3 -2
- package/dist/views/mobile/Mobile.svelte +17 -0
- package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
- package/dist/views/mobile/MobileDay.svelte +665 -0
- package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
- package/dist/views/mobile/MobileWeek.svelte +456 -0
- package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
- package/dist/views/mobile/index.d.ts +1 -0
- package/dist/views/mobile/index.js +1 -0
- package/dist/views/planner/PlannerDay.svelte +144 -10
- package/dist/views/planner/PlannerWeek.svelte +145 -40
- package/dist/widget/CalendarWidget.svelte +2 -17
- 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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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(
|
|
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,12 +9,11 @@
|
|
|
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
|
-
import type { EventStore } from '../../engine/event-store.svelte.js';
|
|
18
17
|
import { DAY_MS, HOUR_MS, sod } from '../../core/time.js';
|
|
19
18
|
import { startOfWeek as sowFn, fractionalHour, isAllDay, isMultiDay, segmentForDay } from '../../core/time.js';
|
|
20
19
|
import type { DaySegment } from '../../core/time.js';
|
|
@@ -53,9 +52,31 @@
|
|
|
53
52
|
const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
|
|
54
53
|
const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
|
|
55
54
|
const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
|
|
56
|
-
const
|
|
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;
|
|
57
56
|
|
|
58
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())) ?? []));
|
|
59
80
|
|
|
60
81
|
// ─── Infinite-scroll config ────────────────────────
|
|
61
82
|
const BUFFER_WEEKS = 6; // weeks rendered on each side of anchor
|
|
@@ -82,18 +103,22 @@
|
|
|
82
103
|
|
|
83
104
|
// ─── Derived ────────────────────────────────────────
|
|
84
105
|
const todayMs = $derived(clock.today);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
91
116
|
$effect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
97
122
|
});
|
|
98
123
|
|
|
99
124
|
// ─── Week data ──────────────────────────────────────
|
|
@@ -119,24 +144,20 @@
|
|
|
119
144
|
|
|
120
145
|
const weeks = $derived.by(() => {
|
|
121
146
|
const result: WeekRow[] = [];
|
|
122
|
-
const currentWeekStart = sowFn(todayMs, mondayStart);
|
|
123
147
|
|
|
124
148
|
for (let w = -BUFFER_WEEKS; w <= BUFFER_WEEKS; w++) {
|
|
125
|
-
const
|
|
126
|
-
const isCurrent =
|
|
149
|
+
const periodStart = anchorPeriodStart + w * customDays * DAY_MS;
|
|
150
|
+
const isCurrent = todayMs >= periodStart && todayMs < periodStart + customDays * DAY_MS;
|
|
127
151
|
const days: DayCell[] = [];
|
|
128
152
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (let d = 0; d < 7; d++) {
|
|
133
|
-
const ms = weekStart + d * DAY_MS;
|
|
153
|
+
for (let d = 0; d < customDays; d++) {
|
|
154
|
+
const ms = periodStart + d * DAY_MS;
|
|
134
155
|
const date = new Date(ms);
|
|
135
156
|
const dayNum = date.getDate();
|
|
136
157
|
const dow = date.getDay();
|
|
137
158
|
const isWeekend = dow === 0 || dow === 6;
|
|
138
159
|
const isToday = ms === todayMs;
|
|
139
|
-
const isPast = ms < todayMs;
|
|
160
|
+
const isPast = equalDays ? false : ms < todayMs;
|
|
140
161
|
const isFirstOfMonth = dayNum === 1;
|
|
141
162
|
const monthLabel = (d === 0 || isFirstOfMonth)
|
|
142
163
|
? monthLong(ms, locale).toUpperCase()
|
|
@@ -162,12 +183,24 @@
|
|
|
162
183
|
days.push({ ms, dayNum, isToday, isPast, isWeekend, isFirstOfMonth, monthLabel, events: timedEvents, allDaySegments });
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
// Month label: show when first day of
|
|
166
|
-
const startDate = new Date(
|
|
167
|
-
const showMonth = startDate.getDate() <= 7;
|
|
168
|
-
const monthLabel = showMonth ? monthLong(
|
|
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;
|
|
169
190
|
|
|
170
|
-
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
|
+
}
|
|
171
204
|
}
|
|
172
205
|
|
|
173
206
|
return result;
|
|
@@ -239,7 +272,7 @@
|
|
|
239
272
|
}
|
|
240
273
|
|
|
241
274
|
const shift = SHIFT_WEEKS * direction;
|
|
242
|
-
internalFocusMs += shift *
|
|
275
|
+
internalFocusMs += shift * customDays * DAY_MS;
|
|
243
276
|
lastExternalMs = internalFocusMs;
|
|
244
277
|
viewState?.setFocusDate(new Date(internalFocusMs));
|
|
245
278
|
|
|
@@ -310,8 +343,12 @@
|
|
|
310
343
|
const target = e.target as HTMLElement;
|
|
311
344
|
if (target.closest('.wg-ev')) return;
|
|
312
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;
|
|
313
350
|
const start = new Date(ms + 9 * HOUR_MS);
|
|
314
|
-
const end = new Date(
|
|
351
|
+
const end = new Date(start.getTime() + durMin * 60_000);
|
|
315
352
|
oneventcreate({ start, end });
|
|
316
353
|
}
|
|
317
354
|
|
|
@@ -416,8 +453,7 @@
|
|
|
416
453
|
class="wg-cell"
|
|
417
454
|
class:wg-cell--today={day.isToday}
|
|
418
455
|
class:wg-cell--past={day.isPast}
|
|
419
|
-
class:wg-cell--weekend={day.isWeekend}
|
|
420
|
-
role="gridcell"
|
|
456
|
+
class:wg-cell--weekend={day.isWeekend} class:wg-cell--disabled={disabledSet.has(day.ms)} role="gridcell"
|
|
421
457
|
tabindex="0"
|
|
422
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)}"
|
|
423
459
|
onclick={(e) => handleDayCellClick(day.ms, e)}
|
|
@@ -425,16 +461,40 @@
|
|
|
425
461
|
>
|
|
426
462
|
<!-- Day label in top-right corner -->
|
|
427
463
|
<div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
|
|
428
|
-
|
|
429
|
-
{day.
|
|
430
|
-
|
|
464
|
+
{#if showDates}
|
|
465
|
+
<span class="wg-day-num" class:wg-day-num--today={day.isToday}>
|
|
466
|
+
{day.dayNum}
|
|
467
|
+
</span>
|
|
468
|
+
{/if}
|
|
431
469
|
<span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
|
|
432
470
|
</div>
|
|
433
471
|
|
|
434
|
-
{#if day.monthLabel}
|
|
472
|
+
{#if day.monthLabel && showDates}
|
|
435
473
|
<span class="wg-cell-month">{day.monthLabel}</span>
|
|
436
474
|
{/if}
|
|
437
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
|
+
|
|
438
498
|
<!-- All-day / multi-day events -->
|
|
439
499
|
{#if day.allDaySegments.length > 0}
|
|
440
500
|
<div class="wg-allday">
|
|
@@ -478,8 +538,7 @@
|
|
|
478
538
|
role="button"
|
|
479
539
|
tabindex="0"
|
|
480
540
|
aria-label="{ev.title}{ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick ? ` (${L.inProgress})` : ''}"
|
|
481
|
-
onpointerdown={(e) => onEventPointerDown(e, ev)}
|
|
482
|
-
|
|
541
|
+
onpointerdown={(e) => onEventPointerDown(e, ev)} onpointerenter={() => oneventhover?.(ev)}
|
|
483
542
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
|
|
484
543
|
>
|
|
485
544
|
<span class="wg-ev-time">{fmtAmPm(ev.start)}</span>
|
|
@@ -498,7 +557,7 @@
|
|
|
498
557
|
{/each}
|
|
499
558
|
</div>
|
|
500
559
|
|
|
501
|
-
{#if scrolled}
|
|
560
|
+
{#if showNav && scrolled}
|
|
502
561
|
<nav class="wg-nav" aria-label={L.weekNavigation}>
|
|
503
562
|
<button
|
|
504
563
|
class="wg-nav-pill"
|
|
@@ -590,8 +649,54 @@
|
|
|
590
649
|
.wg-week--current .wg-cell--past { opacity: 0.8; }
|
|
591
650
|
.wg-week--current .wg-cell--past:hover { opacity: 0.9; }
|
|
592
651
|
|
|
652
|
+
/* equalDays: when no cells are marked past, all are full brightness */
|
|
653
|
+
|
|
593
654
|
.wg-cell--weekend { background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012)); }
|
|
594
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
|
+
|
|
595
700
|
/* ─── Cell header (day label top-right) ──────────── */
|
|
596
701
|
.wg-cell-hd {
|
|
597
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]
|
|
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
|
-
|
|
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.
|
|
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",
|