@intinyagroup/calendar 0.1.1 → 0.2.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.
- package/dist/Calendar.svelte +1 -1
- package/dist/DatePicker.svelte +9 -8
- package/dist/DatePicker.svelte.d.ts +4 -1
- package/dist/DateRangePicker.svelte +16 -10
- package/dist/DateRangePicker.svelte.d.ts +2 -0
- package/dist/EventCalendar.svelte +18 -19
- package/dist/format-date.d.ts +6 -0
- package/dist/format-date.js +13 -0
- package/dist/format-date.test.d.ts +1 -0
- package/dist/format-date.test.js +13 -0
- package/dist/helpers.test.js +90 -66
- package/package.json +4 -3
package/dist/Calendar.svelte
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
class="grid grid-cols-7 gap-1 mb-2 text-center"
|
|
171
171
|
style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.25rem; margin-bottom: 0.5rem; text-align: center;"
|
|
172
172
|
>
|
|
173
|
-
{#each dayNames as d}
|
|
173
|
+
{#each dayNames as d (d)}
|
|
174
174
|
<div
|
|
175
175
|
class="text-center text-[11px] font-medium text-[var(--ui-muted-foreground)] py-1"
|
|
176
176
|
style="text-align: center; font-size: 0.6875rem; padding: 0.25rem 0;"
|
package/dist/DatePicker.svelte
CHANGED
|
@@ -14,31 +14,32 @@
|
|
|
14
14
|
import Calendar, { type CalendarDate } from "./Calendar.svelte";
|
|
15
15
|
import { Calendar as CalendarIcon, X } from "lucide-svelte";
|
|
16
16
|
import { cn } from "@intinyagroup/ui/utils";
|
|
17
|
+
import { formatCalendarDate } from "./format-date";
|
|
17
18
|
|
|
18
19
|
let {
|
|
19
20
|
value = $bindable(null),
|
|
20
21
|
placeholder = "Select date...",
|
|
22
|
+
locale = "en-GB",
|
|
23
|
+
format = "d MMM yyyy",
|
|
21
24
|
disabled = false,
|
|
22
25
|
class: className,
|
|
23
26
|
onSelect,
|
|
24
27
|
}: {
|
|
25
28
|
value?: CalendarDate | null;
|
|
26
29
|
placeholder?: string;
|
|
30
|
+
/** date-fns format pattern used for the selected date. */
|
|
31
|
+
format?: string;
|
|
32
|
+
locale?: string;
|
|
27
33
|
disabled?: boolean;
|
|
28
34
|
class?: string;
|
|
29
|
-
onSelect?: (date: CalendarDate) => void;
|
|
35
|
+
onSelect?: (date: CalendarDate | null) => void;
|
|
30
36
|
} = $props();
|
|
31
37
|
|
|
32
38
|
let open = $state(false);
|
|
33
39
|
|
|
34
40
|
function formatDateDisplay(date: CalendarDate | null): string {
|
|
35
41
|
if (!date) return "";
|
|
36
|
-
|
|
37
|
-
return d.toLocaleDateString("en-GB", {
|
|
38
|
-
day: "numeric",
|
|
39
|
-
month: "short",
|
|
40
|
-
year: "numeric",
|
|
41
|
-
});
|
|
42
|
+
return formatCalendarDate(date, format, locale);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function handleSelect(date: CalendarDate) {
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
|
|
50
51
|
function clear() {
|
|
51
52
|
value = null;
|
|
52
|
-
onSelect?.(null
|
|
53
|
+
onSelect?.(null);
|
|
53
54
|
}
|
|
54
55
|
</script>
|
|
55
56
|
|
|
@@ -2,9 +2,12 @@ import { type CalendarDate } from "./Calendar.svelte";
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
value?: CalendarDate | null;
|
|
4
4
|
placeholder?: string;
|
|
5
|
+
/** date-fns format pattern used for the selected date. */
|
|
6
|
+
format?: string;
|
|
7
|
+
locale?: string;
|
|
5
8
|
disabled?: boolean;
|
|
6
9
|
class?: string;
|
|
7
|
-
onSelect?: (date: CalendarDate) => void;
|
|
10
|
+
onSelect?: (date: CalendarDate | null) => void;
|
|
8
11
|
};
|
|
9
12
|
declare const DatePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
13
|
type DatePicker = ReturnType<typeof DatePicker>;
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
isSameMonth,
|
|
22
22
|
isWithinInterval,
|
|
23
23
|
isBefore,
|
|
24
|
-
format,
|
|
24
|
+
format as formatDateFns,
|
|
25
25
|
} from "date-fns";
|
|
26
26
|
import { id as localeId, enUS as localeEn } from "date-fns/locale";
|
|
27
|
+
import { formatCalendarDate } from "./format-date";
|
|
27
28
|
|
|
28
29
|
export type CalendarDate = { year: number; month: number; day: number };
|
|
29
30
|
export type DateRange = {
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
disabled = false,
|
|
44
45
|
placeholder = "Select date range...",
|
|
45
46
|
locale = "en-US",
|
|
47
|
+
format = "dd/MM/yyyy",
|
|
46
48
|
firstDayOfWeek = 0, // 0 = Sunday, 1 = Monday
|
|
47
49
|
presets,
|
|
48
50
|
class: className,
|
|
@@ -54,6 +56,8 @@
|
|
|
54
56
|
disabled?: boolean;
|
|
55
57
|
placeholder?: string;
|
|
56
58
|
locale?: string;
|
|
59
|
+
/** date-fns format pattern used for each endpoint. */
|
|
60
|
+
format?: string;
|
|
57
61
|
firstDayOfWeek?: number;
|
|
58
62
|
presets?: DateRangePreset[];
|
|
59
63
|
class?: string;
|
|
@@ -92,7 +96,9 @@
|
|
|
92
96
|
const dayNames = $derived.by(() => {
|
|
93
97
|
const start = startOfWeek(viewDate, { weekStartsOn });
|
|
94
98
|
return Array.from({ length: 7 }, (_, i) => {
|
|
95
|
-
return
|
|
99
|
+
return formatDateFns(addDays(start, i), "EEE", {
|
|
100
|
+
locale: activeDateFnsLocale,
|
|
101
|
+
});
|
|
96
102
|
});
|
|
97
103
|
});
|
|
98
104
|
|
|
@@ -145,9 +151,8 @@
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
function formatDisplayDate(date: CalendarDate | null): string {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return format(d, "dd/MM/yyyy", { locale: activeDateFnsLocale });
|
|
154
|
+
if (!date) return "";
|
|
155
|
+
return formatCalendarDate(date, format, locale);
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
const label = $derived.by(() => {
|
|
@@ -210,7 +215,6 @@
|
|
|
210
215
|
</script>
|
|
211
216
|
|
|
212
217
|
<div class={cn("relative inline-block w-full max-w-sm", className)}>
|
|
213
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
214
218
|
<div
|
|
215
219
|
role="button"
|
|
216
220
|
tabindex={disabled ? -1 : 0}
|
|
@@ -276,7 +280,9 @@
|
|
|
276
280
|
<span
|
|
277
281
|
class="text-xs font-semibold text-[var(--ui-foreground)] capitalize"
|
|
278
282
|
>
|
|
279
|
-
{
|
|
283
|
+
{formatDateFns(viewDate, "MMMM yyyy", {
|
|
284
|
+
locale: activeDateFnsLocale,
|
|
285
|
+
})}
|
|
280
286
|
</span>
|
|
281
287
|
<Button
|
|
282
288
|
variant="ghost"
|
|
@@ -293,7 +299,7 @@
|
|
|
293
299
|
class="grid grid-cols-7 mb-1 text-center"
|
|
294
300
|
style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); text-align: center; margin-bottom: 0.25rem;"
|
|
295
301
|
>
|
|
296
|
-
{#each dayNames as day}
|
|
302
|
+
{#each dayNames as day (day)}
|
|
297
303
|
<span
|
|
298
304
|
class="text-[11px] font-semibold text-[var(--ui-muted-foreground)] py-1 capitalize"
|
|
299
305
|
style="text-align: center; font-size: 0.6875rem; padding: 0.25rem 0;"
|
|
@@ -308,7 +314,7 @@
|
|
|
308
314
|
class="grid grid-cols-7 gap-y-1"
|
|
309
315
|
style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); row-gap: 0.25rem;"
|
|
310
316
|
>
|
|
311
|
-
{#each calendarDays as { date, calendarDate, currentMonth }}
|
|
317
|
+
{#each calendarDays as { date, calendarDate, currentMonth } (date.toISOString())}
|
|
312
318
|
{@const isStart = startDateObj
|
|
313
319
|
? isSameDay(startDateObj, date)
|
|
314
320
|
: false}
|
|
@@ -373,7 +379,7 @@
|
|
|
373
379
|
<div
|
|
374
380
|
class="flex flex-wrap items-center gap-1.5 border-t border-[var(--ui-border)] mt-3 pt-2.5"
|
|
375
381
|
>
|
|
376
|
-
{#each activePresets as preset}
|
|
382
|
+
{#each activePresets as preset (preset.label)}
|
|
377
383
|
<button
|
|
378
384
|
type="button"
|
|
379
385
|
onclick={() => {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
type CalendarRecurrence,
|
|
33
33
|
type CalendarResource,
|
|
34
34
|
} from "./recurrence";
|
|
35
|
+
import { SvelteDate, SvelteMap } from "svelte/reactivity";
|
|
35
36
|
|
|
36
37
|
export type CalendarView = "month" | "week" | "day" | "resourceTimeline";
|
|
37
38
|
|
|
@@ -153,7 +154,7 @@
|
|
|
153
154
|
|
|
154
155
|
const origDurationMs =
|
|
155
156
|
new Date(ev.end).getTime() - new Date(ev.start).getTime();
|
|
156
|
-
const newStart = new
|
|
157
|
+
const newStart = new SvelteDate(targetDate);
|
|
157
158
|
if (targetHour !== undefined) {
|
|
158
159
|
newStart.setHours(targetHour, 0, 0, 0);
|
|
159
160
|
} else {
|
|
@@ -283,14 +284,14 @@
|
|
|
283
284
|
const endMinutes = dragCreateEnd.hour * 60 + dragCreateEnd.minute;
|
|
284
285
|
|
|
285
286
|
if (endMinutes - startMinutes >= 15) {
|
|
286
|
-
const startDate = new
|
|
287
|
+
const startDate = new SvelteDate(date);
|
|
287
288
|
startDate.setHours(
|
|
288
289
|
dragCreateStart.hour,
|
|
289
290
|
dragCreateStart.minute,
|
|
290
291
|
0,
|
|
291
292
|
0,
|
|
292
293
|
);
|
|
293
|
-
const endDate = new
|
|
294
|
+
const endDate = new SvelteDate(date);
|
|
294
295
|
endDate.setHours(dragCreateEnd.hour, dragCreateEnd.minute, 0, 0);
|
|
295
296
|
|
|
296
297
|
onDragCreate?.({ start: startDate, end: endDate });
|
|
@@ -406,7 +407,7 @@
|
|
|
406
407
|
});
|
|
407
408
|
|
|
408
409
|
const columns: CalendarEvent[][] = [];
|
|
409
|
-
const layoutMap = new
|
|
410
|
+
const layoutMap = new SvelteMap<string, { colIndex: number }>();
|
|
410
411
|
|
|
411
412
|
for (const ev of sorted) {
|
|
412
413
|
let placed = false;
|
|
@@ -610,10 +611,10 @@
|
|
|
610
611
|
const [startH, startM] = newEventStartTime.split(":").map(Number);
|
|
611
612
|
const [endH, endM] = newEventEndTime.split(":").map(Number);
|
|
612
613
|
|
|
613
|
-
const startDate = new
|
|
614
|
+
const startDate = new SvelteDate(newEventDate);
|
|
614
615
|
startDate.setHours(startH || 0, startM || 0, 0, 0);
|
|
615
616
|
|
|
616
|
-
const endDate = new
|
|
617
|
+
const endDate = new SvelteDate(newEventDate);
|
|
617
618
|
endDate.setHours(endH || 0, endM || 0, 0, 0);
|
|
618
619
|
|
|
619
620
|
const newEv: CalendarEvent = {
|
|
@@ -747,7 +748,7 @@
|
|
|
747
748
|
class="grid grid-cols-7 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/20 text-center text-xs font-semibold text-[var(--ui-muted-foreground)] py-2"
|
|
748
749
|
style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); text-align: center;"
|
|
749
750
|
>
|
|
750
|
-
{#each dayNames as day}
|
|
751
|
+
{#each dayNames as day (day)}
|
|
751
752
|
<div class="capitalize">{day}</div>
|
|
752
753
|
{/each}
|
|
753
754
|
</div>
|
|
@@ -756,7 +757,7 @@
|
|
|
756
757
|
class="grid grid-cols-7 divide-x divide-y divide-[var(--ui-border)]"
|
|
757
758
|
style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));"
|
|
758
759
|
>
|
|
759
|
-
{#each monthDays as { date, currentMonth }}
|
|
760
|
+
{#each monthDays as { date, currentMonth } (date.toISOString())}
|
|
760
761
|
{@const dayEvents = getEventsForDay(date)}
|
|
761
762
|
{@const todayActive = isToday(date)}
|
|
762
763
|
|
|
@@ -856,7 +857,7 @@
|
|
|
856
857
|
class="grid grid-cols-4 md:grid-cols-8 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/20 text-center text-xs font-semibold text-[var(--ui-muted-foreground)] py-2"
|
|
857
858
|
>
|
|
858
859
|
<div class="col-span-1">Time</div>
|
|
859
|
-
{#each isMobile ? mobileWeekDays : weekDays as d}
|
|
860
|
+
{#each isMobile ? mobileWeekDays : weekDays as d (d.toISOString())}
|
|
860
861
|
<div class="col-span-1 flex flex-col items-center">
|
|
861
862
|
<span class="capitalize"
|
|
862
863
|
>{format(d, "EEE", { locale: activeDateFnsLocale })}</span
|
|
@@ -914,13 +915,13 @@
|
|
|
914
915
|
<div
|
|
915
916
|
class="col-span-1 divide-y divide-[var(--ui-border)]/50 text-right pr-2 text-[11px] font-medium text-[var(--ui-muted-foreground)]"
|
|
916
917
|
>
|
|
917
|
-
{#each hours as hour}
|
|
918
|
+
{#each hours as hour (hour)}
|
|
918
919
|
<div class="h-14 pt-1">{String(hour).padStart(2, "0")}:00</div>
|
|
919
920
|
{/each}
|
|
920
921
|
</div>
|
|
921
922
|
|
|
922
923
|
<!-- Day Grid Columns -->
|
|
923
|
-
{#each isMobile ? mobileWeekDays : weekDays as d}
|
|
924
|
+
{#each isMobile ? mobileWeekDays : weekDays as d (d.toISOString())}
|
|
924
925
|
{@const dayEvents = getTimedEventsForDay(d)}
|
|
925
926
|
{@const layoutEvents = computeOverlappingLayout(dayEvents)}
|
|
926
927
|
{@const isCurrentDay = isToday(d)}
|
|
@@ -935,7 +936,7 @@
|
|
|
935
936
|
onclick={() => openCreateModal(d)}
|
|
936
937
|
class="col-span-1 divide-y divide-[var(--ui-border)]/50 relative hover:bg-[var(--ui-secondary)]/10 cursor-pointer select-none"
|
|
937
938
|
>
|
|
938
|
-
{#each hours as hour}
|
|
939
|
+
{#each hours as hour (hour)}
|
|
939
940
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
940
941
|
<div
|
|
941
942
|
class="h-14 transition-colors hover:bg-[var(--ui-primary)]/10 pointer-events-auto"
|
|
@@ -981,7 +982,6 @@
|
|
|
981
982
|
{@const widthPercent = 100 / ev.colTotal}
|
|
982
983
|
{@const leftPercent = ev.colIndex * widthPercent}
|
|
983
984
|
|
|
984
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
985
985
|
<div
|
|
986
986
|
role="button"
|
|
987
987
|
tabindex="0"
|
|
@@ -1076,7 +1076,7 @@
|
|
|
1076
1076
|
<div
|
|
1077
1077
|
class="col-span-2 divide-y divide-[var(--ui-border)]/50 text-right pr-3 text-xs font-medium text-[var(--ui-muted-foreground)]"
|
|
1078
1078
|
>
|
|
1079
|
-
{#each hours as hour}
|
|
1079
|
+
{#each hours as hour (hour)}
|
|
1080
1080
|
<div class="h-16 pt-2">{String(hour).padStart(2, "0")}:00</div>
|
|
1081
1081
|
{/each}
|
|
1082
1082
|
</div>
|
|
@@ -1088,7 +1088,7 @@
|
|
|
1088
1088
|
onclick={() => openCreateModal(currentDate)}
|
|
1089
1089
|
class="col-span-10 divide-y divide-[var(--ui-border)]/50 relative p-1 hover:bg-[var(--ui-secondary)]/10 cursor-pointer select-none"
|
|
1090
1090
|
>
|
|
1091
|
-
{#each hours as hour}
|
|
1091
|
+
{#each hours as hour (hour)}
|
|
1092
1092
|
<div
|
|
1093
1093
|
class="h-16 pointer-events-auto"
|
|
1094
1094
|
onmousedown={(e) =>
|
|
@@ -1129,7 +1129,6 @@
|
|
|
1129
1129
|
{@const widthPercent = 100 / ev.colTotal}
|
|
1130
1130
|
{@const leftPercent = ev.colIndex * widthPercent}
|
|
1131
1131
|
|
|
1132
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
1133
1132
|
<div
|
|
1134
1133
|
role="button"
|
|
1135
1134
|
tabindex="0"
|
|
@@ -1222,7 +1221,7 @@
|
|
|
1222
1221
|
class="relative grid"
|
|
1223
1222
|
style="grid-template-columns: repeat({timelineHours.length}, minmax(0, 1fr));"
|
|
1224
1223
|
>
|
|
1225
|
-
{#each timelineHours as hour}
|
|
1224
|
+
{#each timelineHours as hour (hour)}
|
|
1226
1225
|
<div
|
|
1227
1226
|
class="border-r border-[var(--ui-border)] px-1 py-2 text-center"
|
|
1228
1227
|
>
|
|
@@ -1262,7 +1261,7 @@
|
|
|
1262
1261
|
class="pointer-events-none absolute inset-0 grid"
|
|
1263
1262
|
style="grid-template-columns: repeat({timelineHours.length}, minmax(0, 1fr));"
|
|
1264
1263
|
>
|
|
1265
|
-
{#each timelineHours as _hour}<div
|
|
1264
|
+
{#each timelineHours as _hour (_hour)}<div
|
|
1266
1265
|
class="border-r border-[var(--ui-border)]/50"
|
|
1267
1266
|
></div>{/each}
|
|
1268
1267
|
</div>
|
|
@@ -1372,7 +1371,7 @@
|
|
|
1372
1371
|
>Tag Color</span
|
|
1373
1372
|
>
|
|
1374
1373
|
<div class="flex items-center gap-2">
|
|
1375
|
-
{#each presetColors as col}
|
|
1374
|
+
{#each presetColors as col (col)}
|
|
1376
1375
|
<button
|
|
1377
1376
|
type="button"
|
|
1378
1377
|
onclick={() => (newEventColor = col)}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { format } from "date-fns";
|
|
2
|
+
import { enGB, enUS, id } from "date-fns/locale";
|
|
3
|
+
function resolveLocale(locale) {
|
|
4
|
+
if (locale.toLowerCase().startsWith("id"))
|
|
5
|
+
return id;
|
|
6
|
+
if (locale.toLowerCase() === "en-gb")
|
|
7
|
+
return enGB;
|
|
8
|
+
return enUS;
|
|
9
|
+
}
|
|
10
|
+
export function formatCalendarDate(value, pattern, locale) {
|
|
11
|
+
const date = new Date(value.year, value.month, value.day);
|
|
12
|
+
return format(date, pattern, { locale: resolveLocale(locale) });
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatCalendarDate } from "./format-date";
|
|
3
|
+
const sampleDate = { year: 2026, month: 10, day: 7 };
|
|
4
|
+
describe("formatCalendarDate", () => {
|
|
5
|
+
it("supports day-first and month-first numeric formats", () => {
|
|
6
|
+
expect(formatCalendarDate(sampleDate, "dd/MM/yyyy", "id-ID")).toBe("07/11/2026");
|
|
7
|
+
expect(formatCalendarDate(sampleDate, "MM/dd/yyyy", "en-US")).toBe("11/07/2026");
|
|
8
|
+
});
|
|
9
|
+
it("formats month names using the requested locale", () => {
|
|
10
|
+
expect(formatCalendarDate(sampleDate, "d MMMM yyyy", "id-ID")).toBe("7 November 2026");
|
|
11
|
+
expect(formatCalendarDate(sampleDate, "d MMM yyyy", "en-GB")).toBe("7 Nov 2026");
|
|
12
|
+
});
|
|
13
|
+
});
|
package/dist/helpers.test.js
CHANGED
|
@@ -1,129 +1,153 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from
|
|
2
|
-
import { render, screen, fireEvent } from
|
|
3
|
-
import EventCalendar from
|
|
4
|
-
describe(
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/svelte";
|
|
3
|
+
import EventCalendar from "./EventCalendar.svelte";
|
|
4
|
+
describe("EventCalendar", () => {
|
|
5
5
|
beforeEach(() => {
|
|
6
6
|
vi.useFakeTimers();
|
|
7
|
-
vi.setSystemTime(new Date(
|
|
7
|
+
vi.setSystemTime(new Date("2025-06-15T12:00:00"));
|
|
8
8
|
});
|
|
9
9
|
afterEach(() => {
|
|
10
10
|
vi.useRealTimers();
|
|
11
11
|
});
|
|
12
|
-
it(
|
|
12
|
+
it("renders the calendar component with correct month title", () => {
|
|
13
13
|
render(EventCalendar);
|
|
14
14
|
// Title uses full month name: "MMMM yyyy"
|
|
15
|
-
expect(screen.getByText(
|
|
15
|
+
expect(screen.getByText("June 2025")).toBeInTheDocument();
|
|
16
16
|
});
|
|
17
|
-
it(
|
|
17
|
+
it("displays day-of-week headers", () => {
|
|
18
18
|
render(EventCalendar);
|
|
19
|
-
expect(screen.getByText(
|
|
20
|
-
expect(screen.getByText(
|
|
21
|
-
expect(screen.getByText(
|
|
22
|
-
expect(screen.getByText(
|
|
23
|
-
expect(screen.getByText(
|
|
24
|
-
expect(screen.getByText(
|
|
25
|
-
expect(screen.getByText(
|
|
19
|
+
expect(screen.getByText("Sun")).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByText("Mon")).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText("Tue")).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText("Wed")).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText("Thu")).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText("Fri")).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText("Sat")).toBeInTheDocument();
|
|
26
26
|
});
|
|
27
|
-
it(
|
|
27
|
+
it("renders month view with day grid", () => {
|
|
28
28
|
render(EventCalendar);
|
|
29
29
|
// Should show days of June 2025. Use getAllByText for numbers that appear
|
|
30
30
|
// multiple times in the month grid (e.g. from previous/next month overflow).
|
|
31
|
-
expect(screen.getAllByText(
|
|
32
|
-
expect(screen.getAllByText(
|
|
33
|
-
expect(screen.getAllByText(
|
|
31
|
+
expect(screen.getAllByText("15").length).toBeGreaterThanOrEqual(1);
|
|
32
|
+
expect(screen.getAllByText("30").length).toBeGreaterThanOrEqual(1);
|
|
33
|
+
expect(screen.getAllByText("1").length).toBeGreaterThanOrEqual(1);
|
|
34
34
|
});
|
|
35
|
-
it(
|
|
35
|
+
it("renders events in the calendar", () => {
|
|
36
36
|
const events = [
|
|
37
37
|
{
|
|
38
|
-
id:
|
|
39
|
-
title:
|
|
40
|
-
start: new Date(
|
|
41
|
-
end: new Date(
|
|
38
|
+
id: "1",
|
|
39
|
+
title: "Team Meeting",
|
|
40
|
+
start: new Date("2025-06-15T09:00:00"),
|
|
41
|
+
end: new Date("2025-06-15T10:00:00"),
|
|
42
42
|
},
|
|
43
43
|
];
|
|
44
44
|
render(EventCalendar, { props: { events } });
|
|
45
|
-
expect(screen.getByText(
|
|
45
|
+
expect(screen.getByText("Team Meeting")).toBeInTheDocument();
|
|
46
46
|
});
|
|
47
|
-
it(
|
|
47
|
+
it("renders multiple events on the same day", () => {
|
|
48
48
|
const events = [
|
|
49
49
|
{
|
|
50
|
-
id:
|
|
51
|
-
title:
|
|
52
|
-
start: new Date(
|
|
53
|
-
end: new Date(
|
|
50
|
+
id: "1",
|
|
51
|
+
title: "Morning Standup",
|
|
52
|
+
start: new Date("2025-06-15T09:00:00"),
|
|
53
|
+
end: new Date("2025-06-15T09:30:00"),
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
id:
|
|
57
|
-
title:
|
|
58
|
-
start: new Date(
|
|
59
|
-
end: new Date(
|
|
56
|
+
id: "2",
|
|
57
|
+
title: "Lunch Break",
|
|
58
|
+
start: new Date("2025-06-15T12:00:00"),
|
|
59
|
+
end: new Date("2025-06-15T13:00:00"),
|
|
60
60
|
},
|
|
61
61
|
];
|
|
62
62
|
render(EventCalendar, { props: { events } });
|
|
63
|
-
expect(screen.getByText(
|
|
64
|
-
expect(screen.getByText(
|
|
63
|
+
expect(screen.getByText("Morning Standup")).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText("Lunch Break")).toBeInTheDocument();
|
|
65
65
|
});
|
|
66
|
-
it(
|
|
66
|
+
it("navigates to the previous month", async () => {
|
|
67
67
|
render(EventCalendar);
|
|
68
68
|
// Prev button is a Button with ChevronLeft icon; find by role
|
|
69
|
-
const prevBtn = screen.getAllByRole(
|
|
69
|
+
const prevBtn = screen.getAllByRole("button")[0];
|
|
70
70
|
await fireEvent.click(prevBtn);
|
|
71
|
-
expect(screen.getByText(
|
|
71
|
+
expect(screen.getByText("May 2025")).toBeInTheDocument();
|
|
72
72
|
});
|
|
73
|
-
it(
|
|
73
|
+
it("navigates to the next month", async () => {
|
|
74
74
|
render(EventCalendar);
|
|
75
75
|
// Next button is the third button (prev=0, today=1, next=2)
|
|
76
|
-
const nextBtn = screen.getAllByRole(
|
|
76
|
+
const nextBtn = screen.getAllByRole("button")[2];
|
|
77
77
|
await fireEvent.click(nextBtn);
|
|
78
|
-
expect(screen.getByText(
|
|
78
|
+
expect(screen.getByText("July 2025")).toBeInTheDocument();
|
|
79
79
|
});
|
|
80
|
-
it(
|
|
80
|
+
it("navigates back to today", async () => {
|
|
81
81
|
render(EventCalendar);
|
|
82
82
|
// Navigate to next month first
|
|
83
|
-
const nextBtn = screen.getAllByRole(
|
|
83
|
+
const nextBtn = screen.getAllByRole("button")[2];
|
|
84
84
|
await fireEvent.click(nextBtn);
|
|
85
|
-
expect(screen.getByText(
|
|
85
|
+
expect(screen.getByText("July 2025")).toBeInTheDocument();
|
|
86
86
|
// Click Today button (text content is "Today")
|
|
87
|
-
const todayBtn = screen.getByText(
|
|
87
|
+
const todayBtn = screen.getByText("Today");
|
|
88
88
|
await fireEvent.click(todayBtn);
|
|
89
|
-
expect(screen.getByText(
|
|
89
|
+
expect(screen.getByText("June 2025")).toBeInTheDocument();
|
|
90
90
|
});
|
|
91
|
-
it(
|
|
91
|
+
it("displays view switcher buttons", () => {
|
|
92
92
|
render(EventCalendar);
|
|
93
|
-
expect(screen.getByText(
|
|
94
|
-
expect(screen.getByText(
|
|
95
|
-
expect(screen.getByText(
|
|
93
|
+
expect(screen.getByText("Month")).toBeInTheDocument();
|
|
94
|
+
expect(screen.getByText("Week")).toBeInTheDocument();
|
|
95
|
+
expect(screen.getByText("Day")).toBeInTheDocument();
|
|
96
96
|
});
|
|
97
|
-
it(
|
|
97
|
+
it("shows event with color indicator", () => {
|
|
98
98
|
const events = [
|
|
99
99
|
{
|
|
100
|
-
id:
|
|
101
|
-
title:
|
|
102
|
-
start: new Date(
|
|
103
|
-
end: new Date(
|
|
104
|
-
color:
|
|
100
|
+
id: "1",
|
|
101
|
+
title: "Important Call",
|
|
102
|
+
start: new Date("2025-06-15T14:00:00"),
|
|
103
|
+
end: new Date("2025-06-15T15:00:00"),
|
|
104
|
+
color: "#dc2626",
|
|
105
105
|
},
|
|
106
106
|
];
|
|
107
107
|
render(EventCalendar, { props: { events } });
|
|
108
|
-
expect(screen.getByText(
|
|
108
|
+
expect(screen.getByText("Important Call")).toBeInTheDocument();
|
|
109
109
|
});
|
|
110
|
-
it(
|
|
110
|
+
it("renders empty state without crashing", () => {
|
|
111
111
|
render(EventCalendar, { props: { events: [] } });
|
|
112
|
-
expect(screen.getByText(
|
|
112
|
+
expect(screen.getByText("June 2025")).toBeInTheDocument();
|
|
113
113
|
});
|
|
114
|
-
it(
|
|
114
|
+
it("renders all-day events (may appear in both grid and all-day header)", () => {
|
|
115
115
|
const events = [
|
|
116
116
|
{
|
|
117
|
-
id:
|
|
118
|
-
title:
|
|
119
|
-
start: new Date(
|
|
120
|
-
end: new Date(
|
|
117
|
+
id: "1",
|
|
118
|
+
title: "Company Holiday",
|
|
119
|
+
start: new Date("2025-06-15T00:00:00"),
|
|
120
|
+
end: new Date("2025-06-16T00:00:00"),
|
|
121
121
|
allDay: true,
|
|
122
122
|
},
|
|
123
123
|
];
|
|
124
124
|
render(EventCalendar, { props: { events } });
|
|
125
125
|
// All-day events appear in both the month grid and the all-day section
|
|
126
|
-
const matches = screen.getAllByText(
|
|
126
|
+
const matches = screen.getAllByText("Company Holiday");
|
|
127
127
|
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
128
128
|
});
|
|
129
|
+
it("renders resource timeline rows and assigned events", () => {
|
|
130
|
+
const events = [
|
|
131
|
+
{
|
|
132
|
+
id: "resource-event",
|
|
133
|
+
title: "Room Booking",
|
|
134
|
+
start: new Date("2025-06-15T09:00:00"),
|
|
135
|
+
end: new Date("2025-06-15T10:00:00"),
|
|
136
|
+
resourceId: "room-a",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
render(EventCalendar, {
|
|
140
|
+
props: {
|
|
141
|
+
view: "resourceTimeline",
|
|
142
|
+
events,
|
|
143
|
+
resources: [{ id: "room-a", label: "Room A" }],
|
|
144
|
+
resourceTimelineStartHour: 8,
|
|
145
|
+
resourceTimelineEndHour: 18,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
expect(screen.getByText("Room A")).toBeInTheDocument();
|
|
149
|
+
expect(screen.getByText("Room Booking")).toBeInTheDocument();
|
|
150
|
+
expect(screen.getByText("08:00")).toBeInTheDocument();
|
|
151
|
+
expect(screen.getByText("17:00")).toBeInTheDocument();
|
|
152
|
+
});
|
|
129
153
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intinyagroup/calendar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.2.
|
|
25
|
+
"@intinyagroup/ui": "0.2.10",
|
|
26
26
|
"date-fns": "^4.4.0",
|
|
27
27
|
"lucide-svelte": "^1.0.1"
|
|
28
28
|
},
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"@sveltejs/package": "^2.5.8",
|
|
31
31
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
|
32
32
|
"svelte": "^5.55.2",
|
|
33
|
-
"typescript": "^6.0.2"
|
|
33
|
+
"typescript": "^6.0.2",
|
|
34
|
+
"@intinyagroup/vitest-preset": "0.1.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"svelte": "^5.0.0"
|