@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.
- 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 +147 -28
- 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,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
|
|
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
|
|
112
|
-
const isCurrent =
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
152
|
-
const startDate = new Date(
|
|
153
|
-
const showMonth = startDate.getDate() <= 7;
|
|
154
|
-
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;
|
|
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 *
|
|
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(
|
|
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
|
-
|
|
415
|
-
{day.
|
|
416
|
-
|
|
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]
|
|
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",
|