@nomideusz/svelte-calendar 0.7.4 → 0.8.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 (36) hide show
  1. package/README.md +19 -4
  2. package/dist/calendar/Calendar.svelte +364 -497
  3. package/dist/calendar/Calendar.svelte.d.ts +6 -2
  4. package/dist/core/clock.svelte.js +12 -5
  5. package/dist/core/locale.d.ts +4 -0
  6. package/dist/core/locale.js +4 -0
  7. package/dist/engine/view-state.svelte.d.ts +2 -2
  8. package/dist/engine/view-state.svelte.js +20 -0
  9. package/dist/headless/create-calendar.svelte.js +18 -2
  10. package/dist/headless/types.d.ts +6 -6
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +3 -0
  13. package/dist/primitives/DayHeader.svelte +9 -25
  14. package/dist/primitives/EmptySlot.svelte +15 -31
  15. package/dist/primitives/EventBlock.svelte +33 -61
  16. package/dist/primitives/NowIndicator.svelte +16 -42
  17. package/dist/primitives/TimeGutter.svelte +9 -23
  18. package/dist/views/agenda/Agenda.svelte +3 -10
  19. package/dist/views/agenda/AgendaDay.svelte +195 -167
  20. package/dist/views/agenda/AgendaWeek.svelte +141 -195
  21. package/dist/views/mobile/Mobile.svelte +3 -10
  22. package/dist/views/mobile/MobileDay.svelte +422 -266
  23. package/dist/views/mobile/MobileWeek.svelte +131 -186
  24. package/dist/views/month/MonthGrid.svelte +334 -0
  25. package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
  26. package/dist/views/planner/Planner.svelte +3 -10
  27. package/dist/views/planner/PlannerDay.svelte +645 -549
  28. package/dist/views/planner/PlannerWeek.svelte +299 -412
  29. package/dist/views/shared/EventContent.svelte +16 -0
  30. package/dist/views/shared/EventContent.svelte.d.ts +9 -0
  31. package/dist/views/shared/context.svelte.d.ts +1 -0
  32. package/dist/views/shared/context.svelte.js +1 -0
  33. package/dist/widget/CalendarWidget.svelte +79 -116
  34. package/package.json +3 -2
  35. package/widget/svelte-calendar.css +329 -13
  36. package/widget/widget.js +1945 -1031
@@ -1,193 +1,124 @@
1
- <script lang="ts">
2
- /**
3
- * AgendaWeek rolling N-day agenda view.
4
- *
5
- * "The Week Ahead":
6
- * Today + tomorrow expanded with time slots/countdowns.
7
- * Future days compact (dot + time + title).
8
- * Past days dimmed.
9
- *
10
- * Answers: "What's coming up and when do I need to be ready?"
11
- */
12
- import { createClock } from '../../core/clock.svelte.js';
13
- import type { TimelineEvent } from '../../core/types.js';
14
- import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay } from '../../core/time.js';
15
- import { weekdayLong, monthLong, getLabels } from '../../core/locale.js';
16
- import { useCalendarContext } from '../shared/context.svelte.js';
17
- import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from '../shared/format.js';
18
-
19
- const L = $derived(getLabels());
20
- const ctx = useCalendarContext();
21
-
22
- interface Props {
23
- mondayStart?: boolean;
24
- locale?: string;
25
- height?: number;
26
- events?: TimelineEvent[];
27
- style?: string;
28
- focusDate?: Date;
29
- oneventclick?: (event: TimelineEvent) => void;
30
- selectedEventId?: string | null;
31
- [key: string]: unknown;
32
- }
33
-
34
- let {
35
- mondayStart = true,
36
- locale,
37
- height = 520,
38
- events = [],
39
- style = '',
40
- focusDate,
41
- oneventclick,
42
- selectedEventId = null,
43
- }: Props = $props();
44
-
45
- const clock = createClock();
46
- const viewState = $derived(ctx.viewState);
47
- const equalDays = $derived(ctx.equalDays);
48
- const showDates = $derived(ctx.showDates);
49
- const hideDays = $derived(ctx.hideDays);
50
- const isMobile = $derived(ctx.isMobile);
51
- const autoHeight = $derived(ctx.autoHeight);
52
- const compact = $derived(ctx.compact);
53
- const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
54
- const oneventhover = $derived(ctx.oneventhover);
55
- const disabledSet = $derived(ctx.disabledSet);
56
-
57
- // ── Swipe navigation (mobile) ──────────────────────
58
- let swipeStartX = 0;
59
- let swipeStartY = 0;
60
- const SWIPE_THRESHOLD = 50;
61
-
62
- function onPointerDown(e: PointerEvent) {
63
- if (!isMobile) return;
64
- swipeStartX = e.clientX;
65
- swipeStartY = e.clientY;
66
- }
67
-
68
- function onPointerUp(e: PointerEvent) {
69
- if (!isMobile) return;
70
- const dx = e.clientX - swipeStartX;
71
- const dy = e.clientY - swipeStartY;
72
- if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
73
- if (dx > 0) viewState?.prev();
74
- else viewState?.next();
75
- }
76
- }
77
-
78
- // ── Format helpers (delegated to shared/format.ts) ──
79
- // fmtTime, duration, groupIntoSlots imported at top
80
- // Thin wrappers that bind locale / clock.tick:
81
- const fmt = (d: Date) => fmtTime(d, locale);
82
- const eta = (ms: number) => timeUntilMs(ms, clock.tick);
83
- const prog = (ev: TimelineEvent) => progress(ev, clock.tick);
84
-
85
- // ── Event handlers ──────────────────────────────────
86
- function handleClick(ev: TimelineEvent): void {
87
- oneventclick?.(ev);
88
- }
89
-
90
- function handleKeydown(e: KeyboardEvent, ev: TimelineEvent): void {
91
- if (e.key === 'Enter' || e.key === ' ') {
92
- e.preventDefault();
93
- oneventclick?.(ev);
94
- }
95
- }
96
-
97
- // ── Week derivations ────────────────────────────────
98
- type DayTier = 'today' | 'tomorrow' | 'upcoming' | 'past';
99
-
100
- interface DayGroup {
101
- ms: number;
102
- dayName: string;
103
- dateLabel: string;
104
- tier: DayTier;
105
- events: TimelineEvent[];
106
- allDayEvents: TimelineEvent[];
107
- timedEvents: TimelineEvent[];
108
- pastEvents: TimelineEvent[];
109
- currentEvents: TimelineEvent[];
110
- upcomingEvents: TimelineEvent[];
111
- totalHours: number;
112
- }
113
-
114
- const weekStartMs = $derived(
115
- focusDate
116
- ? (viewState?.dayCount === 7
117
- ? startOfWeek(sod(focusDate.getTime()), mondayStart)
118
- : sod(focusDate.getTime()))
119
- : (viewState?.dayCount === 7
120
- ? startOfWeek(clock.today, mondayStart)
121
- : clock.today),
122
- );
123
-
124
- const customDays = $derived(viewState?.dayCount ?? 7);
125
-
126
- const weekDays = $derived.by((): DayGroup[] => {
127
- const now = clock.tick;
128
- const todayMs = clock.today;
129
- const tomorrowMs = todayMs + DAY_MS;
130
- const out: DayGroup[] = [];
131
- for (let i = 0; i < customDays; i++) {
132
- const ms = weekStartMs + i * DAY_MS;
133
- const dEnd = ms + DAY_MS;
134
- const dayEvts = events
135
- .filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms)
136
- .sort((a, b) => a.start.getTime() - b.start.getTime());
137
- const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
138
- const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
139
- const totalMinutes = timedEvts.reduce((sum, ev) => {
140
- const s = Math.max(ev.start.getTime(), ms);
141
- const e = Math.min(ev.end.getTime(), dEnd);
142
- return sum + (e - s) / 60000;
143
- }, 0);
144
- const pastEvents: TimelineEvent[] = [];
145
- const currentEvents: TimelineEvent[] = [];
146
- const upcomingEvents: TimelineEvent[] = [];
147
- for (const ev of timedEvts) {
148
- if (ev.end.getTime() <= now) pastEvents.push(ev);
149
- else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
150
- else upcomingEvents.push(ev);
151
- }
152
- let tier: DayTier;
153
- if (equalDays) {
154
- tier = 'upcoming';
155
- } else if (ms === todayMs) {
156
- tier = 'today';
157
- } else if (ms === tomorrowMs) {
158
- tier = 'tomorrow';
159
- } else if (ms < todayMs) {
160
- tier = 'past';
161
- } else {
162
- tier = 'upcoming';
163
- }
164
-
165
- out.push({
166
- ms,
167
- dayName: weekdayLong(ms, locale),
168
- dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
169
- tier,
170
- events: dayEvts,
171
- allDayEvents: allDayEvts,
172
- timedEvents: timedEvts,
173
- pastEvents,
174
- currentEvents,
175
- upcomingEvents,
176
- totalHours: Math.round((totalMinutes / 60) * 10) / 10,
177
- });
178
- }
179
-
180
- // Filter hidden days if hideDays is set
181
- if (hideDays?.length) {
182
- return out.filter((d) => {
183
- const jsDay = new Date(d.ms).getDay();
184
- const iso = jsDay === 0 ? 7 : jsDay;
185
- return !hideDays.includes(iso);
186
- });
187
- }
188
-
189
- return out;
190
- });
1
+ <script lang="ts">import { createClock } from "../../core/clock.svelte.js";
2
+ import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay } from "../../core/time.js";
3
+ import { weekdayLong, monthLong, getLabels } from "../../core/locale.js";
4
+ import { useCalendarContext } from "../shared/context.svelte.js";
5
+ import EventContent from "../shared/EventContent.svelte";
6
+ import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
7
+ const L = $derived(getLabels());
8
+ const ctx = useCalendarContext();
9
+ let {
10
+ mondayStart = true,
11
+ locale,
12
+ height = 520,
13
+ events = [],
14
+ style = "",
15
+ focusDate,
16
+ oneventclick,
17
+ selectedEventId = null
18
+ } = $props();
19
+ const clock = createClock();
20
+ const viewState = $derived(ctx.viewState);
21
+ const equalDays = $derived(ctx.equalDays);
22
+ const showDates = $derived(ctx.showDates);
23
+ const hideDays = $derived(ctx.hideDays);
24
+ const isMobile = $derived(ctx.isMobile);
25
+ const autoHeight = $derived(ctx.autoHeight);
26
+ const compact = $derived(ctx.compact);
27
+ const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
28
+ const oneventhover = $derived(ctx.oneventhover);
29
+ const disabledSet = $derived(ctx.disabledSet);
30
+ let swipeStartX = 0;
31
+ let swipeStartY = 0;
32
+ const SWIPE_THRESHOLD = 50;
33
+ function onPointerDown(e) {
34
+ if (!isMobile) return;
35
+ swipeStartX = e.clientX;
36
+ swipeStartY = e.clientY;
37
+ }
38
+ function onPointerUp(e) {
39
+ if (!isMobile) return;
40
+ const dx = e.clientX - swipeStartX;
41
+ const dy = e.clientY - swipeStartY;
42
+ if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
43
+ if (dx > 0) viewState?.prev();
44
+ else viewState?.next();
45
+ }
46
+ }
47
+ const fmt = (d) => fmtTime(d, locale);
48
+ const eta = (ms) => timeUntilMs(ms, clock.tick);
49
+ const prog = (ev) => progress(ev, clock.tick);
50
+ function handleClick(ev) {
51
+ oneventclick?.(ev);
52
+ }
53
+ function handleKeydown(e, ev) {
54
+ if (e.key === "Enter" || e.key === " ") {
55
+ e.preventDefault();
56
+ oneventclick?.(ev);
57
+ }
58
+ }
59
+ const weekStartMs = $derived(
60
+ focusDate ? viewState?.dayCount === 7 ? startOfWeek(sod(focusDate.getTime()), mondayStart) : sod(focusDate.getTime()) : viewState?.dayCount === 7 ? startOfWeek(clock.today, mondayStart) : clock.today
61
+ );
62
+ const customDays = $derived(viewState?.dayCount ?? 7);
63
+ const weekDays = $derived.by(() => {
64
+ const now = clock.tick;
65
+ const todayMs = clock.today;
66
+ const tomorrowMs = todayMs + DAY_MS;
67
+ const out = [];
68
+ for (let i = 0; i < customDays; i++) {
69
+ const ms = weekStartMs + i * DAY_MS;
70
+ const dEnd = ms + DAY_MS;
71
+ const dayEvts = events.filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
72
+ const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
73
+ const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
74
+ const totalMinutes = timedEvts.reduce((sum, ev) => {
75
+ const s = Math.max(ev.start.getTime(), ms);
76
+ const e = Math.min(ev.end.getTime(), dEnd);
77
+ return sum + (e - s) / 6e4;
78
+ }, 0);
79
+ const pastEvents = [];
80
+ const currentEvents = [];
81
+ const upcomingEvents = [];
82
+ for (const ev of timedEvts) {
83
+ if (ev.end.getTime() <= now) pastEvents.push(ev);
84
+ else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
85
+ else upcomingEvents.push(ev);
86
+ }
87
+ let tier;
88
+ if (equalDays) {
89
+ tier = "upcoming";
90
+ } else if (ms === todayMs) {
91
+ tier = "today";
92
+ } else if (ms === tomorrowMs) {
93
+ tier = "tomorrow";
94
+ } else if (ms < todayMs) {
95
+ tier = "past";
96
+ } else {
97
+ tier = "upcoming";
98
+ }
99
+ out.push({
100
+ ms,
101
+ dayName: weekdayLong(ms, locale),
102
+ dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
103
+ tier,
104
+ events: dayEvts,
105
+ allDayEvents: allDayEvts,
106
+ timedEvents: timedEvts,
107
+ pastEvents,
108
+ currentEvents,
109
+ upcomingEvents,
110
+ totalHours: Math.round(totalMinutes / 60 * 10) / 10
111
+ });
112
+ }
113
+ if (hideDays?.length) {
114
+ return out.filter((d) => {
115
+ const jsDay = new Date(d.ms).getDay();
116
+ const iso = jsDay === 0 ? 7 : jsDay;
117
+ return !hideDays.includes(iso);
118
+ });
119
+ }
120
+ return out;
121
+ });
191
122
  </script>
192
123
 
193
124
  <!-- ═══ Shared event card snippet ═══ -->
@@ -208,6 +139,7 @@
208
139
  onkeydown={(e) => handleKeydown(e, ev)}
209
140
  >
210
141
  <div class="ag-card-body">
142
+ <EventContent event={ev}>
211
143
  <span class="ag-card-title">{ev.title}</span>
212
144
  {#if ev.subtitle}
213
145
  <span class="ag-card-sub">{ev.subtitle}</span>
@@ -233,6 +165,7 @@
233
165
  {/each}
234
166
  </div>
235
167
  {/if}
168
+ </EventContent>
236
169
  {#if isNow}
237
170
  <div class="ag-card-progress">
238
171
  <div class="ag-card-progress-fill" style:width="{prog(ev) * 100}%"></div>
@@ -260,7 +193,7 @@
260
193
  <div class="ag-wday-head">
261
194
  <div class="ag-wday-head-left">
262
195
  <span class="ag-wday-name">{day.dayName}</span>
263
- {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
196
+ {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
264
197
  </div>
265
198
  {#if dayHeaderSnippet}
266
199
  <div class="ag-wday-custom-header">
@@ -338,6 +271,7 @@
338
271
  onpointerenter={() => oneventhover?.(ev)}
339
272
  onkeydown={(e) => handleKeydown(e, ev)}
340
273
  >
274
+ <EventContent event={ev}>
341
275
  <span class="ag-compact-dot"></span>
342
276
  <span class="ag-compact-time">{fmt(ev.start)}</span>
343
277
  <span class="ag-compact-title">{ev.title}</span>
@@ -350,6 +284,7 @@
350
284
  {/each}
351
285
  {/if}
352
286
  <span class="ag-compact-dur">{duration(ev)}</span>
287
+ </EventContent>
353
288
  </div>
354
289
  {/each}
355
290
  </div>
@@ -411,6 +346,7 @@
411
346
  aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
412
347
  onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
413
348
  >
349
+ <EventContent event={ev}>
414
350
  <span class="ag-compact-dot"></span>
415
351
  <span class="ag-compact-time">{fmt(ev.start)}</span>
416
352
  <span class="ag-compact-title">{ev.title}</span>
@@ -426,6 +362,7 @@
426
362
  {/each}
427
363
  {/if}
428
364
  <span class="ag-compact-dur">{duration(ev)}</span>
365
+ </EventContent>
429
366
  </div>
430
367
  {/each}
431
368
  {#if day.timedEvents.length > 4}
@@ -642,6 +579,9 @@
642
579
  .ag-wday {
643
580
  border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
644
581
  }
582
+ .ag-wday:last-child {
583
+ border-bottom: none;
584
+ }
645
585
  .ag-wday--today {
646
586
  background: color-mix(in srgb, var(--dt-accent, #2563eb) 2%, transparent);
647
587
  }
@@ -682,7 +622,7 @@
682
622
  }
683
623
  .ag-wday-head-left {
684
624
  display: flex;
685
- align-items: center;
625
+ align-items: baseline;
686
626
  gap: 8px;
687
627
  }
688
628
  .ag-wday-badge {
@@ -706,11 +646,13 @@
706
646
  .ag-wday-name {
707
647
  font-size: 13px;
708
648
  font-weight: 600;
649
+ line-height: 1.2;
709
650
  }
710
651
  .ag-wday-date {
711
652
  font-size: 11px;
712
653
  font-family: var(--dt-mono, monospace);
713
654
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
655
+ line-height: 1.2;
714
656
  }
715
657
 
716
658
  .ag-wday-empty {
@@ -759,16 +701,15 @@
759
701
 
760
702
  /* Compact day events */
761
703
  .ag-wday-compact {
762
- padding: 0 20px 6px;
704
+ padding: 0 20px 8px;
763
705
  }
764
706
  .ag-compact {
765
707
  display: flex;
766
- align-items: center;
708
+ align-items: baseline;
767
709
  gap: 6px;
768
710
  padding: 3px 0;
769
711
  cursor: pointer;
770
712
  min-width: 0;
771
- overflow: hidden;
772
713
  }
773
714
  .ag-compact--selected {
774
715
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
@@ -789,6 +730,7 @@
789
730
  border-radius: 50%;
790
731
  background: var(--ev-color, var(--dt-accent));
791
732
  flex-shrink: 0;
733
+ align-self: center;
792
734
  }
793
735
  .ag-compact-time {
794
736
  font-size: 11px;
@@ -797,6 +739,7 @@
797
739
  min-width: 40px;
798
740
  flex-shrink: 0;
799
741
  white-space: nowrap;
742
+ line-height: 1.4;
800
743
  }
801
744
  .ag-compact-title {
802
745
  font-size: 12px;
@@ -808,6 +751,7 @@
808
751
  overflow: hidden;
809
752
  text-overflow: ellipsis;
810
753
  transition: color 150ms;
754
+ line-height: 1.4;
811
755
  }
812
756
  .ag-compact-dur {
813
757
  font-size: 10px;
@@ -815,6 +759,7 @@
815
759
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
816
760
  flex-shrink: 0;
817
761
  white-space: nowrap;
762
+ line-height: 1.4;
818
763
  }
819
764
  .ag-compact-sub {
820
765
  font-size: 10px;
@@ -824,6 +769,7 @@
824
769
  overflow: hidden;
825
770
  text-overflow: ellipsis;
826
771
  max-width: 120px;
772
+ line-height: 1.4;
827
773
  }
828
774
  .ag-compact-loc {
829
775
  font-size: 9px;
@@ -1,13 +1,6 @@
1
- <script lang="ts">
2
- import MobileDay from './MobileDay.svelte';
3
- import MobileWeek from './MobileWeek.svelte';
4
-
5
- interface Props {
6
- mode?: 'day' | 'week';
7
- [key: string]: unknown;
8
- }
9
-
10
- let { mode = 'day', ...rest }: Props = $props();
1
+ <script lang="ts">import MobileDay from "./MobileDay.svelte";
2
+ import MobileWeek from "./MobileWeek.svelte";
3
+ let { mode = "day", ...rest } = $props();
11
4
  </script>
12
5
 
13
6
  {#if mode === 'day'}