@intinyagroup/calendar 0.1.1-alpha.22 → 0.1.1-alpha.24
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/dist/DatePicker.svelte +27 -13
- package/dist/DatePicker.svelte.d.ts +1 -1
- package/dist/DateRangePicker.svelte +1 -0
- package/dist/EventCalendar.svelte +204 -7
- package/dist/EventCalendar.svelte.d.ts +9 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +5 -4
- package/dist/recurrence.d.ts +31 -0
- package/dist/recurrence.js +57 -0
- package/package.json +2 -2
package/dist/DatePicker.svelte
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
disabled &&
|
|
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 &&
|
|
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)]" />
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2
|
-
export { default as DatePicker } from
|
|
3
|
-
export { default as DateRangePicker } from
|
|
4
|
-
export { default as EventCalendar } from
|
|
5
|
-
export type { CalendarDate } from
|
|
6
|
-
export type { DateRange } from
|
|
7
|
-
export type { CalendarEvent, CalendarView } from
|
|
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
|
|
2
|
-
export { default as DatePicker } from
|
|
3
|
-
export { default as DateRangePicker } from
|
|
4
|
-
export { default as EventCalendar } from
|
|
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.
|
|
3
|
+
"version": "0.1.1-alpha.24",
|
|
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.
|
|
25
|
+
"@intinyagroup/ui": "0.1.1-alpha.21",
|
|
26
26
|
"date-fns": "^4.4.0",
|
|
27
27
|
"lucide-svelte": "^1.0.1"
|
|
28
28
|
},
|