@intinyagroup/calendar 0.1.1-alpha.22 → 0.1.1-alpha.23

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.
@@ -1,13 +1,23 @@
1
1
  <script lang="ts">
2
- import * as Popover from '@intinyagroup/ui';
3
- import { Button, Input } from '@intinyagroup/ui';
4
- import Calendar, { type CalendarDate } from './Calendar.svelte';
5
- import { Calendar as CalendarIcon, X } from 'lucide-svelte';
6
- import { cn } from '@intinyagroup/ui/utils';
2
+ import {
3
+ Popover as PopoverRoot,
4
+ PopoverTrigger,
5
+ PopoverContent,
6
+ Button,
7
+ Input,
8
+ } from "@intinyagroup/ui";
9
+ const Popover = {
10
+ Root: PopoverRoot,
11
+ Trigger: PopoverTrigger,
12
+ Content: PopoverContent,
13
+ };
14
+ import Calendar, { type CalendarDate } from "./Calendar.svelte";
15
+ import { Calendar as CalendarIcon, X } from "lucide-svelte";
16
+ import { cn } from "@intinyagroup/ui/utils";
7
17
 
8
18
  let {
9
19
  value = $bindable(null),
10
- placeholder = 'Select date...',
20
+ placeholder = "Select date...",
11
21
  disabled = false,
12
22
  class: className,
13
23
  onSelect,
@@ -22,9 +32,13 @@
22
32
  let open = $state(false);
23
33
 
24
34
  function formatDateDisplay(date: CalendarDate | null): string {
25
- if (!date) return '';
35
+ if (!date) return "";
26
36
  const d = new Date(date.year, date.month, date.day);
27
- return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
37
+ return d.toLocaleDateString("en-GB", {
38
+ day: "numeric",
39
+ month: "short",
40
+ year: "numeric",
41
+ });
28
42
  }
29
43
 
30
44
  function handleSelect(date: CalendarDate) {
@@ -45,14 +59,14 @@
45
59
  <button
46
60
  {...props}
47
61
  class={cn(
48
- 'flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 py-2 text-sm text-[var(--ui-foreground)] transition-colors cursor-pointer',
49
- 'hover:border-[var(--ui-primary)]/50 focus:border-[var(--ui-primary)] focus:ring-2 focus:ring-[var(--ui-ring)]/20',
50
- disabled && 'opacity-50 cursor-not-allowed',
51
- className
62
+ "flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 py-2 text-sm text-[var(--ui-foreground)] transition-colors cursor-pointer",
63
+ "hover:border-[var(--ui-primary)]/50 focus:border-[var(--ui-primary)] focus:ring-2 focus:ring-[var(--ui-ring)]/20",
64
+ disabled && "opacity-50 cursor-not-allowed",
65
+ className,
52
66
  )}
53
67
  {disabled}
54
68
  >
55
- <span class={cn(!value && 'text-[var(--ui-muted-foreground)]')}>
69
+ <span class={cn(!value && "text-[var(--ui-muted-foreground)]")}>
56
70
  {value ? formatDateDisplay(value) : placeholder}
57
71
  </span>
58
72
  <CalendarIcon class="size-4 text-[var(--ui-muted-foreground)]" />
@@ -1,4 +1,4 @@
1
- import { type CalendarDate } from './Calendar.svelte';
1
+ import { type CalendarDate } from "./Calendar.svelte";
2
2
  type $$ComponentProps = {
3
3
  value?: CalendarDate | null;
4
4
  placeholder?: string;
@@ -329,6 +329,7 @@
329
329
 
330
330
  <div
331
331
  role="gridcell"
332
+ tabindex="-1"
332
333
  aria-selected={isEdge}
333
334
  class={cn(
334
335
  "relative py-0.5 flex items-center justify-center",
@@ -27,8 +27,13 @@
27
27
  format,
28
28
  } from "date-fns";
29
29
  import { id as localeId, enUS as localeEn } from "date-fns/locale";
30
+ import {
31
+ expandRecurringEvents,
32
+ type CalendarRecurrence,
33
+ type CalendarResource,
34
+ } from "./recurrence";
30
35
 
31
- export type CalendarView = "month" | "week" | "day";
36
+ export type CalendarView = "month" | "week" | "day" | "resourceTimeline";
32
37
 
33
38
  export type CalendarEvent = {
34
39
  id: string;
@@ -38,6 +43,10 @@
38
43
  color?: string;
39
44
  description?: string;
40
45
  allDay?: boolean;
46
+ /** Optional minimal recurrence. See CalendarRecurrence for supported semantics. */
47
+ recurrence?: CalendarRecurrence;
48
+ /** Resource row used by resourceTimeline view. */
49
+ resourceId?: string;
41
50
  };
42
51
 
43
52
  let {
@@ -48,6 +57,9 @@
48
57
  timeZone,
49
58
  firstDayOfWeek = 0, // 0 = Sunday, 1 = Monday
50
59
  enableEventModal = true,
60
+ resources = [],
61
+ resourceTimelineStartHour = 0,
62
+ resourceTimelineEndHour = 24,
51
63
  class: className,
52
64
  onEventClick,
53
65
  onDateClick,
@@ -62,6 +74,9 @@
62
74
  timeZone?: string;
63
75
  firstDayOfWeek?: number;
64
76
  enableEventModal?: boolean;
77
+ resources?: CalendarResource[];
78
+ resourceTimelineStartHour?: number;
79
+ resourceTimelineEndHour?: number;
65
80
  class?: string;
66
81
  onEventClick?: (event: CalendarEvent) => void;
67
82
  onDateClick?: (date: Date) => void;
@@ -300,6 +315,78 @@
300
315
  window.addEventListener("mousemove", onMouseMove);
301
316
  window.addEventListener("mouseup", onMouseUp);
302
317
  }
318
+ const visibleRange = $derived.by(() => {
319
+ if (view === "month") {
320
+ const start = startOfWeek(startOfMonth(currentDate), { weekStartsOn });
321
+ const end = endOfWeek(endOfMonth(currentDate), { weekStartsOn });
322
+ return { start, end };
323
+ }
324
+ if (view === "week") {
325
+ return {
326
+ start: startOfWeek(currentDate, { weekStartsOn }),
327
+ end: endOfWeek(currentDate, { weekStartsOn }),
328
+ };
329
+ }
330
+ return {
331
+ start: new Date(
332
+ currentDate.getFullYear(),
333
+ currentDate.getMonth(),
334
+ currentDate.getDate(),
335
+ ),
336
+ end: new Date(
337
+ currentDate.getFullYear(),
338
+ currentDate.getMonth(),
339
+ currentDate.getDate() + 1,
340
+ ),
341
+ };
342
+ });
343
+
344
+ // Recurrence expansion stays viewport-bounded, preserving existing event APIs.
345
+ const displayEvents = $derived(
346
+ expandRecurringEvents(events, visibleRange.start, visibleRange.end),
347
+ );
348
+
349
+ function getResourceEvents(resourceId: string): CalendarEvent[] {
350
+ return displayEvents.filter((event) => {
351
+ if (event.resourceId !== resourceId) return false;
352
+ const start = new Date(event.start);
353
+ const end = new Date(event.end);
354
+ const dayStart = new Date(
355
+ currentDate.getFullYear(),
356
+ currentDate.getMonth(),
357
+ currentDate.getDate(),
358
+ );
359
+ const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);
360
+ return end >= dayStart && start <= dayEnd;
361
+ });
362
+ }
363
+
364
+ function timelinePosition(date: Date): number {
365
+ const start = Math.min(23, Math.max(0, resourceTimelineStartHour));
366
+ const end = Math.max(start + 1, Math.min(24, resourceTimelineEndHour));
367
+ const total = (end - start) * 60;
368
+ const local = getTimeInZone(date).hours * 60 + getTimeInZone(date).minutes;
369
+ return ((local - start * 60) / total) * 100;
370
+ }
371
+
372
+ function timelineWidth(event: CalendarEvent): number {
373
+ const start = Math.min(23, Math.max(0, resourceTimelineStartHour));
374
+ const end = Math.max(start + 1, Math.min(24, resourceTimelineEndHour));
375
+ const total = (end - start) * 60;
376
+ const begin = getTimeInZone(new Date(event.start));
377
+ const finish = getTimeInZone(new Date(event.end));
378
+ const duration = Math.max(
379
+ 15,
380
+ finish.hours * 60 + finish.minutes - (begin.hours * 60 + begin.minutes),
381
+ );
382
+ return (duration / total) * 100;
383
+ }
384
+
385
+ const timelineHours = $derived.by(() => {
386
+ const start = Math.min(23, Math.max(0, resourceTimelineStartHour));
387
+ const end = Math.max(start + 1, Math.min(24, resourceTimelineEndHour));
388
+ return Array.from({ length: end - start }, (_, index) => start + index);
389
+ });
303
390
 
304
391
  type LayoutEvent = CalendarEvent & {
305
392
  colIndex: number;
@@ -421,9 +508,8 @@
421
508
  });
422
509
 
423
510
  function getEventsForDay(d: Date): CalendarEvent[] {
424
- return events.filter((ev) => {
511
+ return displayEvents.filter((ev) => {
425
512
  if (isSameDay(new Date(ev.start), d)) return true;
426
- // Multi-day spanning: check if date falls between start and end
427
513
  const s = new Date(ev.start);
428
514
  const e = new Date(ev.end);
429
515
  return d >= s && d <= e;
@@ -431,13 +517,13 @@
431
517
  }
432
518
 
433
519
  function getTimedEventsForDay(d: Date): CalendarEvent[] {
434
- return events.filter(
520
+ return displayEvents.filter(
435
521
  (ev) => !isMultiDayOrAllDay(ev) && isSameDay(new Date(ev.start), d),
436
522
  );
437
523
  }
438
524
 
439
525
  function getAllDayEventsForWeek(days: Date[]): CalendarEvent[] {
440
- return events.filter((ev) => {
526
+ return displayEvents.filter((ev) => {
441
527
  if (!isMultiDayOrAllDay(ev)) return false;
442
528
  const s = new Date(ev.start);
443
529
  const e = new Date(ev.end);
@@ -626,6 +712,19 @@
626
712
  <Clock class="size-4 sm:hidden" />
627
713
  <span class="hidden sm:inline">Day</span>
628
714
  </button>
715
+ <button
716
+ type="button"
717
+ onclick={() => (view = "resourceTimeline")}
718
+ class={cn(
719
+ "rounded-md px-2 sm:px-3 py-1 text-xs font-semibold transition-colors cursor-pointer",
720
+ view === "resourceTimeline"
721
+ ? "bg-[var(--ui-card)] text-[var(--ui-foreground)] shadow-xs"
722
+ : "text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)]",
723
+ )}
724
+ >
725
+ <span class="hidden sm:inline">Resources</span>
726
+ <span class="sm:hidden">Rows</span>
727
+ </button>
629
728
  </div>
630
729
 
631
730
  {#if enableEventModal}
@@ -957,7 +1056,7 @@
957
1056
  Swipe to see more days
958
1057
  </div>
959
1058
  {/if}
960
- {:else}
1059
+ {:else if view === "day"}
961
1060
  <!-- DAY VIEW -->
962
1061
  <div
963
1062
  class="p-3 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/20 flex items-center justify-between"
@@ -1098,8 +1197,106 @@
1098
1197
  {/if}
1099
1198
  </div>
1100
1199
  </div>
1200
+ {:else if view === "resourceTimeline"}
1201
+ <div
1202
+ class="border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/20 px-4 py-2 text-xs text-[var(--ui-muted-foreground)]"
1203
+ >
1204
+ Resource timeline · {format(currentDate, "dd MMMM yyyy", {
1205
+ locale: activeDateFnsLocale,
1206
+ })}
1207
+ </div>
1208
+ {#if resources.length === 0}
1209
+ <div class="p-8 text-center text-sm text-[var(--ui-muted-foreground)]">
1210
+ Add resources to render warehouse, user, or room schedules.
1211
+ </div>
1212
+ {:else}
1213
+ <div class="overflow-x-auto">
1214
+ <div class="min-w-[760px]">
1215
+ <div
1216
+ class="grid grid-cols-[180px_1fr] border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/20 text-[10px] font-semibold text-[var(--ui-muted-foreground)]"
1217
+ >
1218
+ <div class="border-r border-[var(--ui-border)] px-3 py-2">
1219
+ Resource
1220
+ </div>
1221
+ <div
1222
+ class="relative grid"
1223
+ style="grid-template-columns: repeat({timelineHours.length}, minmax(0, 1fr));"
1224
+ >
1225
+ {#each timelineHours as hour}
1226
+ <div
1227
+ class="border-r border-[var(--ui-border)] px-1 py-2 text-center"
1228
+ >
1229
+ {String(hour).padStart(2, "0")}:00
1230
+ </div>
1231
+ {/each}
1232
+ </div>
1233
+ </div>
1234
+ {#each resources as resource (resource.id)}
1235
+ {@const resourceEvents = getResourceEvents(resource.id)}
1236
+ <div
1237
+ class="grid min-h-16 grid-cols-[180px_1fr] border-b border-[var(--ui-border)]"
1238
+ >
1239
+ <div class="border-r border-[var(--ui-border)] px-3 py-3">
1240
+ <div class="text-xs font-semibold text-[var(--ui-foreground)]">
1241
+ {resource.label}
1242
+ </div>
1243
+ {#if resource.description}<div
1244
+ class="mt-0.5 truncate text-[10px] text-[var(--ui-muted-foreground)]"
1245
+ >
1246
+ {resource.description}
1247
+ </div>{/if}
1248
+ </div>
1249
+ <div
1250
+ role="button"
1251
+ tabindex="0"
1252
+ class="relative min-h-16 bg-[var(--ui-card)]"
1253
+ onclick={() => onDateClick?.(currentDate)}
1254
+ onkeydown={(event) => {
1255
+ if (event.key === "Enter" || event.key === " ") {
1256
+ event.preventDefault();
1257
+ onDateClick?.(currentDate);
1258
+ }
1259
+ }}
1260
+ >
1261
+ <div
1262
+ class="pointer-events-none absolute inset-0 grid"
1263
+ style="grid-template-columns: repeat({timelineHours.length}, minmax(0, 1fr));"
1264
+ >
1265
+ {#each timelineHours as _hour}<div
1266
+ class="border-r border-[var(--ui-border)]/50"
1267
+ ></div>{/each}
1268
+ </div>
1269
+ {#each resourceEvents as ev (ev.id)}
1270
+ <button
1271
+ type="button"
1272
+ class="absolute top-2 h-12 min-w-4 truncate rounded px-2 text-left text-[10px] font-semibold text-white shadow-xs hover:opacity-90"
1273
+ style="left: {Math.max(
1274
+ 0,
1275
+ timelinePosition(new Date(ev.start)),
1276
+ )}%; width: {Math.max(
1277
+ 2,
1278
+ Math.min(100, timelineWidth(ev)),
1279
+ )}%; background-color: {ev.color ||
1280
+ resource.color ||
1281
+ 'var(--ui-primary)'};"
1282
+ onclick={(event) => {
1283
+ event.stopPropagation();
1284
+ onEventClick?.(ev);
1285
+ }}
1286
+ >
1287
+ <span>{ev.title}</span>
1288
+ <span class="ml-1 font-normal opacity-80"
1289
+ >{formatTime(ev.start)}</span
1290
+ >
1291
+ </button>
1292
+ {/each}
1293
+ </div>
1294
+ </div>
1295
+ {/each}
1296
+ </div>
1297
+ </div>
1298
+ {/if}
1101
1299
  {/if}
1102
-
1103
1300
  <!-- Quick Event Creation Modal -->
1104
1301
  {#if modalOpen}
1105
1302
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -1,4 +1,5 @@
1
- export type CalendarView = "month" | "week" | "day";
1
+ import { type CalendarRecurrence, type CalendarResource } from "./recurrence";
2
+ export type CalendarView = "month" | "week" | "day" | "resourceTimeline";
2
3
  export type CalendarEvent = {
3
4
  id: string;
4
5
  title: string;
@@ -7,6 +8,10 @@ export type CalendarEvent = {
7
8
  color?: string;
8
9
  description?: string;
9
10
  allDay?: boolean;
11
+ /** Optional minimal recurrence. See CalendarRecurrence for supported semantics. */
12
+ recurrence?: CalendarRecurrence;
13
+ /** Resource row used by resourceTimeline view. */
14
+ resourceId?: string;
10
15
  };
11
16
  type $$ComponentProps = {
12
17
  events?: CalendarEvent[];
@@ -16,6 +21,9 @@ type $$ComponentProps = {
16
21
  timeZone?: string;
17
22
  firstDayOfWeek?: number;
18
23
  enableEventModal?: boolean;
24
+ resources?: CalendarResource[];
25
+ resourceTimelineStartHour?: number;
26
+ resourceTimelineEndHour?: number;
19
27
  class?: string;
20
28
  onEventClick?: (event: CalendarEvent) => void;
21
29
  onDateClick?: (date: Date) => void;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export { default as Calendar } from './Calendar.svelte';
2
- export { default as DatePicker } from './DatePicker.svelte';
3
- export { default as DateRangePicker } from './DateRangePicker.svelte';
4
- export { default as EventCalendar } from './EventCalendar.svelte';
5
- export type { CalendarDate } from './Calendar.svelte';
6
- export type { DateRange } from './DateRangePicker.svelte';
7
- export type { CalendarEvent, CalendarView } from './EventCalendar.svelte';
1
+ export { default as Calendar } from "./Calendar.svelte";
2
+ export { default as DatePicker } from "./DatePicker.svelte";
3
+ export { default as DateRangePicker } from "./DateRangePicker.svelte";
4
+ export { default as EventCalendar } from "./EventCalendar.svelte";
5
+ export type { CalendarDate } from "./Calendar.svelte";
6
+ export type { DateRange } from "./DateRangePicker.svelte";
7
+ export type { CalendarEvent, CalendarView } from "./EventCalendar.svelte";
8
+ export { expandRecurringEvent, expandRecurringEvents } from "./recurrence";
9
+ export type { CalendarRecurrence, CalendarRecurrenceFrequency, CalendarResource, RecurringCalendarEvent, } from "./recurrence";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { default as Calendar } from './Calendar.svelte';
2
- export { default as DatePicker } from './DatePicker.svelte';
3
- export { default as DateRangePicker } from './DateRangePicker.svelte';
4
- export { default as EventCalendar } from './EventCalendar.svelte';
1
+ export { default as Calendar } from "./Calendar.svelte";
2
+ export { default as DatePicker } from "./DatePicker.svelte";
3
+ export { default as DateRangePicker } from "./DateRangePicker.svelte";
4
+ export { default as EventCalendar } from "./EventCalendar.svelte";
5
+ export { expandRecurringEvent, expandRecurringEvents } from "./recurrence";
@@ -0,0 +1,31 @@
1
+ import type { CalendarEvent } from "./EventCalendar";
2
+ /** Supported recurrence frequencies. This is intentionally smaller than RFC 5545 RRULE. */
3
+ export type CalendarRecurrenceFrequency = "daily" | "weekly" | "monthly" | "yearly";
4
+ /**
5
+ * Minimal RRULE-like recurrence model.
6
+ *
7
+ * Only frequency, interval, until, and count are supported. BYDAY, BYMONTHDAY,
8
+ * EXDATE, timezone transition rules, and other RFC 5545 semantics are not
9
+ * implemented. `until` is inclusive and `count` counts generated occurrences.
10
+ */
11
+ export type CalendarRecurrence = {
12
+ frequency: CalendarRecurrenceFrequency;
13
+ interval?: number;
14
+ until?: Date;
15
+ count?: number;
16
+ };
17
+ /** Resource displayed as a row in `resourceTimeline` view. */
18
+ export type CalendarResource = {
19
+ id: string;
20
+ label: string;
21
+ description?: string;
22
+ color?: string;
23
+ };
24
+ export type RecurringCalendarEvent = CalendarEvent & {
25
+ recurrence?: CalendarRecurrence;
26
+ resourceId?: string;
27
+ };
28
+ /** Expand one event into occurrences intersecting inclusive range. */
29
+ export declare function expandRecurringEvent(event: RecurringCalendarEvent, rangeStart: Date, rangeEnd: Date): CalendarEvent[];
30
+ /** Expand recurring events for a calendar viewport. Non-recurring events pass through. */
31
+ export declare function expandRecurringEvents(events: RecurringCalendarEvent[], rangeStart: Date, rangeEnd: Date): CalendarEvent[];
@@ -0,0 +1,57 @@
1
+ function addOccurrence(date, recurrence) {
2
+ const result = new Date(date);
3
+ const interval = Math.max(1, Math.floor(recurrence.interval ?? 1));
4
+ if (recurrence.frequency === "daily")
5
+ result.setDate(result.getDate() + interval);
6
+ else if (recurrence.frequency === "weekly")
7
+ result.setDate(result.getDate() + interval * 7);
8
+ else if (recurrence.frequency === "monthly")
9
+ result.setMonth(result.getMonth() + interval);
10
+ else
11
+ result.setFullYear(result.getFullYear() + interval);
12
+ return result;
13
+ }
14
+ /** Expand one event into occurrences intersecting inclusive range. */
15
+ export function expandRecurringEvent(event, rangeStart, rangeEnd) {
16
+ if (!event.recurrence)
17
+ return [event];
18
+ const recurrence = event.recurrence;
19
+ const duration = new Date(event.end).getTime() - new Date(event.start).getTime();
20
+ const until = recurrence.until
21
+ ? new Date(recurrence.until).getTime()
22
+ : Infinity;
23
+ const maxCount = recurrence.count === undefined
24
+ ? Infinity
25
+ : Math.max(0, Math.floor(recurrence.count));
26
+ const occurrences = [];
27
+ let occurrenceStart = new Date(event.start);
28
+ let index = 0;
29
+ // Guard malformed input while keeping expansion deterministic for UI rendering.
30
+ while (index < maxCount && occurrenceStart.getTime() <= until) {
31
+ const occurrenceEnd = new Date(occurrenceStart.getTime() + duration);
32
+ if (occurrenceEnd.getTime() >= rangeStart.getTime() &&
33
+ occurrenceStart.getTime() <= rangeEnd.getTime()) {
34
+ occurrences.push({
35
+ ...event,
36
+ id: index === 0 ? event.id : `${event.id}::${occurrenceStart.getTime()}`,
37
+ start: occurrenceStart,
38
+ end: occurrenceEnd,
39
+ });
40
+ }
41
+ index += 1;
42
+ const nextStart = addOccurrence(occurrenceStart, recurrence);
43
+ if (nextStart.getTime() <= occurrenceStart.getTime())
44
+ break;
45
+ occurrenceStart = nextStart;
46
+ // No need to iterate unbounded future occurrences after range end.
47
+ if (occurrenceStart.getTime() > rangeEnd.getTime() &&
48
+ !recurrence.until &&
49
+ recurrence.count === undefined)
50
+ break;
51
+ }
52
+ return occurrences;
53
+ }
54
+ /** Expand recurring events for a calendar viewport. Non-recurring events pass through. */
55
+ export function expandRecurringEvents(events, rangeStart, rangeEnd) {
56
+ return events.flatMap((event) => expandRecurringEvent(event, rangeStart, rangeEnd));
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intinyagroup/calendar",
3
- "version": "0.1.1-alpha.22",
3
+ "version": "0.1.1-alpha.23",
4
4
  "description": "Calendar and DatePicker components for intinyagroup UI",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "test": "vitest run"
23
23
  },
24
24
  "dependencies": {
25
- "@intinyagroup/ui": "0.1.1-alpha.19",
25
+ "@intinyagroup/ui": "0.1.1-alpha.20",
26
26
  "date-fns": "^4.4.0",
27
27
  "lucide-svelte": "^1.0.1"
28
28
  },