@nomideusz/svelte-calendar 0.6.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,9 @@
10
10
  -->
11
11
  <script lang="ts">
12
12
  import { onMount, tick, untrack } from 'svelte';
13
+ import { flip } from 'svelte/animate';
14
+ import { crossfade } from 'svelte/transition';
15
+ import { prefersReducedMotion } from 'svelte/motion';
13
16
  import { useCalendarContext } from '../shared/context.svelte.js';
14
17
  import { createClock } from '../../core/clock.svelte.js';
15
18
  import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
@@ -51,11 +54,15 @@
51
54
 
52
55
  const ctx = useCalendarContext();
53
56
  const clock = createClock();
57
+
58
+ // Drag ghost flies between day cells instead of teleporting.
59
+ // No fallback: without a counterpart (drag start/end) it appears/disappears instantly.
60
+ const ANIM = $derived(prefersReducedMotion.current ? 0 : 180);
61
+ const [previewSend, previewReceive] = crossfade({ duration: () => (prefersReducedMotion.current ? 0 : 160) });
54
62
  const drag = $derived(ctx.drag);
55
63
  const commitDragCtx = $derived(ctx.commitDrag);
56
64
  const viewState = $derived(ctx.viewState);
57
65
  const loadRangeCtx = $derived(ctx.loadRange);
58
- const showNav = $derived(ctx.showNav);
59
66
  const equalDays = $derived(ctx.equalDays);
60
67
  const showDates = $derived(ctx.showDates);
61
68
  const hideDays = $derived(ctx.hideDays);
@@ -79,7 +86,6 @@
79
86
  let lastExternalMs = _initMs;
80
87
 
81
88
  let el: HTMLDivElement;
82
- let scrolled = $state(false);
83
89
 
84
90
  function scrollWeekIntoContainer(targetMs?: number, behavior: ScrollBehavior = 'auto') {
85
91
  if (!el) return;
@@ -231,24 +237,31 @@
231
237
  if (ext !== lastExternalMs) {
232
238
  lastExternalMs = ext;
233
239
  internalFocusMs = ext;
234
- tick().then(() => scrollWeekIntoContainer(ext, 'smooth'));
240
+ tick().then(() => scrollWeekIntoContainer(ext));
235
241
  }
236
242
  });
237
243
 
238
- // ─── Scroll state ─────────────────────────────────────
239
- function isCurrentWeekVisible(): boolean {
240
- if (!el) return false;
241
- const current = el.querySelector<HTMLElement>('.wg-week--current');
242
- if (!current) return false;
243
- const top = current.offsetTop - el.scrollTop;
244
- const bottom = top + current.offsetHeight;
245
- return bottom > 0 && top < el.clientHeight;
244
+ /** Push the week at the viewport centre to viewState (drives the header label + Today button). */
245
+ function syncFocusFromScroll() {
246
+ if (!el || !viewState) return;
247
+ const centerY = el.scrollTop + el.clientHeight / 2;
248
+ const rows = el.querySelectorAll<HTMLElement>('[data-week]');
249
+ for (const row of rows) {
250
+ if (row.offsetTop + row.offsetHeight >= centerY) {
251
+ const ms = Number(row.dataset.week);
252
+ if (Number.isFinite(ms) && ms !== lastExternalMs) {
253
+ lastExternalMs = ms;
254
+ viewState.setFocusDate(new Date(ms));
255
+ }
256
+ return;
257
+ }
258
+ }
246
259
  }
247
260
 
248
261
  let extending = false;
249
262
 
250
263
  function handleUserScroll() {
251
- scrolled = !isCurrentWeekVisible();
264
+ syncFocusFromScroll();
252
265
  if (!el || extending) return;
253
266
  // Extend buffer when user scrolls near the edge
254
267
  if (el.scrollTop < EDGE_PX) {
@@ -268,16 +281,6 @@
268
281
  }
269
282
  }
270
283
 
271
- function jumpToday() {
272
- internalFocusMs = clock.today;
273
- lastExternalMs = clock.today;
274
- viewState?.goToday();
275
- scrolled = false;
276
- tick().then(() => {
277
- scrollWeekIntoContainer(clock.today, 'smooth');
278
- });
279
- }
280
-
281
284
  function handleDayCellClick(ms: number, e: Event) {
282
285
  const target = e.target as HTMLElement;
283
286
  if (target.closest('.wg-ev')) return;
@@ -317,17 +320,27 @@
317
320
  return day.events.filter((ev) => ev.id !== dragPreviewEvent.id);
318
321
  }
319
322
 
323
+ // Crossfade keys for the previews, snapshotted at render time. Transition
324
+ // params are evaluated lazily at unmount — after the drag payload is
325
+ // already null — so they must never read the reactive preview directly.
326
+ // Plain Map (not $state): written during render, read only by transitions.
327
+ const previewKeySnapshot = new Map<number | 'timed', string>();
328
+
320
329
  function dragPreviewTimedForDay(dayMs: number): TimelineEvent | null {
321
330
  const ev = dragPreviewEvent;
322
331
  if (!ev || isAllDay(ev) || isMultiDay(ev)) return null;
323
332
  const dayEnd = dayMs + DAY_MS;
324
- return ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs ? ev : null;
333
+ const hit = ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs;
334
+ if (hit) previewKeySnapshot.set('timed', ev.id);
335
+ return hit ? ev : null;
325
336
  }
326
337
 
327
338
  function dragPreviewSegmentForDay(dayMs: number): DaySegment | null {
328
339
  const ev = dragPreviewEvent;
329
340
  if (!ev || (!isAllDay(ev) && !isMultiDay(ev))) return null;
330
- return segmentForDay(ev, dayMs);
341
+ const seg = segmentForDay(ev, dayMs);
342
+ if (seg) previewKeySnapshot.set(dayMs, `${ev.id}:${seg.dayIndex}`);
343
+ return seg;
331
344
  }
332
345
 
333
346
  function getCellWidth(): number {
@@ -495,6 +508,9 @@
495
508
  <div class="wg-allday">
496
509
  {#each visibleAllDaySegments as seg (seg.ev.id)}
497
510
  <div
511
+ animate:flip={{ duration: ANIM }}
512
+ in:previewReceive={{ key: `${seg.ev.id}:${seg.dayIndex}` }}
513
+ out:previewSend={{ key: `${seg.ev.id}:${seg.dayIndex}` }}
498
514
  class="wg-ad"
499
515
  class:wg-ad--start={seg.isStart}
500
516
  class:wg-ad--end={seg.isEnd}
@@ -511,6 +527,8 @@
511
527
  </div>
512
528
  {/each}
513
529
  {#if previewSegment}
530
+ <!-- key by event id + dayIndex: multi-day previews render one segment
531
+ per cell, and each pairs with its own real-card counterpart -->
514
532
  <div
515
533
  class="wg-ad wg-ad--drag-preview"
516
534
  class:wg-ad--start={previewSegment.isStart}
@@ -518,6 +536,8 @@
518
536
  class:wg-ad--mid={!previewSegment.isStart && !previewSegment.isEnd}
519
537
  style:--ev-color={previewSegment.ev.color ?? 'var(--dt-accent)'}
520
538
  aria-hidden="true"
539
+ in:previewReceive={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
540
+ out:previewSend={{ key: previewKeySnapshot.get(day.ms) ?? '' }}
521
541
  >
522
542
  {@render allDaySegmentContent(previewSegment)}
523
543
  </div>
@@ -528,7 +548,12 @@
528
548
  <!-- Timed events -->
529
549
  <div class="wg-cell-events">
530
550
  {#each visibleTimedEvents.slice(0, MAX_EVENTS_SHOWN) as ev (ev.id)}
551
+ <!-- send/receive keyed by event id pair the card with the drag ghost:
552
+ drag start morphs card → ghost, drop morphs ghost → placed card -->
531
553
  <div
554
+ animate:flip={{ duration: ANIM }}
555
+ in:previewReceive={{ key: ev.id }}
556
+ out:previewSend={{ key: ev.id }}
532
557
  class="wg-ev"
533
558
  class:wg-ev--selected={selectedEventId === ev.id}
534
559
  class:wg-ev--current={ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
@@ -554,6 +579,8 @@
554
579
  class="wg-ev wg-ev--drag-preview"
555
580
  style:--ev-color={previewTimedEvent.color ?? 'var(--dt-accent)'}
556
581
  aria-hidden="true"
582
+ in:previewReceive={{ key: previewKeySnapshot.get('timed') ?? '' }}
583
+ out:previewSend={{ key: previewKeySnapshot.get('timed') ?? '' }}
557
584
  >
558
585
  {@render timedEventContent(previewTimedEvent)}
559
586
  </div>
@@ -570,17 +597,6 @@
570
597
  {/each}
571
598
  </div>
572
599
 
573
- {#if showNav && scrolled}
574
- <nav class="wg-nav" aria-label={L.weekNavigation}>
575
- <button
576
- class="wg-nav-pill"
577
- onclick={jumpToday}
578
- aria-label={L.goToToday}
579
- >
580
- {L.today}
581
- </button>
582
- </nav>
583
- {/if}
584
600
  </div>
585
601
 
586
602
  <style>
@@ -601,9 +617,8 @@
601
617
  overflow-y: auto;
602
618
  overflow-x: hidden;
603
619
  box-sizing: border-box;
604
- padding-top: 48px;
605
620
  scrollbar-width: thin;
606
- scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.08)) transparent;
621
+ scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
607
622
  }
608
623
  .wg--auto .wg-body { overflow-y: visible; }
609
624
 
@@ -624,7 +639,7 @@
624
639
  }
625
640
 
626
641
  .wg-week--current {
627
- background: var(--dt-today-bg, rgba(239, 68, 68, 0.02));
642
+ background: var(--dt-today-bg, rgba(37, 99, 235, 0.04));
628
643
  border: 2.5px solid var(--dt-accent, #2563eb);
629
644
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--dt-accent, #2563eb) 15%, transparent);
630
645
  }
@@ -648,7 +663,7 @@
648
663
  position: relative;
649
664
  min-height: 170px;
650
665
  padding: 4px 4px 8px;
651
- border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.06));
666
+ border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
652
667
  cursor: pointer;
653
668
  transition: background 0.15s;
654
669
  }
@@ -656,8 +671,8 @@
656
671
  .wg-cell:last-child { border-right: none; }
657
672
  .wg-cell:hover { background: var(--dt-hover, rgba(0, 0, 0, 0.015)); }
658
673
 
659
- .wg-cell--today { background: var(--dt-today-bg, rgba(239, 68, 68, 0.03)); }
660
- .wg-cell--today:hover { background: rgba(239, 68, 68, 0.05); }
674
+ .wg-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
675
+ .wg-cell--today:hover { background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent); }
661
676
 
662
677
  /* Dim all cells by default; current week overrides to full brightness */
663
678
  .wg-cell { opacity: 0.55; }
@@ -676,8 +691,8 @@
676
691
  45deg,
677
692
  transparent,
678
693
  transparent 6px,
679
- var(--dt-border, rgba(0, 0, 0, 0.06)) 6px,
680
- var(--dt-border, rgba(0, 0, 0, 0.06)) 7px
694
+ var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
695
+ var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
681
696
  ) !important;
682
697
  }
683
698
 
@@ -690,8 +705,8 @@
690
705
  border-radius: 3px;
691
706
  background: repeating-linear-gradient(
692
707
  -45deg,
693
- color-mix(in srgb, var(--dt-text, rgba(0,0,0,0.85)) 4%, transparent),
694
- color-mix(in srgb, var(--dt-text, rgba(0,0,0,0.85)) 4%, transparent) 3px,
708
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
709
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 3px,
695
710
  transparent 3px,
696
711
  transparent 6px
697
712
  );
@@ -701,7 +716,7 @@
701
716
 
702
717
  .wg-blocked-label {
703
718
  font: 500 8px/1 var(--dt-sans, system-ui, sans-serif);
704
- color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
719
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
705
720
  text-transform: uppercase;
706
721
  letter-spacing: 0.04em;
707
722
  white-space: nowrap;
@@ -726,11 +741,11 @@
726
741
  font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
727
742
  letter-spacing: 0.04em;
728
743
  text-transform: uppercase;
729
- color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
744
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
730
745
  }
731
746
 
732
747
  .wg-week--current .wg-day-wd {
733
- color: var(--dt-text-2, rgba(0, 0, 0, 0.5));
748
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
734
749
  }
735
750
 
736
751
  .wg-cell-hd--today .wg-day-wd {
@@ -740,11 +755,11 @@
740
755
 
741
756
  .wg-day-num {
742
757
  font: 700 14px / 1 var(--dt-sans, system-ui, sans-serif);
743
- color: var(--dt-text, rgba(0, 0, 0, 0.85));
758
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
744
759
  }
745
760
 
746
761
  .wg-week--current .wg-day-num {
747
- color: var(--dt-text, rgba(0, 0, 0, 0.95));
762
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
748
763
  }
749
764
 
750
765
  .wg-day-num--today {
@@ -762,13 +777,13 @@
762
777
  font: 800 22px / 1 var(--dt-sans, system-ui, sans-serif);
763
778
  letter-spacing: 0.02em;
764
779
  text-transform: uppercase;
765
- color: color-mix(in srgb, var(--dt-text, rgba(255,255,255,0.85)) 4%, transparent);
780
+ color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
766
781
  pointer-events: none;
767
782
  white-space: nowrap;
768
783
  }
769
784
 
770
785
  .wg-week--current .wg-cell-month {
771
- color: color-mix(in srgb, var(--dt-text, rgba(255,255,255,0.85)) 8%, transparent);
786
+ color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 8%, transparent);
772
787
  }
773
788
 
774
789
  /* ─── All-day / multi-day events ─────────────────── */
@@ -826,7 +841,7 @@
826
841
 
827
842
  .wg-ad-title {
828
843
  font: 500 10px / 1.1 var(--dt-sans, system-ui, sans-serif);
829
- color: var(--dt-text, rgba(0, 0, 0, 0.85));
844
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
830
845
  white-space: nowrap;
831
846
  overflow: hidden;
832
847
  text-overflow: ellipsis;
@@ -915,14 +930,14 @@
915
930
 
916
931
  .wg-ev-time {
917
932
  font: 400 10px / 1 var(--dt-sans, system-ui, sans-serif);
918
- color: var(--dt-text-3, rgba(0, 0, 0, 0.4));
933
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
919
934
  flex-shrink: 0;
920
935
  white-space: nowrap;
921
936
  }
922
937
 
923
938
  .wg-ev-title {
924
939
  font: 500 12px / 1.1 var(--dt-sans, system-ui, sans-serif);
925
- color: var(--dt-text, rgba(0, 0, 0, 0.85));
940
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
926
941
  white-space: nowrap;
927
942
  overflow: hidden;
928
943
  text-overflow: ellipsis;
@@ -930,7 +945,7 @@
930
945
 
931
946
  .wg-ev-loc {
932
947
  font: 400 9px / 1 var(--dt-sans, system-ui, sans-serif);
933
- color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
948
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
934
949
  white-space: nowrap;
935
950
  overflow: hidden;
936
951
  text-overflow: ellipsis;
@@ -939,56 +954,13 @@
939
954
 
940
955
  .wg-ev-more {
941
956
  font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
942
- color: var(--dt-text-3, rgba(0, 0, 0, 0.35));
957
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
943
958
  padding: 2px 8px;
944
959
  cursor: pointer;
945
960
  }
946
961
 
947
962
  .wg-ev-more:hover {
948
- color: var(--dt-text-2, rgba(0, 0, 0, 0.55));
949
- }
950
-
951
- /* ─── Navigation pill ────────────────────────────── */
952
- .wg-nav {
953
- position: absolute;
954
- top: 22px;
955
- right: 14px;
956
- z-index: 20;
957
- display: flex;
958
- gap: 2px;
959
- background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
960
- backdrop-filter: blur(6px);
961
- -webkit-backdrop-filter: blur(6px);
962
- border-radius: 8px;
963
- padding: 2px;
964
- border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
965
- animation: wg-nav-in 200ms ease both;
966
- }
967
- @keyframes wg-nav-in {
968
- from { opacity: 0; transform: translateY(-6px); }
969
- to { opacity: 1; transform: translateY(0); }
970
- }
971
- .wg-nav-pill {
972
- border: none;
973
- background: transparent;
974
- color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
975
- cursor: pointer;
976
- font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
977
- padding: 6px 12px;
978
- border-radius: 6px;
979
- letter-spacing: 0.04em;
980
- text-transform: uppercase;
981
- transition: background 100ms, color 100ms;
982
- display: inline-flex;
983
- align-items: center;
984
- justify-content: center;
985
- }
986
- .wg-nav-pill:hover {
987
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
988
- }
989
- .wg-nav-pill:focus-visible {
990
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
991
- outline-offset: 2px;
963
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
992
964
  }
993
965
 
994
966
  /* ─── Focus-visible ──────────────────────────────── */
@@ -7,7 +7,6 @@ export interface CalendarContext {
7
7
  readonly drag: DragState | undefined;
8
8
  readonly commitDrag: (() => void) | undefined;
9
9
  readonly snapInterval: number;
10
- readonly showNav: boolean;
11
10
  readonly equalDays: boolean;
12
11
  readonly showDates: boolean;
13
12
  readonly hideDays: number[] | undefined;
@@ -18,7 +18,6 @@ export function useCalendarContext() {
18
18
  get drag() { return raw?.drag; },
19
19
  get commitDrag() { return raw?.commitDrag; },
20
20
  get snapInterval() { return raw?.snapInterval ?? 15; },
21
- get showNav() { return raw?.showNavigation ?? true; },
22
21
  get equalDays() { return raw?.equalDays ?? false; },
23
22
  get showDates() { return raw?.showDates ?? true; },
24
23
  get hideDays() { return raw?.hideDays; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "0.6.9",
3
+ "version": "0.7.1",
4
4
  "description": "A themeable Svelte 5 calendar with Day and Week views — Planner and Agenda.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,8 +34,8 @@
34
34
  "test:watch": "vitest"
35
35
  },
36
36
  "peerDependencies": {
37
- "svelte": "^5.0.0",
38
- "@chenglou/pretext": "^0.0.5"
37
+ "@chenglou/pretext": "^0.0.5",
38
+ "svelte": "^5.0.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@chenglou/pretext": {
@@ -44,22 +44,22 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@chenglou/pretext": "^0.0.5",
47
- "@sveltejs/adapter-auto": "^7.0.0",
48
- "@sveltejs/kit": "^2.60.1",
49
- "@sveltejs/package": "^2.5.7",
47
+ "@sveltejs/adapter-auto": "^7.0.1",
48
+ "@sveltejs/kit": "^2.69.1",
49
+ "@sveltejs/package": "^2.5.8",
50
50
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
51
- "@types/node": "^22.0.0",
51
+ "@types/node": "^22.20.0",
52
52
  "@vercel/analytics": "^1.6.1",
53
- "jsdom": "^29.0.0",
54
- "marked": "^17.0.3",
55
- "svelte": "^5.51.0",
56
- "svelte-check": "^4.3.6",
53
+ "jsdom": "^29.1.1",
54
+ "marked": "^17.0.6",
55
+ "svelte": "^5.56.4",
56
+ "svelte-check": "^4.7.1",
57
57
  "typescript": "^5.9.3",
58
- "vite": "^7.3.5",
59
- "vitest": "^4.1.0"
58
+ "vite": "^7.3.6",
59
+ "vitest": "^4.1.9"
60
60
  },
61
61
  "dependencies": {
62
- "date-fns": "^4.1.0",
62
+ "date-fns": "^4.4.0",
63
63
  "date-fns-tz": "^3.2.0"
64
64
  },
65
65
  "keywords": [