@nomideusz/svelte-calendar 1.1.1 → 1.3.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.
@@ -14,14 +14,15 @@
14
14
  disabled) applies on drop.
15
15
  • Now-line across today's column only; auto-scrolls to the current time.
16
16
  -->
17
- <script lang="ts">import { untrack } from "svelte";
17
+ <script lang="ts">import { tick, untrack } from "svelte";
18
18
  import { useCalendarContext } from "../shared/context.svelte.js";
19
19
  import EventContent from "../shared/EventContent.svelte";
20
20
  import { createClock } from "../../core/clock.svelte.js";
21
21
  import { fitParts } from "../../text-fit.js";
22
22
  import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
23
23
  import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
24
- import { fmtHParts, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
24
+ import { fmtHParts, fmtTime, fmtDuration, fmtWeekRange, weekdayShort, weekdayLong } from "../../core/locale.js";
25
+ import { dateLong } from "../shared/format.js";
25
26
  let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
26
27
  // ── Context ────────────────────────────────────────
27
28
  const ctx = useCalendarContext();
@@ -79,6 +80,8 @@ function inAllDayStrip(ev) {
79
80
  }
80
81
  // ── Config ─────────────────────────────────────────
81
82
  const HOUR_H = 64;
83
+ /** Half a gutter label plus a gap: what a scroll needs above an hour line */
84
+ const LABEL_ROOM = 18;
82
85
  const GUTTER_W = 60;
83
86
  const MIN_COL_W = $derived(ctx.minColumnWidth);
84
87
  const ALLDAY_MAX = 3;
@@ -297,7 +300,8 @@ const nowY = $derived.by(() => {
297
300
  const weekIsEmpty = $derived(!events.some((ev) => ev.start.getTime() < weekEndMs && ev.end.getTime() > weekStartMs));
298
301
  // ── Auto-scroll (mount + week change) ──────────────
299
302
  // Week with today → current time minus ~1h of context; otherwise the
300
- // visibleHours start or 08:00, whichever is later.
303
+ // visibleHours start or 08:00, whichever is later. Lands on a whole hour,
304
+ // with room for its label (centred on the line) below the sticky top.
301
305
  $effect(() => {
302
306
  void weekStartMs;
303
307
  const el = scrollEl;
@@ -310,7 +314,7 @@ $effect(() => {
310
314
  } else {
311
315
  targetHour = Math.max(startHour, Math.min(8, endHour - 1));
312
316
  }
313
- el.scrollTop = (targetHour - startHour) * HOUR_H;
317
+ el.scrollTop = Math.max(0, (Math.floor(targetHour) - startHour) * HOUR_H - LABEL_ROOM);
314
318
  });
315
319
  });
316
320
  // ── Pointer coalescing ─────────────────────────────
@@ -623,6 +627,79 @@ function handleColsClick(e) {
623
627
  end: new Date(startMs + durMin * 6e4)
624
628
  });
625
629
  }
630
+ // ── Keyboard ───────────────────────────────────────
631
+ // The day columns are one tab stop (roving): ←/→ walk the days, Enter or
632
+ // Space adds an event at the next slot (today) or 09:00. A focused event
633
+ // moves with Alt+↑/↓ (one snap) and Alt+←/→ (one shown day); Alt+Shift+↑/↓
634
+ // moves its end. Each key is a one-step drag through ctx.commitDrag, so
635
+ // Calendar validates it exactly as it does a pointer drag.
636
+ let focusedColMs = $state(null);
637
+ const tabbableColMs = $derived((dayCols.find((d) => d.ms === focusedColMs) ?? dayCols.find((d) => d.isToday) ?? dayCols[0])?.ms);
638
+ const colsCreate = $derived(!!oneventcreate && !readOnly);
639
+ function onColKeydown(e, i) {
640
+ if (e.target !== e.currentTarget) return;
641
+ const day = dayCols[i];
642
+ if (e.key === "Enter" || e.key === " ") {
643
+ e.preventDefault();
644
+ if (!oneventcreate || readOnly || day.isDisabled) return;
645
+ const step = Math.max(1, ctx.snapInterval) / 60;
646
+ const want = day.isToday ? snapHour(nowFracHour, "floor") + step : 9;
647
+ const hour = Math.min(Math.max(want, startHour), endHour - step);
648
+ if (isBlockedAt(day.ms, hour)) return;
649
+ const startMs = atHour(day.ms, hour);
650
+ oneventcreate({
651
+ start: new Date(startMs),
652
+ end: new Date(startMs + Math.max(60, minDuration ?? 0) * 6e4)
653
+ });
654
+ return;
655
+ }
656
+ const dir = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
657
+ if (!dir) return;
658
+ e.preventDefault();
659
+ const next = dayCols[i + (isRtl() ? -dir : dir)];
660
+ if (!next) return;
661
+ focusedColMs = next.ms;
662
+ colsEl?.querySelector(`.tw-col[data-day="${next.ms}"]`)?.focus();
663
+ }
664
+ async function onEventKeydown(e, ev) {
665
+ if (e.key === "Enter" || e.key === " ") {
666
+ e.preventDefault();
667
+ e.stopPropagation();
668
+ oneventclick?.(ev, e.currentTarget.getBoundingClientRect());
669
+ return;
670
+ }
671
+ if (!e.altKey || !drag || readOnly || ev.data?.readOnly) return;
672
+ const vert = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0;
673
+ const horiz = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0;
674
+ if (!vert && (!horiz || e.shiftKey)) return;
675
+ let start = ev.start.getTime();
676
+ let end = ev.end.getTime();
677
+ // Up lands on the snap line above (or the one before, when already on
678
+ // one); down on the line below.
679
+ const nudge = (ms) => vert < 0 ? snapMs(ms - 1, "floor") : snapMs(ms, "floor") + SNAP_MS;
680
+ if (e.shiftKey) end = nudge(end);
681
+ else if (vert) {
682
+ start = nudge(start);
683
+ end = start + (ev.end.getTime() - ev.start.getTime());
684
+ } else {
685
+ const fromDay = sod(start);
686
+ const i = dayCols.findIndex((d) => d.ms === fromDay);
687
+ const to = dayCols[i + (isRtl() ? -horiz : horiz)];
688
+ if (i < 0 || !to) return;
689
+ start = atHour(to.ms, hourOf(start, fromDay));
690
+ end = start + (ev.end.getTime() - ev.start.getTime());
691
+ }
692
+ e.preventDefault();
693
+ e.stopPropagation();
694
+ if (e.shiftKey) drag.beginResize(ev.id, "end", ev.start, ev.end);
695
+ else drag.beginMove(ev.id, ev.start, ev.end);
696
+ drag.updatePointer(new Date(start), new Date(end));
697
+ await commitDragCtx?.();
698
+ // A move to another day re-creates the block: keep the keyboard on it,
699
+ // or on its day when the host re-ids it (a recurring copy made one-off).
700
+ await tick();
701
+ (colsEl?.querySelector(`.tw-ev[data-event-id="${CSS.escape(ev.id)}"]`) ?? colsEl?.querySelector(`.tw-col[data-day="${sod(start)}"]`))?.focus();
702
+ }
626
703
  // ── Drag-to-move (vertical = time, horizontal = day) ──
627
704
  const DRAG_THRESHOLD = 5;
628
705
  let evDragStartX = 0;
@@ -823,7 +900,9 @@ function onWindowKeydown(e) {
823
900
  style:--tw-col-min="{MIN_COL_W}px"
824
901
  style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
825
902
  role="region"
826
- aria-label={L.weekAhead}
903
+ aria-label="{singleDay ? L.dayPlanner : L.weekAhead}{dayCols.length
904
+ ? `, ${singleDay ? dateLong(dayCols[0].ms, locale) : fmtWeekRange(dayCols[0].ms, locale, dayCols[dayCols.length - 1].ms)}`
905
+ : ''}"
827
906
  >
828
907
  <div class="tw-scroll" bind:this={scrollEl}>
829
908
  <div class="tw-inner" style:min-width="{innerMinWidth}px">
@@ -929,9 +1008,10 @@ function onWindowKeydown(e) {
929
1008
  {/if}
930
1009
  </div>
931
1010
 
932
- {#each dayCols as day (day.ms)}
1011
+ {#each dayCols as day, di (day.ms)}
933
1012
  {@const positioned = layoutByDay.get(day.ms) ?? []}
934
1013
  {@const ghost = ghostForDay(day.ms)}
1014
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
935
1015
  <div
936
1016
  class="tw-col"
937
1017
  class:tw-col--today={day.isToday}
@@ -939,6 +1019,12 @@ function onWindowKeydown(e) {
939
1019
  class:tw-col--weekend={day.isWeekend}
940
1020
  class:tw-col--disabled={day.isDisabled}
941
1021
  data-day={day.ms}
1022
+ role={colsCreate ? 'group' : undefined}
1023
+ aria-label={colsCreate ? `${dateLong(day.ms, locale)}, ${L.createEvent}` : undefined}
1024
+ aria-keyshortcuts={colsCreate ? 'Enter' : undefined}
1025
+ tabindex={colsCreate ? (day.ms === tabbableColMs ? 0 : -1) : undefined}
1026
+ onfocusin={() => (focusedColMs = day.ms)}
1027
+ onkeydown={colsCreate ? (e) => onColKeydown(e, di) : undefined}
942
1028
  >
943
1029
  <!-- Blocked slot overlays -->
944
1030
  {#if blockedSlots?.length}
@@ -981,6 +1067,7 @@ function onWindowKeydown(e) {
981
1067
  class:tw-ev--past={p.ev.end.getTime() < clock.tick}
982
1068
  class:tw-ev--short={p.height < 44}
983
1069
  class:tw-ev--compact={p.height < 34}
1070
+ class:tw-ev--on-now={weekHasToday && nowY !== null && p.top < nowY && p.top + p.height > nowY}
984
1071
  style:top="{p.top}px"
985
1072
  style:height="{p.height}px"
986
1073
  style:left="calc({(p.col / p.totalCols) * 100}% + 1px)"
@@ -988,11 +1075,13 @@ function onWindowKeydown(e) {
988
1075
  style:--ev-color={p.ev.color ?? 'var(--dt-event, var(--dt-accent))'}
989
1076
  role="button"
990
1077
  tabindex="0"
1078
+ data-event-id={p.ev.id}
1079
+ aria-keyshortcuts={!readOnly && !p.ev.data?.readOnly ? 'Alt+ArrowUp Alt+ArrowDown Alt+ArrowLeft Alt+ArrowRight Alt+Shift+ArrowUp Alt+Shift+ArrowDown' : undefined}
991
1080
  title={p.ev.title}
992
1081
  aria-label="{p.ev.title}, {fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}, {fmtDuration(p.ev.start, p.ev.end)}{statusText(p.ev)}{isCurrent ? ` (${L.inProgress})` : ''}"
993
1082
  onpointerdown={(e) => onEventPointerDown(e, p.ev)}
994
1083
  onpointerenter={() => oneventhover?.(p.ev)}
995
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(p.ev, e.currentTarget.getBoundingClientRect()); } }}
1084
+ onkeydown={(e) => onEventKeydown(e, p.ev)}
996
1085
 
997
1086
  >
998
1087
  <div class="tw-ev-stripe" aria-hidden="true"></div>
@@ -1053,9 +1142,8 @@ function onWindowKeydown(e) {
1053
1142
 
1054
1143
  <!-- Now line (today's column only) -->
1055
1144
  {#if day.isToday && nowY !== null}
1056
- <div class="tw-now" style:top="{nowY}px" aria-label={L.currentTime}>
1057
- <span class="tw-now-dot" aria-hidden="true"></span>
1058
- </div>
1145
+ <div class="tw-now" style:top="{nowY}px" aria-label={L.currentTime}></div>
1146
+ <span class="tw-now-dot" style:top="{nowY}px" aria-hidden="true"></span>
1059
1147
  {/if}
1060
1148
  </div>
1061
1149
  {/each}
@@ -1198,8 +1286,9 @@ function onWindowKeydown(e) {
1198
1286
  padding: 6px 8px 6px 0;
1199
1287
  }
1200
1288
  .tw-ad-gutter-lb {
1201
- font: 500 10px/1.2 var(--dt-sans, system-ui, sans-serif);
1202
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
1289
+ font: 500 12px/1.2 var(--dt-sans, system-ui, sans-serif);
1290
+ letter-spacing: 0.08em;
1291
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.66));
1203
1292
  text-align: right;
1204
1293
  }
1205
1294
 
@@ -1265,7 +1354,7 @@ function onWindowKeydown(e) {
1265
1354
  min-width: 0;
1266
1355
  }
1267
1356
  .tw-ad-span {
1268
- font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
1357
+ font: 400 12.5px/1 var(--dt-sans, system-ui, sans-serif);
1269
1358
  font-variant-numeric: tabular-nums;
1270
1359
  color: inherit;
1271
1360
  opacity: 0.75;
@@ -1298,7 +1387,7 @@ function onWindowKeydown(e) {
1298
1387
  border-radius: 3px;
1299
1388
  text-align: left;
1300
1389
  align-self: flex-start;
1301
- font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
1390
+ font: 400 12.5px/1 var(--dt-sans, system-ui, sans-serif);
1302
1391
  color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1303
1392
  padding: 2px 6px;
1304
1393
  cursor: pointer;
@@ -1427,20 +1516,27 @@ function onWindowKeydown(e) {
1427
1516
  border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1428
1517
  box-sizing: border-box;
1429
1518
  }
1519
+ .tw-col:focus-visible {
1520
+ outline: 2px solid var(--dt-accent, #2563eb);
1521
+ outline-offset: -2px;
1522
+ }
1430
1523
  /* The gutter's right border already bounds the first column */
1431
1524
  .tw-lines + .tw-col { border-left: none; }
1432
1525
 
1433
- .tw-col--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
1526
+ /* --tw-col-wash: the column's ground, repeated under an event that the
1527
+ now line crosses (that event is opaque, so the line runs beneath it). */
1528
+ .tw-col { background: var(--tw-col-wash, transparent); }
1529
+ .tw-col--today { --tw-col-wash: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
1434
1530
  /* Dim past days with a wash, never a subtree opacity (event contrast) */
1435
1531
  .tw-col--past {
1436
- background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
1532
+ --tw-col-wash: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
1437
1533
  }
1438
1534
  /* What has already happened steps back; a cancelled block keeps its own look. */
1439
1535
  .tw-ev--past:not(.tw-ev--cancelled) {
1440
1536
  opacity: 0.78;
1441
1537
  }
1442
1538
  .tw-col--weekend:not(.tw-col--today):not(.tw-col--past) {
1443
- background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012));
1539
+ --tw-col-wash: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012));
1444
1540
  }
1445
1541
  .tw-col--disabled {
1446
1542
  background: repeating-linear-gradient(
@@ -1472,8 +1568,8 @@ function onWindowKeydown(e) {
1472
1568
  overflow: hidden;
1473
1569
  }
1474
1570
  .tw-blocked-lb {
1475
- font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
1476
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
1571
+ font: 500 11px/1 var(--dt-sans, system-ui, sans-serif);
1572
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1477
1573
  text-transform: uppercase;
1478
1574
  letter-spacing: 0.04em;
1479
1575
  white-space: nowrap;
@@ -1481,24 +1577,28 @@ function onWindowKeydown(e) {
1481
1577
  }
1482
1578
 
1483
1579
  /* ─── Now line ───────────────────────────────────── */
1580
+ /* Under the events (z 6): the one it crosses is opaque (.tw-ev--on-now),
1581
+ so the line never strikes through a title. The dot stays on top. */
1484
1582
  .tw-now {
1485
1583
  position: absolute;
1486
1584
  left: 0;
1487
1585
  right: 0;
1488
1586
  height: 0;
1489
1587
  border-top: 2px solid var(--dt-accent, #2563eb);
1490
- z-index: 12;
1588
+ z-index: 5;
1491
1589
  pointer-events: none;
1492
1590
  transform: translateY(-1px);
1493
1591
  }
1494
1592
  .tw-now-dot {
1495
1593
  position: absolute;
1496
1594
  left: -4px;
1497
- top: -5px;
1498
1595
  width: 8px;
1499
1596
  height: 8px;
1597
+ margin-top: -4px;
1500
1598
  border-radius: 50%;
1501
1599
  background: var(--dt-accent, #2563eb);
1600
+ z-index: 12;
1601
+ pointer-events: none;
1502
1602
  }
1503
1603
 
1504
1604
  /* ─── Events ─────────────────────────────────────── */
@@ -1541,6 +1641,17 @@ function onWindowKeydown(e) {
1541
1641
  .tw-ev--current {
1542
1642
  background: var(--dt-accent-dim, rgba(37, 99, 235, 0.12));
1543
1643
  }
1644
+ /* An event the now line crosses lays its column's ground (and its own
1645
+ wash) over the paper, so the line passes beneath its text. */
1646
+ .tw-ev.tw-ev--on-now {
1647
+ background:
1648
+ linear-gradient(var(--tw-ev-wash, transparent), var(--tw-ev-wash, transparent)),
1649
+ linear-gradient(var(--tw-col-wash, transparent), var(--tw-col-wash, transparent)),
1650
+ var(--dt-paper, none),
1651
+ var(--dt-bg, #ffffff);
1652
+ }
1653
+ .tw-ev--on-now.tw-ev--current { --tw-ev-wash: var(--dt-accent-dim, rgba(37, 99, 235, 0.12)); }
1654
+ .tw-ev--on-now:hover { --tw-ev-wash: var(--dt-hover, rgba(0, 0, 0, 0.04)); }
1544
1655
  .tw-ev--resizing {
1545
1656
  z-index: 50;
1546
1657
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
@@ -1592,14 +1703,20 @@ function onWindowKeydown(e) {
1592
1703
  gap: 1px;
1593
1704
  overflow: hidden;
1594
1705
  }
1595
- /* Compact (< ~35min at default zoom): single inline line "9:00 Title" */
1706
+ /* Compact (< ~35min at default zoom): one line, "Title 9:00". The title
1707
+ wins the room: the time wraps onto a second line, which the block
1708
+ clips, before the title is cut; a title too long alone ellipsizes. */
1596
1709
  .tw-ev--compact .tw-ev-body {
1597
1710
  flex-direction: row;
1598
- align-items: center;
1599
- gap: 4px;
1600
- padding-top: 1px;
1601
- padding-bottom: 1px;
1602
- }
1711
+ flex-wrap: wrap;
1712
+ align-content: flex-start;
1713
+ align-items: baseline;
1714
+ gap: 24px 5px;
1715
+ padding-top: 2px;
1716
+ padding-bottom: 0;
1717
+ }
1718
+ .tw-ev--compact .tw-ev-title { flex: 0 1 auto; min-width: 0; }
1719
+ .tw-ev--compact .tw-ev-end { display: none; }
1603
1720
 
1604
1721
  /* Title first, time under it; both one size, rank by weight and ink. */
1605
1722
  .tw-ev-time {
@@ -1610,7 +1727,6 @@ function onWindowKeydown(e) {
1610
1727
  white-space: nowrap;
1611
1728
  flex-shrink: 0;
1612
1729
  }
1613
- .tw-ev--compact .tw-ev-time { order: 0; }
1614
1730
  /* A narrow overlap column keeps the start time whole and drops the end. */
1615
1731
  @container tw-ev (max-width: 104px) {
1616
1732
  .tw-ev-end { display: none; }
@@ -1731,13 +1847,13 @@ function onWindowKeydown(e) {
1731
1847
  box-shadow: none;
1732
1848
  }
1733
1849
  .tw-ghost-time {
1734
- font: 700 12px/1.15 var(--dt-sans, system-ui, sans-serif);
1850
+ font: 500 12.5px/1.15 var(--dt-sans, system-ui, sans-serif);
1735
1851
  font-variant-numeric: tabular-nums;
1736
1852
  color: var(--ev-color, var(--dt-accent, #2563eb));
1737
1853
  white-space: nowrap;
1738
1854
  }
1739
1855
  .tw-ghost-title {
1740
- font: 600 12px/1.2 var(--dt-sans, system-ui, sans-serif);
1856
+ font: 500 12.5px/1.2 var(--dt-sans, system-ui, sans-serif);
1741
1857
  color: var(--dt-text, rgba(0, 0, 0, 0.87));
1742
1858
  white-space: nowrap;
1743
1859
  overflow: hidden;
@@ -18,3 +18,6 @@ export interface TimeSlot {
18
18
  events: TimelineEvent[];
19
19
  }
20
20
  export declare function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[];
21
+ /** A day in full, in the locale's own order: "Monday, October 19" (en-US),
22
+ * "poniedziałek, 19 października" (pl-PL). For screen-reader names. */
23
+ export declare function dateLong(ms: number, locale?: string): string;
@@ -1,4 +1,4 @@
1
- import { fmtTime as _fmtTime, fmtDuration, getLabels } from '../../core/locale.js';
1
+ import { fmtTime as _fmtTime, fmtDuration, getLabels, getDefaultLocale } from '../../core/locale.js';
2
2
  export function fmtTime(d, locale) {
3
3
  return _fmtTime(d, locale);
4
4
  }
@@ -47,3 +47,12 @@ export function groupIntoSlots(evts) {
47
47
  }
48
48
  return slots;
49
49
  }
50
+ /** A day in full, in the locale's own order: "Monday, October 19" (en-US),
51
+ * "poniedziałek, 19 października" (pl-PL). For screen-reader names. */
52
+ export function dateLong(ms, locale) {
53
+ return new Date(ms).toLocaleDateString(locale ?? getDefaultLocale(), {
54
+ weekday: 'long',
55
+ month: 'long',
56
+ day: 'numeric',
57
+ });
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A themeable Svelte 5 calendar: day, week, scrolling-week and month views, agenda lists, touch-first mobile layouts, drag-and-drop, timezones, a headless API and an embeddable web component.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -50,13 +50,11 @@
50
50
  "svelte": "^5.29.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@sveltejs/adapter-auto": "^8.0.0",
54
- "@sveltejs/adapter-vercel": "^7.0.0",
53
+ "@sveltejs/adapter-node": "^6.0.0",
55
54
  "@sveltejs/kit": "^3.0.0",
56
55
  "@sveltejs/package": "^3.0.0",
57
56
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
58
57
  "@types/node": "^22.20.0",
59
- "@vercel/analytics": "^1.6.1",
60
58
  "jsdom": "^29.1.1",
61
59
  "marked": "^17.0.6",
62
60
  "svelte": "^5.57.1",