@valentinkolb/cloud 0.4.0 → 0.5.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.
Files changed (194) hide show
  1. package/package.json +18 -6
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +53 -46
  4. package/src/api/accounts-entities.ts +4 -0
  5. package/src/api/admin-core-settings.ts +98 -0
  6. package/src/api/announcements.ts +131 -0
  7. package/src/api/auth/schemas.ts +24 -0
  8. package/src/api/auth.ts +116 -13
  9. package/src/api/index.ts +7 -2
  10. package/src/api/me.ts +203 -14
  11. package/src/api/search/schemas.ts +1 -0
  12. package/src/api/search.ts +62 -8
  13. package/src/config/ssr.ts +2 -9
  14. package/src/contracts/announcements.test.ts +37 -0
  15. package/src/contracts/announcements.ts +121 -0
  16. package/src/contracts/app.ts +2 -0
  17. package/src/contracts/index.ts +3 -2
  18. package/src/contracts/registry.ts +2 -0
  19. package/src/contracts/shared.ts +108 -1
  20. package/src/desktop/index.ts +704 -0
  21. package/src/desktop/solid.tsx +938 -0
  22. package/src/server/api/index.ts +1 -1
  23. package/src/server/api/respond.ts +50 -10
  24. package/src/server/index.ts +44 -38
  25. package/src/server/middleware/auth.ts +98 -9
  26. package/src/server/middleware/index.ts +2 -1
  27. package/src/server/middleware/settings.ts +26 -0
  28. package/src/server/services/access.test.ts +197 -0
  29. package/src/server/services/access.ts +254 -6
  30. package/src/server/services/index.ts +14 -11
  31. package/src/server/services/pagination.ts +22 -0
  32. package/src/server/time.ts +45 -0
  33. package/src/services/account-lifecycle/index.ts +142 -18
  34. package/src/services/accounts/app.ts +658 -170
  35. package/src/services/accounts/authz.test.ts +77 -0
  36. package/src/services/accounts/authz.ts +22 -0
  37. package/src/services/accounts/entities.ts +84 -5
  38. package/src/services/accounts/groups.ts +30 -24
  39. package/src/services/accounts/model.test.ts +30 -0
  40. package/src/services/accounts/switching.test.ts +14 -0
  41. package/src/services/accounts/switching.ts +15 -6
  42. package/src/services/accounts/users.ts +75 -52
  43. package/src/services/announcements/index.test.ts +32 -0
  44. package/src/services/announcements/index.ts +224 -0
  45. package/src/services/audit/index.test.ts +84 -0
  46. package/src/services/audit/index.ts +431 -0
  47. package/src/services/auth-flows/index.ts +9 -2
  48. package/src/services/auth-flows/ipa.ts +47 -7
  49. package/src/services/auth-flows/magic-link.ts +92 -20
  50. package/src/services/auth-flows/password-reset.ts +284 -0
  51. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  52. package/src/services/auth-flows/proxy-return.ts +49 -0
  53. package/src/services/gateway.ts +162 -0
  54. package/src/services/index.ts +44 -2
  55. package/src/services/ipa/effective-groups.test.ts +33 -0
  56. package/src/services/ipa/effective-groups.ts +70 -0
  57. package/src/services/ipa/profile.ts +45 -3
  58. package/src/services/ipa/search.ts +3 -5
  59. package/src/services/ipa/service-account.ts +15 -0
  60. package/src/services/ipa/sync-planning.test.ts +32 -0
  61. package/src/services/ipa/sync-planning.ts +22 -0
  62. package/src/services/ipa/sync.ts +110 -38
  63. package/src/services/notifications/index.ts +82 -11
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +79 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +58 -0
  92. package/src/shared/redirect.ts +56 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,1291 @@
1
+ import { dates as calendar, type DateContext } from "@valentinkolb/stdlib";
2
+ import type { Accessor, JSX } from "solid-js";
3
+ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
4
+ import SegmentedControl from "../input/SegmentedControl";
5
+
6
+ export type CalendarView = "day" | "week" | "month" | "year" | "mobile-month";
7
+
8
+ export type CalendarEventColor = "blue" | "emerald" | "amber" | "red" | "violet" | "cyan" | "zinc";
9
+
10
+ export type CalendarEvent = {
11
+ id: string;
12
+ title: string;
13
+ start: Date | string;
14
+ end?: Date | string;
15
+ allDay?: boolean;
16
+ color?: CalendarEventColor;
17
+ colorHex?: string;
18
+ href?: string;
19
+ dataSpaceItemId?: string;
20
+ meta?: string;
21
+ description?: string;
22
+ display?: "event" | "background";
23
+ location?: string;
24
+ calendarName?: string;
25
+ attendees?: CalendarAttendee[];
26
+ resources?: CalendarResource[];
27
+ recurrence?: CalendarRecurrence;
28
+ };
29
+
30
+ export type CalendarAttendee = {
31
+ name: string;
32
+ status?: "accepted" | "declined" | "tentative" | "needs-action";
33
+ };
34
+
35
+ export type CalendarResource = {
36
+ name: string;
37
+ kind?: "room" | "equipment" | "link" | "other";
38
+ };
39
+
40
+ export type CalendarRecurrence = {
41
+ rrule: string;
42
+ exdate?: Array<Date | string>;
43
+ recurrenceId?: Date | string;
44
+ };
45
+
46
+ export type CalendarLabels = Partial<{
47
+ today: string;
48
+ day: string;
49
+ week: string;
50
+ month: string;
51
+ year: string;
52
+ allDay: string;
53
+ noEvents: string;
54
+ previous: string;
55
+ next: string;
56
+ }>;
57
+
58
+ export type CalendarEventRenderContext = {
59
+ compact: boolean;
60
+ fill: boolean;
61
+ start: Date;
62
+ end: Date;
63
+ allDay: boolean;
64
+ durationHours: number;
65
+ timeLabel: string;
66
+ };
67
+
68
+ export type CalendarProps = {
69
+ date: Date | string;
70
+ events: CalendarEvent[];
71
+ view?: CalendarView;
72
+ views?: CalendarView[];
73
+ labels?: CalendarLabels;
74
+ /** stdlib date context used for timezone-aware rendering and calendar math. */
75
+ dateConfig?: DateContext;
76
+ /** Convenience override for dateConfig.timeZone. */
77
+ timeZone?: string;
78
+ firstDayOfWeek?: 0 | 1;
79
+ withWeekNumbers?: boolean;
80
+ startHour?: number;
81
+ endHour?: number;
82
+ visibleStartHour?: number;
83
+ visibleEndHour?: number;
84
+ allDayMaxHeightRem?: number;
85
+ hideAllDay?: boolean;
86
+ selectedDate?: Date | string;
87
+ selectedEventId?: string;
88
+ dayBadges?: Record<string, CalendarDayBadge>;
89
+ getViewHref?: (view: CalendarView) => string;
90
+ getDateHref?: (date: Date, view: CalendarView) => string;
91
+ getEventHref?: (event: CalendarEvent) => string | undefined;
92
+ renderEvent?: (event: CalendarEvent, context: CalendarEventRenderContext) => JSX.Element;
93
+ onViewChange?: (view: CalendarView) => void;
94
+ onDateChange?: (date: Date, view: CalendarView) => void;
95
+ onEventClick?: (event: CalendarEvent) => void;
96
+ onEventDrop?: (event: CalendarEvent, next: CalendarEventTimeChange) => void;
97
+ onEventResize?: (event: CalendarEvent, next: CalendarEventTimeChange) => void;
98
+ onEventDoubleClick?: (event: CalendarEvent) => void;
99
+ onSlotClick?: (slot: CalendarEventTimeChange) => void;
100
+ onSlotDoubleClick?: (slot: CalendarEventTimeChange) => void;
101
+ class?: string;
102
+ };
103
+
104
+ export type CalendarEventTimeChange = {
105
+ start: Date;
106
+ end: Date;
107
+ allDay?: boolean;
108
+ };
109
+
110
+ export type CalendarDayBadge = {
111
+ icon?: string;
112
+ label: string;
113
+ };
114
+
115
+ type NormalizedEvent = CalendarEvent & {
116
+ startDate: Date;
117
+ endDate: Date;
118
+ dayKey: string;
119
+ sourceStartDate: Date;
120
+ sourceEndDate: Date;
121
+ };
122
+
123
+ type CalendarPreview = CalendarEventTimeChange & {
124
+ id: string;
125
+ };
126
+
127
+ type TimedEventLayout = {
128
+ event: NormalizedEvent;
129
+ lane: number;
130
+ lanes: number;
131
+ groupId: number;
132
+ groupStartDate: Date;
133
+ groupEndDate: Date;
134
+ };
135
+
136
+ type TimedOverflowLayout = {
137
+ groupId: number;
138
+ hiddenEvents: NormalizedEvent[];
139
+ groupStartDate: Date;
140
+ groupEndDate: Date;
141
+ };
142
+
143
+ const labels: Required<CalendarLabels> = {
144
+ today: "Today",
145
+ day: "Day",
146
+ week: "Week",
147
+ month: "Month",
148
+ year: "Year",
149
+ allDay: "All day",
150
+ noEvents: "No events",
151
+ previous: "Previous",
152
+ next: "Next",
153
+ };
154
+
155
+ const colorClass: Record<CalendarEventColor, string> = {
156
+ blue: "border-l-blue-500 text-blue-700 dark:border-l-blue-400 dark:text-blue-200",
157
+ emerald: "border-l-emerald-500 text-emerald-700 dark:border-l-emerald-400 dark:text-emerald-200",
158
+ amber: "border-l-amber-500 text-amber-700 dark:border-l-amber-400 dark:text-amber-200",
159
+ red: "border-l-red-500 text-red-700 dark:border-l-red-400 dark:text-red-200",
160
+ violet: "border-l-violet-500 text-violet-700 dark:border-l-violet-400 dark:text-violet-200",
161
+ cyan: "border-l-cyan-500 text-cyan-700 dark:border-l-cyan-400 dark:text-cyan-200",
162
+ zinc: "border-l-zinc-400 text-zinc-700 dark:border-l-zinc-500 dark:text-zinc-200",
163
+ };
164
+
165
+ const dotClass: Record<CalendarEventColor, string> = {
166
+ blue: "bg-blue-500",
167
+ emerald: "bg-emerald-500",
168
+ amber: "bg-amber-500",
169
+ red: "bg-red-500",
170
+ violet: "bg-violet-500",
171
+ cyan: "bg-cyan-500",
172
+ zinc: "bg-zinc-400",
173
+ };
174
+
175
+ const ownerDateConfig = (owner: CalendarProps): DateContext => ({
176
+ ...owner.dateConfig,
177
+ timeZone: owner.timeZone ?? owner.dateConfig?.timeZone,
178
+ firstDayOfWeek: owner.firstDayOfWeek ?? owner.dateConfig?.firstDayOfWeek ?? owner.dateConfig?.weekStartsOn ?? 1,
179
+ });
180
+
181
+ const yearIndicatorClass = (date: Date, color: CalendarEventColor, context?: DateContext): string =>
182
+ calendar.isToday(date, context) ? "bg-white" : dotClass[color];
183
+ let activeDraggedEventId = "";
184
+
185
+ const parseDate = (value: Date | string): Date => {
186
+ if (value instanceof Date) return new Date(value);
187
+ const normalized = value.includes("T") ? value : value.replace(" ", "T");
188
+ const parsed = new Date(normalized);
189
+ if (!Number.isNaN(parsed.getTime())) return parsed;
190
+ const [year, month = "1", day = "1"] = value.split("-");
191
+ return new Date(Number(year), Number(month) - 1, Number(day), 12);
192
+ };
193
+
194
+ const weekNumber = (date: Date): number => {
195
+ const target = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
196
+ const day = target.getUTCDay() || 7;
197
+ target.setUTCDate(target.getUTCDate() + 4 - day);
198
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
199
+ return Math.ceil(((target.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
200
+ };
201
+ const zonedWeekNumber = (date: Date, context?: DateContext): number => {
202
+ if (!context?.timeZone) return weekNumber(date);
203
+ const [year = "1970", month = "1", day = "1"] = calendar.formatDateKey(date, context).split("-");
204
+ return weekNumber(new Date(Date.UTC(Number(year), Number(month) - 1, Number(day))));
205
+ };
206
+
207
+ const formatTime = (date: Date, context?: DateContext): string => calendar.formatTime(date, context);
208
+ const formatDay = (date: Date, context?: DateContext): string =>
209
+ date.toLocaleDateString(context?.locale ?? "en", { weekday: "short", day: "numeric", timeZone: context?.timeZone });
210
+ const formatMonth = (date: Date, context?: DateContext): string => calendar.formatMonthYear(date, context);
211
+ const zonedYearMonth = (date: Date, context?: DateContext): { year: number; month: number } => {
212
+ const [year = "1970", month = "1"] = calendar.formatDateKey(date, context).split("-");
213
+ return { year: Number(year), month: Number(month) - 1 };
214
+ };
215
+ const zonedMonthDate = (year: number, month: number, context?: DateContext): Date => {
216
+ const value = `${year}-${String(month + 1).padStart(2, "0")}-01T12:00`;
217
+ if (!context?.timeZone) return parseDate(value);
218
+ return new Date(calendar.zonedDateTimeToInstant(value, context.timeZone, { disambiguation: "compatible" }));
219
+ };
220
+
221
+ const startOfDay = (date: Date, context?: DateContext): Date => calendar.startOfDay(date, context);
222
+ const endOfDay = (date: Date, context?: DateContext): Date => calendar.endOfDay(date, context);
223
+ const isStartOfDay = (date: Date, context?: DateContext): boolean => date.getTime() === startOfDay(date, context).getTime();
224
+ const addMinutes = (date: Date, minutes: number): Date => new Date(date.getTime() + minutes * 60 * 1000);
225
+ const zonedHour = (date: Date, context?: DateContext): number => {
226
+ if (!context?.timeZone) return date.getHours() + date.getMinutes() / 60;
227
+ const value = calendar.instantToZonedInput(date, context.timeZone);
228
+ return Number(value.slice(11, 13)) + Number(value.slice(14, 16)) / 60;
229
+ };
230
+ const zonedSlot = (day: Date, hour: number, context?: DateContext): Date => {
231
+ const value = `${calendar.formatDateKey(day, context)}T${String(hour).padStart(2, "0")}:00`;
232
+ if (!context?.timeZone) return parseDate(value);
233
+ return new Date(calendar.zonedDateTimeToInstant(value, context.timeZone, { disambiguation: "compatible" }));
234
+ };
235
+ const roundToMinutes = (date: Date, minutes: number): Date => {
236
+ const next = new Date(date);
237
+ const rounded = Math.round(next.getMinutes() / minutes) * minutes;
238
+ next.setMinutes(rounded, 0, 0);
239
+ return next;
240
+ };
241
+
242
+ const normalizeEvents = (events: CalendarEvent[], context?: DateContext): NormalizedEvent[] =>
243
+ events.flatMap((event) => {
244
+ const startDate = parseDate(event.start);
245
+ const endDate = event.end ? parseDate(event.end) : new Date(startDate.getTime() + 60 * 60 * 1000);
246
+ const duration = Math.max(60 * 60 * 1000, endDate.getTime() - startDate.getTime());
247
+ const rangeEnd = endDate > startDate ? endDate : new Date(startDate.getTime() + duration);
248
+ const startKey = calendar.formatDateKey(startDate, context);
249
+ const endKey = calendar.formatDateKey(rangeEnd, context);
250
+ if (!event.allDay && startKey === endKey) {
251
+ return [
252
+ {
253
+ ...event,
254
+ startDate,
255
+ endDate: rangeEnd,
256
+ sourceStartDate: startDate,
257
+ sourceEndDate: rangeEnd,
258
+ dayKey: startKey,
259
+ },
260
+ ];
261
+ }
262
+ const lastDay =
263
+ event.allDay && isStartOfDay(rangeEnd, context)
264
+ ? calendar.addDays(startOfDay(rangeEnd, context), -1, context)
265
+ : startOfDay(rangeEnd, context);
266
+ const days: NormalizedEvent[] = [];
267
+ for (let day = startOfDay(startDate, context); day <= lastDay; day = calendar.addDays(day, 1, context)) {
268
+ const segmentStart = day.getTime() === startOfDay(startDate, context).getTime() ? startDate : startOfDay(day, context);
269
+ const segmentEnd = day.getTime() === startOfDay(rangeEnd, context).getTime() ? rangeEnd : endOfDay(day, context);
270
+ days.push({
271
+ ...event,
272
+ startDate: segmentStart,
273
+ endDate: segmentEnd,
274
+ sourceStartDate: startDate,
275
+ sourceEndDate: rangeEnd,
276
+ dayKey: calendar.formatDateKey(day, context),
277
+ allDay: event.allDay,
278
+ });
279
+ }
280
+ return days;
281
+ });
282
+
283
+ const eventHref = (props: CalendarProps, event: CalendarEvent): string | undefined => props.getEventHref?.(event) ?? event.href;
284
+
285
+ const draggedEventId = (event: DragEvent): string =>
286
+ activeDraggedEventId || event.dataTransfer?.getData("application/x-calendar-event") || event.dataTransfer?.getData("text/plain") || "";
287
+
288
+ const moveEventTo = (event: NormalizedEvent, target: Date, allDay = false, context?: DateContext): CalendarEventTimeChange => {
289
+ const duration = Math.max(30 * 60 * 1000, event.sourceEndDate.getTime() - event.sourceStartDate.getTime());
290
+ const start = allDay ? startOfDay(target, context) : new Date(target);
291
+ start.setSeconds(0, 0);
292
+ return { start, end: new Date(start.getTime() + duration), allDay };
293
+ };
294
+
295
+ const previewSegments = (preview: CalendarPreview | null, days: Date[], context?: DateContext): NormalizedEvent[] =>
296
+ preview
297
+ ? normalizeEvents(
298
+ [
299
+ {
300
+ id: `preview-${preview.id}`,
301
+ title: "Preview",
302
+ start: preview.start,
303
+ end: preview.end,
304
+ allDay: preview.allDay,
305
+ color: "blue",
306
+ },
307
+ ],
308
+ context,
309
+ ).filter((event) => days.some((day) => event.dayKey === calendar.formatDateKey(day, context)))
310
+ : [];
311
+
312
+ const timedEventLayouts = (events: NormalizedEvent[]): TimedEventLayout[] => {
313
+ const sorted = [...events].sort((a, b) => a.startDate.getTime() - b.startDate.getTime() || b.endDate.getTime() - a.endDate.getTime());
314
+ const groups: NormalizedEvent[][] = [];
315
+ let currentGroup: NormalizedEvent[] = [];
316
+ let currentGroupEnd = 0;
317
+
318
+ for (const event of sorted) {
319
+ const start = event.startDate.getTime();
320
+ const end = event.endDate.getTime();
321
+ if (currentGroup.length === 0 || start < currentGroupEnd) {
322
+ currentGroup.push(event);
323
+ currentGroupEnd = Math.max(currentGroupEnd, end);
324
+ continue;
325
+ }
326
+ groups.push(currentGroup);
327
+ currentGroup = [event];
328
+ currentGroupEnd = end;
329
+ }
330
+ if (currentGroup.length > 0) groups.push(currentGroup);
331
+
332
+ return groups.flatMap((group, groupId) => {
333
+ const laneEnds: number[] = [];
334
+ const assigned = group.map((event) => {
335
+ const start = event.startDate.getTime();
336
+ const lane = laneEnds.findIndex((end) => end <= start);
337
+ const nextLane = lane >= 0 ? lane : laneEnds.length;
338
+ laneEnds[nextLane] = event.endDate.getTime();
339
+ return { event, lane: nextLane };
340
+ });
341
+ const lanes = Math.max(1, laneEnds.length);
342
+ const groupStartDate = new Date(Math.min(...group.map((event) => event.startDate.getTime())));
343
+ const groupEndDate = new Date(Math.max(...group.map((event) => event.endDate.getTime())));
344
+ return assigned.map((item) => ({ ...item, lanes, groupId, groupStartDate, groupEndDate }));
345
+ });
346
+ };
347
+
348
+ const EventChip = (props: {
349
+ event: NormalizedEvent;
350
+ owner: CalendarProps;
351
+ href?: string;
352
+ compact?: boolean;
353
+ fill?: boolean;
354
+ }): JSX.Element => {
355
+ const dateConfig = () => ownerDateConfig(props.owner);
356
+ const color = () => props.event.color ?? "blue";
357
+ const style = () => (props.event.colorHex ? { "border-left-color": props.event.colorHex } : undefined);
358
+ const selected = () => props.owner.selectedEventId === props.event.id;
359
+ const className = () =>
360
+ `block min-w-0 rounded-lg border border-l-[3px] border-zinc-200 bg-white text-left leading-tight [box-shadow:var(--theme-bevel-top),var(--theme-bevel-bottom)] transition-[background-color,border-color,box-shadow] dark:border-zinc-700/70 dark:bg-zinc-900 ${props.compact ? "px-1 py-1" : "px-2 py-1.5"} ${props.fill ? "h-full" : ""} ${props.owner.onEventDrop ? "cursor-grab active:cursor-grabbing" : ""} ${props.event.display === "background" ? "opacity-60" : ""} ${props.event.colorHex ? "text-primary" : colorClass[color()]} ${selected() ? "border-blue-400 bg-blue-50 shadow-[inset_0_0_0_1px_rgb(59_130_246_/_0.26)] dark:border-blue-400 dark:bg-blue-950/45 dark:shadow-[inset_0_0_0_1px_rgb(96_165_250_/_0.30)]" : "hover:border-blue-500/40 hover:bg-blue-500/[0.04] dark:hover:border-blue-400/45 dark:hover:bg-blue-400/[0.06]"}`;
361
+ const durationHours = () => (props.event.endDate.getTime() - props.event.startDate.getTime()) / 3_600_000;
362
+ const showTime = () => !props.event.allDay && !props.compact && durationHours() >= 0.75;
363
+ const showLocation = () => Boolean(props.event.location && !props.compact && durationHours() >= 1.25);
364
+ const isInteractive = () => Boolean(props.owner.onEventClick || props.owner.onEventDoubleClick);
365
+ const timeLabel = () => `${formatTime(props.event.startDate, dateConfig())} - ${formatTime(props.event.endDate, dateConfig())}`;
366
+ const renderedEvent = () =>
367
+ props.owner.renderEvent?.(props.event, {
368
+ compact: props.compact ?? false,
369
+ fill: props.fill ?? false,
370
+ start: props.event.startDate,
371
+ end: props.event.endDate,
372
+ allDay: props.event.allDay ?? false,
373
+ durationHours: durationHours(),
374
+ timeLabel: timeLabel(),
375
+ });
376
+ const dragProps = () =>
377
+ props.owner.onEventDrop
378
+ ? {
379
+ draggable: true,
380
+ onDragStart: (event: DragEvent) => {
381
+ activeDraggedEventId = props.event.id;
382
+ event.dataTransfer?.setData("application/x-calendar-event", props.event.id);
383
+ event.dataTransfer?.setData("text/plain", props.event.id);
384
+ },
385
+ onDragEnd: () => {
386
+ activeDraggedEventId = "";
387
+ },
388
+ }
389
+ : {};
390
+ const defaultContent = (
391
+ <>
392
+ <span class={`block truncate text-[11px] font-semibold ${selected() ? "text-blue-700 dark:text-blue-200" : "text-primary"}`}>
393
+ {props.event.title}
394
+ </span>
395
+ <Show when={showTime()}>
396
+ <span class="block truncate text-[10px] text-secondary">{timeLabel()}</span>
397
+ </Show>
398
+ <Show when={showLocation()}>
399
+ <span class="block truncate text-[10px] text-secondary">{props.event.location}</span>
400
+ </Show>
401
+ </>
402
+ );
403
+ const content = () => renderedEvent() ?? defaultContent;
404
+ let eventElement: HTMLAnchorElement | HTMLDivElement | undefined;
405
+ let clickTimer: ReturnType<typeof setTimeout> | undefined;
406
+ onCleanup(() => {
407
+ if (clickTimer) clearTimeout(clickTimer);
408
+ });
409
+ const onClick = (event: MouseEvent) => {
410
+ if (!props.owner.onEventClick) return;
411
+ event.preventDefault();
412
+ event.stopPropagation();
413
+ if (clickTimer) clearTimeout(clickTimer);
414
+ clickTimer = setTimeout(
415
+ () => {
416
+ clickTimer = undefined;
417
+ props.owner.onEventClick?.(props.event);
418
+ },
419
+ props.owner.onEventDoubleClick ? 220 : 0,
420
+ );
421
+ };
422
+ const onDoubleClick = (event: MouseEvent) => {
423
+ if (!props.owner.onEventDoubleClick) return;
424
+ event.preventDefault();
425
+ event.stopPropagation();
426
+ if (clickTimer) {
427
+ clearTimeout(clickTimer);
428
+ clickTimer = undefined;
429
+ }
430
+ props.owner.onEventDoubleClick(props.event);
431
+ };
432
+ onMount(() => {
433
+ if (!props.owner.onEventDoubleClick || !eventElement) return;
434
+ const onNativeDoubleClick = (event: Event) => onDoubleClick(event as MouseEvent);
435
+ eventElement.addEventListener("dblclick", onNativeDoubleClick);
436
+ onCleanup(() => eventElement?.removeEventListener("dblclick", onNativeDoubleClick));
437
+ });
438
+ const onKeyDown = (event: KeyboardEvent) => {
439
+ if (!isInteractive() || (event.key !== "Enter" && event.key !== " ")) return;
440
+ event.preventDefault();
441
+ (props.owner.onEventClick ?? props.owner.onEventDoubleClick)?.(props.event);
442
+ };
443
+
444
+ return props.href ? (
445
+ <a
446
+ ref={(element) => {
447
+ eventElement = element;
448
+ }}
449
+ href={props.href}
450
+ class={className()}
451
+ data-calendar-event=""
452
+ data-space-item-id={props.event.dataSpaceItemId}
453
+ style={style()}
454
+ onClick={onClick}
455
+ onKeyDown={onKeyDown}
456
+ aria-label={`${props.event.title}${props.event.allDay ? "" : `, ${formatTime(props.event.startDate, dateConfig())} to ${formatTime(props.event.endDate, dateConfig())}`}`}
457
+ {...dragProps()}
458
+ >
459
+ {content()}
460
+ </a>
461
+ ) : (
462
+ <div
463
+ ref={(element) => {
464
+ eventElement = element;
465
+ }}
466
+ class={className()}
467
+ data-calendar-event=""
468
+ style={style()}
469
+ role="button"
470
+ tabIndex={isInteractive() ? 0 : undefined}
471
+ onClick={onClick}
472
+ onKeyDown={onKeyDown}
473
+ aria-label={`${props.event.title}${props.event.allDay ? "" : `, ${formatTime(props.event.startDate, dateConfig())} to ${formatTime(props.event.endDate, dateConfig())}`}`}
474
+ {...dragProps()}
475
+ >
476
+ {content()}
477
+ </div>
478
+ );
479
+ };
480
+
481
+ const slotInteractionProps = (owner: CalendarProps, slot: () => CalendarEventTimeChange) => {
482
+ const isSlotChild = (event: MouseEvent) =>
483
+ event.target instanceof Element && Boolean(event.target.closest("a,button,[data-calendar-event]"));
484
+ return {
485
+ onClick: (event: MouseEvent) => {
486
+ if (!owner.onSlotClick || isSlotChild(event)) return;
487
+ owner.onSlotClick(slot());
488
+ },
489
+ onDblClick: (event: MouseEvent) => {
490
+ if (!owner.onSlotDoubleClick || isSlotChild(event)) return;
491
+ owner.onSlotDoubleClick(slot());
492
+ },
493
+ };
494
+ };
495
+
496
+ const dropTargetProps = (owner: CalendarProps, events: NormalizedEvent[], target: () => Date, allDay = false) => {
497
+ if (!owner.onEventDrop) return {};
498
+ return {
499
+ onDragOver: (event: DragEvent) => event.preventDefault(),
500
+ onDrop: (event: DragEvent) => {
501
+ event.preventDefault();
502
+ const id = draggedEventId(event);
503
+ const calendarEvent = events.find((candidate) => candidate.id === id);
504
+ if (!calendarEvent) return;
505
+ owner.onEventDrop?.(calendarEvent, moveEventTo(calendarEvent, target(), allDay, ownerDateConfig(owner)));
506
+ },
507
+ };
508
+ };
509
+
510
+ const dropPreviewProps = (
511
+ owner: CalendarProps,
512
+ events: NormalizedEvent[],
513
+ preview: Accessor<string>,
514
+ setPreview: (value: string) => void,
515
+ key: string,
516
+ target: () => Date,
517
+ allDay = false,
518
+ ) => {
519
+ if (!owner.onEventDrop) return {};
520
+ return {
521
+ onDragEnter: (event: DragEvent) => {
522
+ event.preventDefault();
523
+ setPreview(key);
524
+ },
525
+ onDragOver: (event: DragEvent) => {
526
+ event.preventDefault();
527
+ if (preview() !== key) setPreview(key);
528
+ },
529
+ onDragLeave: () => {
530
+ if (preview() === key) setPreview("");
531
+ },
532
+ onDrop: (event: DragEvent) => {
533
+ dropTargetProps(owner, events, target, allDay).onDrop?.(event);
534
+ setPreview("");
535
+ },
536
+ };
537
+ };
538
+
539
+ const CalendarHeader = (props: { date: Date; view: CalendarView; labels: Required<CalendarLabels>; owner: CalendarProps }): JSX.Element => {
540
+ const dateConfig = () => ownerDateConfig(props.owner);
541
+ const previous = () => {
542
+ if (props.view === "year") return calendar.addMonths(props.date, -12, dateConfig());
543
+ if (props.view === "month" || props.view === "mobile-month") return calendar.addMonths(props.date, -1, dateConfig());
544
+ return calendar.addDays(props.date, props.view === "day" ? -1 : -7, dateConfig());
545
+ };
546
+ const next = () => {
547
+ if (props.view === "year") return calendar.addMonths(props.date, 12, dateConfig());
548
+ if (props.view === "month" || props.view === "mobile-month") return calendar.addMonths(props.date, 1, dateConfig());
549
+ return calendar.addDays(props.date, props.view === "day" ? 1 : 7, dateConfig());
550
+ };
551
+ const title = () => {
552
+ if (props.view === "year")
553
+ return new Intl.DateTimeFormat(dateConfig().locale ?? "en", { year: "numeric", timeZone: dateConfig().timeZone }).format(props.date);
554
+ if (props.view === "day")
555
+ return props.date.toLocaleDateString(dateConfig().locale ?? "en", {
556
+ weekday: "long",
557
+ month: "long",
558
+ day: "numeric",
559
+ year: "numeric",
560
+ timeZone: dateConfig().timeZone,
561
+ });
562
+ if (props.view === "week")
563
+ return `${formatDay(calendar.startOfWeek(props.date, dateConfig()), dateConfig())} - ${formatDay(calendar.addDays(calendar.startOfWeek(props.date, dateConfig()), 6, dateConfig()), dateConfig())}`;
564
+ return formatMonth(props.date, dateConfig());
565
+ };
566
+ const goDate = (date: Date) => props.owner.onDateChange?.(date, props.view);
567
+ const goView = (view: CalendarView) => {
568
+ if (props.owner.onViewChange) {
569
+ props.owner.onViewChange(view);
570
+ return;
571
+ }
572
+ const href = props.owner.getViewHref?.(view);
573
+ if (href) window.location.href = href;
574
+ };
575
+ const navButton = (date: Date, icon: string, label: string) => {
576
+ const href = props.owner.getDateHref?.(date, props.view);
577
+ return props.owner.onDateChange ? (
578
+ <button type="button" aria-label={label} class="btn-segment-icon" onClick={() => goDate(date)}>
579
+ <i class={`ti ${icon}`} />
580
+ </button>
581
+ ) : (
582
+ <a href={href ?? "#"} aria-label={label} class="btn-segment-icon">
583
+ <i class={`ti ${icon}`} />
584
+ </a>
585
+ );
586
+ };
587
+ const todayButton = () => {
588
+ const today = calendar.today(dateConfig());
589
+ const href = props.owner.getDateHref?.(today, props.view);
590
+ return props.owner.onDateChange ? (
591
+ <button type="button" class="btn-segment font-semibold" onClick={() => goDate(today)}>
592
+ {props.labels.today}
593
+ </button>
594
+ ) : (
595
+ <a href={href ?? "#"} class="btn-segment font-semibold">
596
+ {props.labels.today}
597
+ </a>
598
+ );
599
+ };
600
+ const viewOptions = () =>
601
+ (
602
+ [
603
+ { value: "day", label: props.labels.day },
604
+ { value: "week", label: props.labels.week },
605
+ { value: "month", label: props.labels.month },
606
+ { value: "year", label: props.labels.year },
607
+ ] satisfies Array<{ value: CalendarView; label: string }>
608
+ ).filter((option) => !props.owner.views || props.owner.views.includes(option.value));
609
+
610
+ return (
611
+ <header class="grid gap-2 border-b border-zinc-100 bg-white p-2 dark:border-zinc-800/70 dark:bg-zinc-900 sm:grid-cols-[auto_1fr_auto] sm:items-center">
612
+ <div class="flex items-center gap-1.5">
613
+ {navButton(previous(), "ti-chevron-left", props.labels.previous)}
614
+ <div class="btn-segment min-w-36 px-4 font-semibold">{title()}</div>
615
+ {navButton(next(), "ti-chevron-right", props.labels.next)}
616
+ {todayButton()}
617
+ </div>
618
+ <div />
619
+ <div class="w-full sm:w-auto">
620
+ <SegmentedControl
621
+ value={() => (props.view === "mobile-month" ? "month" : props.view)}
622
+ onChange={goView}
623
+ ariaLabel="Calendar view"
624
+ options={viewOptions()}
625
+ />
626
+ </div>
627
+ </header>
628
+ );
629
+ };
630
+
631
+ const MonthView = (props: {
632
+ owner: CalendarProps;
633
+ date: Date;
634
+ events: NormalizedEvent[];
635
+ labels: Required<CalendarLabels>;
636
+ }): JSX.Element => {
637
+ const dateConfig = () => ownerDateConfig(props.owner);
638
+ const [dropPreview, setDropPreview] = createSignal("");
639
+ const month = () => zonedYearMonth(props.date, dateConfig());
640
+ const weeks = () => calendar.getMonthGrid(month().year, month().month, dateConfig());
641
+ const weekdays = () => calendar.weekdays(dateConfig());
642
+ const todayKey = () => calendar.formatDateKey(new Date(), dateConfig());
643
+ const eventsByDay = createMemo(() => {
644
+ const grouped = new Map<string, NormalizedEvent[]>();
645
+ for (const event of props.events) {
646
+ const events = grouped.get(event.dayKey);
647
+ if (events) events.push(event);
648
+ else grouped.set(event.dayKey, [event]);
649
+ }
650
+ return grouped;
651
+ });
652
+ return (
653
+ <div>
654
+ <div
655
+ class={`grid ${props.owner.withWeekNumbers ? "grid-cols-[3rem_repeat(7,minmax(0,1fr))]" : "grid-cols-7"} border-b border-zinc-100 bg-white dark:border-zinc-800/70 dark:bg-zinc-900`}
656
+ >
657
+ <Show when={props.owner.withWeekNumbers}>
658
+ <div class="px-2 py-2 text-center text-[11px] font-semibold text-dimmed">Wk</div>
659
+ </Show>
660
+ <For each={weekdays()}>{(day) => <div class="px-2 py-2 text-center text-[11px] font-semibold text-dimmed">{day}</div>}</For>
661
+ </div>
662
+ <div class="grid divide-y divide-zinc-100 dark:divide-zinc-800/70">
663
+ <For each={weeks()}>
664
+ {(week) => (
665
+ <div
666
+ class={`grid min-h-24 ${props.owner.withWeekNumbers ? "grid-cols-[3rem_repeat(7,minmax(0,1fr))]" : "grid-cols-7"} divide-x divide-zinc-100 dark:divide-zinc-800/70`}
667
+ >
668
+ <Show when={props.owner.withWeekNumbers}>
669
+ <div class="flex items-start justify-center px-2 py-2 text-xs font-semibold text-dimmed">
670
+ {zonedWeekNumber(week[0]!, dateConfig())}
671
+ </div>
672
+ </Show>
673
+ <For each={week}>
674
+ {(day) => {
675
+ const dayKey = calendar.formatDateKey(day, dateConfig());
676
+ const events = eventsByDay().get(dayKey) ?? [];
677
+ const href = props.owner.getDateHref?.(day, "day");
678
+ const dayBadge = props.owner.dayBadges?.[dayKey];
679
+ const sameMonth = calendar.isSameMonth(day, props.date, dateConfig());
680
+ const isToday = dayKey === todayKey();
681
+ return (
682
+ <div
683
+ class={`relative min-w-0 p-1.5 ${sameMonth ? "" : "bg-zinc-50/60 dark:bg-zinc-950/25"}`}
684
+ classList={{
685
+ "bg-blue-500/10 ring-1 ring-inset ring-blue-400": dropPreview() === dayKey,
686
+ "cursor-pointer hover:bg-blue-500/5": Boolean(props.owner.onSlotClick || props.owner.onSlotDoubleClick),
687
+ }}
688
+ {...slotInteractionProps(props.owner, () => {
689
+ const start = startOfDay(day, dateConfig());
690
+ return { start, end: calendar.addDays(start, 1, dateConfig()), allDay: true };
691
+ })}
692
+ {...dropPreviewProps(
693
+ props.owner,
694
+ props.events,
695
+ dropPreview,
696
+ setDropPreview,
697
+ dayKey,
698
+ () => startOfDay(day, dateConfig()),
699
+ true,
700
+ )}
701
+ >
702
+ <div class="flex items-center gap-1">
703
+ <a
704
+ href={href ?? "#"}
705
+ class={`inline-flex h-6 min-w-6 items-center justify-center rounded-full px-1 text-xs font-semibold ${
706
+ isToday ? "bg-blue-500 text-white" : sameMonth ? "text-primary" : "text-dimmed"
707
+ }`}
708
+ >
709
+ {calendar.formatDayNumber(day, dateConfig())}
710
+ </a>
711
+ <Show when={dayBadge}>
712
+ {(badge) => (
713
+ <span class="hidden items-center gap-0.5 text-[10px] text-dimmed md:inline-flex">
714
+ <Show when={badge().icon}>{(icon) => <i class={`ti ti-${icon()} text-[10px]`} />}</Show>
715
+ {badge().label}
716
+ </span>
717
+ )}
718
+ </Show>
719
+ </div>
720
+ <div class="mt-1 hidden flex-col gap-1 md:flex">
721
+ <For each={events.slice(0, 3)}>
722
+ {(event) => <EventChip event={event} owner={props.owner} href={eventHref(props.owner, event)} compact />}
723
+ </For>
724
+ <Show when={events.length > 3}>
725
+ <a href={href ?? "#"} class="px-1 text-[11px] font-medium text-dimmed hover:text-primary">
726
+ +{events.length - 3} more
727
+ </a>
728
+ </Show>
729
+ </div>
730
+ <div class="mt-1 flex gap-0.5 md:hidden">
731
+ <For each={events.slice(0, 4)}>
732
+ {(event) => (
733
+ <span
734
+ class={`h-1.5 w-1.5 rounded-full ${event.colorHex ? "" : dotClass[event.color ?? "blue"]}`}
735
+ style={event.colorHex ? { "background-color": event.colorHex } : undefined}
736
+ />
737
+ )}
738
+ </For>
739
+ </div>
740
+ </div>
741
+ );
742
+ }}
743
+ </For>
744
+ </div>
745
+ )}
746
+ </For>
747
+ </div>
748
+ </div>
749
+ );
750
+ };
751
+
752
+ const TimeGridView = (props: {
753
+ owner: CalendarProps;
754
+ date: Date;
755
+ events: NormalizedEvent[];
756
+ labels: Required<CalendarLabels>;
757
+ days: Date[];
758
+ }): JSX.Element => {
759
+ const dateConfig = () => ownerDateConfig(props.owner);
760
+ const gridStartHour = () => props.owner.visibleStartHour ?? 0;
761
+ const gridEndHour = () => props.owner.visibleEndHour ?? 23;
762
+ const businessStartHour = () => props.owner.startHour ?? 8;
763
+ const businessEndHour = () => props.owner.endHour ?? 18;
764
+ const hours = () => Array.from({ length: gridEndHour() - gridStartHour() + 1 }, (_, index) => gridStartHour() + index);
765
+ const [dropPreview, setDropPreview] = createSignal("");
766
+ const [timePreview, setTimePreview] = createSignal<CalendarPreview | null>(null);
767
+ const [expandedOverflow, setExpandedOverflow] = createSignal("");
768
+ let scrollContainer: HTMLDivElement | undefined;
769
+ let defaultHourMarker: HTMLDivElement | undefined;
770
+ const slotEnd = (start: Date) => addMinutes(start, 60);
771
+ const allDayKey = (day: Date) => `${calendar.formatDateKey(day, dateConfig())}-all-day`;
772
+ const previewEvents = () => previewSegments(timePreview(), props.days, dateConfig());
773
+ const timeDropProps = (key: string, target: () => Date, allDay = false) => {
774
+ if (!props.owner.onEventDrop) return {};
775
+ return {
776
+ onDragEnter: (event: DragEvent) => {
777
+ event.preventDefault();
778
+ const id = draggedEventId(event);
779
+ const calendarEvent = props.events.find((candidate) => candidate.id === id);
780
+ setDropPreview(key);
781
+ if (calendarEvent) setTimePreview({ id: calendarEvent.id, ...moveEventTo(calendarEvent, target(), allDay, dateConfig()) });
782
+ },
783
+ onDragOver: (event: DragEvent) => {
784
+ event.preventDefault();
785
+ const id = draggedEventId(event);
786
+ const calendarEvent = props.events.find((candidate) => candidate.id === id);
787
+ setDropPreview(key);
788
+ if (calendarEvent) setTimePreview({ id: calendarEvent.id, ...moveEventTo(calendarEvent, target(), allDay, dateConfig()) });
789
+ },
790
+ onDragLeave: () => {
791
+ if (dropPreview() === key) {
792
+ setDropPreview("");
793
+ setTimePreview(null);
794
+ }
795
+ },
796
+ onDrop: (event: DragEvent) => {
797
+ dropTargetProps(props.owner, props.events, target, allDay).onDrop?.(event);
798
+ setDropPreview("");
799
+ setTimePreview(null);
800
+ },
801
+ };
802
+ };
803
+ const timeRangeLayout = (startDate: Date, endDate: Date) => {
804
+ const start = zonedHour(startDate, dateConfig());
805
+ const end = zonedHour(endDate, dateConfig());
806
+ const visibleStart = Math.max(gridStartHour(), start);
807
+ const visibleEnd = Math.min(gridEndHour() + 1, end);
808
+ return {
809
+ top: Math.max(0, (visibleStart - gridStartHour()) * 4),
810
+ height: Math.max(2.5, (visibleEnd - visibleStart) * 4),
811
+ };
812
+ };
813
+ const eventLayout = (event: NormalizedEvent) => timeRangeLayout(event.startDate, event.endDate);
814
+ const isDayView = () => props.days.length === 1;
815
+ const visibleLaneCount = (lanes: number) => (isDayView() ? lanes : Math.min(lanes, 3));
816
+ const overflowLayouts = (layouts: TimedEventLayout[]): TimedOverflowLayout[] => {
817
+ if (isDayView()) return [];
818
+ const groups = new Map<number, TimedOverflowLayout>();
819
+ for (const layout of layouts) {
820
+ if (layout.lane < visibleLaneCount(layout.lanes)) continue;
821
+ const existing = groups.get(layout.groupId);
822
+ if (existing) existing.hiddenEvents.push(layout.event);
823
+ else
824
+ groups.set(layout.groupId, {
825
+ groupId: layout.groupId,
826
+ hiddenEvents: [layout.event],
827
+ groupStartDate: layout.groupStartDate,
828
+ groupEndDate: layout.groupEndDate,
829
+ });
830
+ }
831
+ return [...groups.values()];
832
+ };
833
+ const dayColumnMinWidth = (layouts: TimedEventLayout[]) => {
834
+ if (!isDayView()) return undefined;
835
+ const lanes = Math.max(1, ...layouts.map((layout) => layout.lanes));
836
+ return `${Math.max(32, lanes * 12)}rem`;
837
+ };
838
+ const laneStyle = (layoutItem: TimedEventLayout) => {
839
+ if (isDayView()) {
840
+ const laneWidth = 100 / layoutItem.lanes;
841
+ return {
842
+ left: `calc(${layoutItem.lane * laneWidth}% + 0.25rem)`,
843
+ width: `calc(${laneWidth}% - 0.5rem)`,
844
+ };
845
+ }
846
+ const visibleLanes = visibleLaneCount(layoutItem.lanes);
847
+ return {
848
+ left: `${layoutItem.lanes <= 1 ? 0 : (28 / Math.max(1, visibleLanes - 1)) * layoutItem.lane}%`,
849
+ width: `${layoutItem.lanes <= 1 ? 100 : layoutItem.lanes > visibleLanes ? 68 : 72}%`,
850
+ };
851
+ };
852
+ const currentTimeLine = (day: Date) => {
853
+ const now = new Date();
854
+ if (calendar.formatDateKey(day, dateConfig()) !== calendar.formatDateKey(now, dateConfig())) return null;
855
+ const hour = zonedHour(now, dateConfig());
856
+ if (hour < gridStartHour() || hour > gridEndHour() + 1) return null;
857
+ return (hour - gridStartHour()) * 4;
858
+ };
859
+ onMount(() => {
860
+ requestAnimationFrame(() => {
861
+ if (!scrollContainer || !defaultHourMarker) return;
862
+ const targetTop = defaultHourMarker.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;
863
+ scrollContainer.scrollTo({ top: Math.max(0, scrollContainer.scrollTop + targetTop), behavior: "smooth" });
864
+ });
865
+ });
866
+ return (
867
+ <div class="flex min-h-0 min-w-160 flex-1 flex-col">
868
+ <div
869
+ class="grid border-b border-zinc-100 bg-white dark:border-zinc-800/70 dark:bg-zinc-900"
870
+ style={{ "grid-template-columns": `4rem repeat(${props.days.length}, minmax(0, 1fr))` }}
871
+ >
872
+ <div />
873
+ <For each={props.days}>
874
+ {(day) => {
875
+ const dayBadge = props.owner.dayBadges?.[calendar.formatDateKey(day, dateConfig())];
876
+ const today = () => calendar.isToday(day, dateConfig());
877
+ return (
878
+ <a
879
+ href={props.owner.getDateHref?.(day, "day") ?? "#"}
880
+ class="px-2 py-2 text-center text-[11px] font-semibold text-primary hover:text-blue-500"
881
+ >
882
+ <span
883
+ classList={{
884
+ "inline-flex rounded-full bg-blue-600 px-2.5 py-0.5 text-white": today(),
885
+ }}
886
+ >
887
+ {formatDay(day, dateConfig())}
888
+ </span>
889
+ <Show when={dayBadge}>
890
+ {(badge) => (
891
+ <span class="mt-0.5 flex items-center justify-center gap-0.5 text-[10px] font-medium text-dimmed">
892
+ <Show when={badge().icon}>{(icon) => <i class={`ti ti-${icon()} text-[10px]`} />}</Show>
893
+ {badge().label}
894
+ </span>
895
+ )}
896
+ </Show>
897
+ </a>
898
+ );
899
+ }}
900
+ </For>
901
+ </div>
902
+ <Show when={!props.owner.hideAllDay}>
903
+ <div
904
+ class="grid overflow-y-auto border-b border-zinc-200/80 bg-zinc-50/65 dark:border-zinc-800/80 dark:bg-zinc-950/40"
905
+ style={{
906
+ "grid-template-columns": `4rem repeat(${props.days.length}, minmax(0, 1fr))`,
907
+ "max-height": `${props.owner.allDayMaxHeightRem ?? 7}rem`,
908
+ }}
909
+ >
910
+ <div class="sticky top-0 bg-inherit px-2 py-2 text-center text-[11px] font-semibold text-dimmed">{props.labels.allDay}</div>
911
+ <For each={props.days}>
912
+ {(day) => {
913
+ const dayKey = calendar.formatDateKey(day, dateConfig());
914
+ const allDay = () => props.events.filter((event) => event.dayKey === dayKey && event.allDay);
915
+ const previewAllDay = previewEvents().filter((event) => event.dayKey === dayKey && event.allDay);
916
+ return (
917
+ <div
918
+ class="min-h-10 border-r border-zinc-100 p-1 dark:border-zinc-800/70"
919
+ classList={{
920
+ "rounded bg-blue-500/10 ring-1 ring-inset ring-blue-400": dropPreview() === allDayKey(day),
921
+ "cursor-pointer hover:bg-blue-500/5": Boolean(props.owner.onSlotClick || props.owner.onSlotDoubleClick),
922
+ }}
923
+ {...slotInteractionProps(props.owner, () => {
924
+ const start = startOfDay(day, dateConfig());
925
+ return { start, end: calendar.addDays(start, 1, dateConfig()), allDay: true };
926
+ })}
927
+ {...dropPreviewProps(
928
+ props.owner,
929
+ props.events,
930
+ dropPreview,
931
+ setDropPreview,
932
+ allDayKey(day),
933
+ () => startOfDay(day, dateConfig()),
934
+ true,
935
+ )}
936
+ >
937
+ <div class="flex flex-col gap-1">
938
+ <For each={previewAllDay}>
939
+ {(event) => (
940
+ <div class="rounded-lg border border-dashed border-blue-500 bg-blue-500/10 px-1.5 py-1 text-[10px] font-semibold text-blue-600">
941
+ {event.title}
942
+ </div>
943
+ )}
944
+ </For>
945
+ <For each={allDay()}>
946
+ {(event) => <EventChip event={event} owner={props.owner} href={eventHref(props.owner, event)} compact />}
947
+ </For>
948
+ </div>
949
+ </div>
950
+ );
951
+ }}
952
+ </For>
953
+ </div>
954
+ </Show>
955
+ <div ref={scrollContainer} class="min-h-0 flex-1 overflow-auto bg-white dark:bg-zinc-900">
956
+ <div class="grid" style={{ "grid-template-columns": `4rem repeat(${props.days.length}, minmax(0, 1fr))` }}>
957
+ <div class="border-r border-zinc-100 dark:border-zinc-800/70">
958
+ <For each={hours()}>
959
+ {(hour) => (
960
+ <div
961
+ ref={(element) => {
962
+ if (hour === businessStartHour()) defaultHourMarker = element;
963
+ }}
964
+ class="h-16 border-b border-zinc-100 bg-zinc-50/60 pr-2 pt-1 text-right text-[11px] text-dimmed dark:border-zinc-800/70 dark:bg-zinc-950/35"
965
+ classList={{ "bg-zinc-100/70 dark:bg-zinc-950/70": hour < businessStartHour() || hour > businessEndHour() }}
966
+ >
967
+ {`${hour}`.padStart(2, "0")}:00
968
+ </div>
969
+ )}
970
+ </For>
971
+ </div>
972
+ <For each={props.days}>
973
+ {(day) => {
974
+ const dayKey = calendar.formatDateKey(day, dateConfig());
975
+ const timed = () => props.events.filter((event) => event.dayKey === dayKey && !event.allDay);
976
+ const layouts = () => timedEventLayouts(timed());
977
+ return (
978
+ <div
979
+ class="relative min-h-full border-r border-zinc-100 dark:border-zinc-800/70"
980
+ style={{ "min-width": dayColumnMinWidth(layouts()) }}
981
+ >
982
+ <Show when={currentTimeLine(day)}>
983
+ {(top) => (
984
+ <div class="pointer-events-none absolute inset-x-0 z-40 border-t border-red-500" style={{ top: `${top()}rem` }} />
985
+ )}
986
+ </Show>
987
+ <For each={hours()}>
988
+ {(hour) => (
989
+ <div
990
+ class="relative h-16 border-b border-zinc-100 dark:border-zinc-800/70"
991
+ classList={{
992
+ "bg-blue-500/10 ring-1 ring-inset ring-blue-400": dropPreview() === `${dayKey}-${hour}`,
993
+ "bg-zinc-50/70 dark:bg-zinc-950/45": hour < businessStartHour() || hour > businessEndHour(),
994
+ "cursor-pointer hover:bg-blue-500/5": Boolean(props.owner.onSlotClick || props.owner.onSlotDoubleClick),
995
+ }}
996
+ {...slotInteractionProps(props.owner, () => {
997
+ const start = zonedSlot(day, hour, dateConfig());
998
+ return { start, end: slotEnd(start), allDay: false };
999
+ })}
1000
+ {...timeDropProps(`${dayKey}-${hour}`, () => zonedSlot(day, hour, dateConfig()), false)}
1001
+ />
1002
+ )}
1003
+ </For>
1004
+ <For each={previewEvents().filter((event) => event.dayKey === dayKey && !event.allDay)}>
1005
+ {(event) => {
1006
+ const layout = eventLayout(event);
1007
+ return (
1008
+ <div
1009
+ class="pointer-events-none absolute inset-x-1.5 z-30 rounded-lg border border-dashed border-blue-500 bg-blue-500/10"
1010
+ style={{ top: `${layout.top}rem`, height: `${layout.height}rem` }}
1011
+ >
1012
+ <div class="px-2 py-1 text-[10px] font-semibold text-blue-600">
1013
+ {formatTime(event.startDate, dateConfig())} - {formatTime(event.endDate, dateConfig())}
1014
+ </div>
1015
+ </div>
1016
+ );
1017
+ }}
1018
+ </For>
1019
+ <For each={layouts()}>
1020
+ {(layoutItem) => {
1021
+ if (!isDayView() && layoutItem.lane >= visibleLaneCount(layoutItem.lanes)) return null;
1022
+ const event = layoutItem.event;
1023
+ const layout = eventLayout(event);
1024
+ const position = laneStyle(layoutItem);
1025
+ const [resizePreview, setResizePreview] = createSignal<Date | null>(null);
1026
+ const resizeStart = (pointerEvent: PointerEvent) => {
1027
+ if (!props.owner.onEventResize) return;
1028
+ pointerEvent.preventDefault();
1029
+ pointerEvent.stopPropagation();
1030
+ const startY = pointerEvent.clientY;
1031
+ const initialEnd = event.sourceEndDate;
1032
+ let nextEnd = initialEnd;
1033
+ const onMove = (moveEvent: PointerEvent) => {
1034
+ const deltaMinutes = Math.round(((moveEvent.clientY - startY) / 64) * 60);
1035
+ const candidate = roundToMinutes(addMinutes(initialEnd, deltaMinutes), 15);
1036
+ if (candidate > event.sourceStartDate) {
1037
+ nextEnd = candidate;
1038
+ setResizePreview(candidate);
1039
+ }
1040
+ };
1041
+ const onUp = () => {
1042
+ window.removeEventListener("pointermove", onMove);
1043
+ window.removeEventListener("pointerup", onUp);
1044
+ setResizePreview(null);
1045
+ if (nextEnd.getTime() !== initialEnd.getTime()) {
1046
+ props.owner.onEventResize?.(event, { start: event.sourceStartDate, end: nextEnd, allDay: event.allDay });
1047
+ }
1048
+ };
1049
+ window.addEventListener("pointermove", onMove);
1050
+ window.addEventListener("pointerup", onUp, { once: true });
1051
+ };
1052
+ return (
1053
+ <div
1054
+ class="absolute"
1055
+ style={{
1056
+ top: `${layout.top}rem`,
1057
+ height: `${layout.height}rem`,
1058
+ left: position.left,
1059
+ width: position.width,
1060
+ "z-index": String(20 + layoutItem.lane),
1061
+ }}
1062
+ >
1063
+ <div class="group relative h-full">
1064
+ <EventChip event={event} owner={props.owner} href={eventHref(props.owner, event)} fill />
1065
+ <Show when={resizePreview()}>
1066
+ {(end) => (
1067
+ <>
1068
+ <div
1069
+ class="pointer-events-none absolute inset-x-0 top-0 z-10 rounded border border-dashed border-blue-500 bg-blue-500/10"
1070
+ style={{
1071
+ height: `${eventLayout({ ...event, endDate: end(), sourceEndDate: end() }).height}rem`,
1072
+ }}
1073
+ />
1074
+ <div class="pointer-events-none absolute inset-x-0 top-full z-30 rounded-b border border-dashed border-blue-500 bg-white/90 px-1.5 py-0.5 text-[10px] font-semibold text-blue-600 dark:bg-zinc-950/90">
1075
+ until {formatTime(end(), dateConfig())}
1076
+ </div>
1077
+ </>
1078
+ )}
1079
+ </Show>
1080
+ <Show when={props.owner.onEventResize}>
1081
+ <button
1082
+ type="button"
1083
+ aria-label="Resize event"
1084
+ draggable={false}
1085
+ class="absolute inset-x-3 bottom-1 z-20 flex h-4 cursor-ns-resize items-center justify-center rounded-full bg-blue-100/90 text-blue-600 opacity-0 backdrop-blur transition-opacity group-hover:opacity-90 focus:opacity-100 hover:opacity-100 dark:bg-blue-500/20 dark:text-blue-200"
1086
+ onPointerDown={resizeStart}
1087
+ onDragStart={(event) => event.preventDefault()}
1088
+ >
1089
+ <i class="ti ti-grip-horizontal text-[12px]" />
1090
+ </button>
1091
+ </Show>
1092
+ </div>
1093
+ </div>
1094
+ );
1095
+ }}
1096
+ </For>
1097
+ <For each={overflowLayouts(layouts())}>
1098
+ {(overflow) => {
1099
+ const layout = timeRangeLayout(overflow.groupStartDate, overflow.groupEndDate);
1100
+ const key = `${dayKey}-${overflow.groupId}`;
1101
+ const hiddenTitle = () =>
1102
+ overflow.hiddenEvents.map((event) => `${formatTime(event.startDate, dateConfig())} ${event.title}`).join("\n");
1103
+ return (
1104
+ <>
1105
+ <button
1106
+ type="button"
1107
+ class="absolute right-1 z-50 flex min-h-9 w-7 items-center justify-center rounded-lg border border-blue-500/35 bg-blue-500/10 text-[10px] font-black text-blue-700 transition-colors hover:bg-blue-500/15 dark:border-blue-300/30 dark:bg-blue-400/10 dark:text-blue-200 dark:hover:bg-blue-400/15"
1108
+ style={{ top: `${layout.top}rem`, height: `${layout.height}rem` }}
1109
+ title={hiddenTitle()}
1110
+ aria-label={`${overflow.hiddenEvents.length} hidden overlapping events`}
1111
+ onClick={(event) => {
1112
+ event.stopPropagation();
1113
+ setExpandedOverflow(expandedOverflow() === key ? "" : key);
1114
+ }}
1115
+ >
1116
+ <span class="[writing-mode:vertical-rl]">+{overflow.hiddenEvents.length}</span>
1117
+ </button>
1118
+ <Show when={expandedOverflow() === key}>
1119
+ <div
1120
+ class="absolute right-9 z-[70] flex w-56 flex-col gap-1 rounded-xl border border-zinc-200 bg-white p-2 shadow-lg dark:border-zinc-700 dark:bg-zinc-950"
1121
+ style={{ top: `${layout.top}rem` }}
1122
+ >
1123
+ <div class="px-1 pb-1 text-[10px] font-semibold text-dimmed">
1124
+ {formatTime(overflow.groupStartDate, dateConfig())} - {formatTime(overflow.groupEndDate, dateConfig())}
1125
+ </div>
1126
+ <For each={overflow.hiddenEvents}>
1127
+ {(event) => <EventChip event={event} owner={props.owner} href={eventHref(props.owner, event)} compact />}
1128
+ </For>
1129
+ </div>
1130
+ </Show>
1131
+ </>
1132
+ );
1133
+ }}
1134
+ </For>
1135
+ </div>
1136
+ );
1137
+ }}
1138
+ </For>
1139
+ </div>
1140
+ </div>
1141
+ </div>
1142
+ );
1143
+ };
1144
+
1145
+ const YearView = (props: { owner: CalendarProps; date: Date; events: NormalizedEvent[] }): JSX.Element => (
1146
+ <div class="grid grid-cols-1 divide-y divide-zinc-100 dark:divide-zinc-800/70 md:grid-cols-3 md:divide-x md:divide-y-0">
1147
+ <For
1148
+ each={Array.from({ length: 12 }, (_, month) =>
1149
+ zonedMonthDate(zonedYearMonth(props.date, ownerDateConfig(props.owner)).year, month, ownerDateConfig(props.owner)),
1150
+ )}
1151
+ >
1152
+ {(monthDate) => (
1153
+ <div class="p-3">
1154
+ <div class="mb-2 text-xs font-semibold text-primary">
1155
+ {monthDate.toLocaleDateString(ownerDateConfig(props.owner).locale ?? "en", {
1156
+ month: "long",
1157
+ timeZone: ownerDateConfig(props.owner).timeZone,
1158
+ })}
1159
+ </div>
1160
+ <div class="grid grid-cols-7 gap-1 text-center text-[10px]">
1161
+ <For
1162
+ each={calendar
1163
+ .getMonthGrid(
1164
+ zonedYearMonth(monthDate, ownerDateConfig(props.owner)).year,
1165
+ zonedYearMonth(monthDate, ownerDateConfig(props.owner)).month,
1166
+ ownerDateConfig(props.owner),
1167
+ )
1168
+ .flat()
1169
+ .slice(0, 35)}
1170
+ >
1171
+ {(day) => {
1172
+ const dateConfig = ownerDateConfig(props.owner);
1173
+ const events = props.events.filter((event) => event.dayKey === calendar.formatDateKey(day, dateConfig));
1174
+ return (
1175
+ <a
1176
+ href={props.owner.getDateHref?.(day, "day") ?? "#"}
1177
+ class={`relative flex aspect-square items-center justify-center rounded-md ${calendar.isToday(day, dateConfig) ? "bg-blue-500 text-white" : calendar.isSameMonth(day, monthDate, dateConfig) ? "text-primary hover:bg-zinc-100 dark:hover:bg-zinc-800" : "text-zinc-300 dark:text-zinc-700"}`}
1178
+ >
1179
+ {calendar.formatDayNumber(day, dateConfig)}
1180
+ <Show when={events.length > 0}>
1181
+ <span
1182
+ class={`absolute bottom-1 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full ${events[0]!.colorHex ? "" : yearIndicatorClass(day, events[0]!.color ?? "blue", dateConfig)}`}
1183
+ style={
1184
+ events[0]!.colorHex
1185
+ ? { "background-color": calendar.isToday(day, dateConfig) ? "white" : events[0]!.colorHex }
1186
+ : undefined
1187
+ }
1188
+ />
1189
+ </Show>
1190
+ </a>
1191
+ );
1192
+ }}
1193
+ </For>
1194
+ </div>
1195
+ </div>
1196
+ )}
1197
+ </For>
1198
+ </div>
1199
+ );
1200
+
1201
+ const MobileMonthView = (props: {
1202
+ owner: CalendarProps;
1203
+ date: Date;
1204
+ selectedDate: Date;
1205
+ events: NormalizedEvent[];
1206
+ labels: Required<CalendarLabels>;
1207
+ }): JSX.Element => {
1208
+ const dateConfig = () => ownerDateConfig(props.owner);
1209
+ const selectedEvents = () => props.events.filter((event) => event.dayKey === calendar.formatDateKey(props.selectedDate, dateConfig()));
1210
+ return (
1211
+ <div class="mx-auto max-w-md p-3">
1212
+ <MonthView owner={{ ...props.owner, withWeekNumbers: false }} date={props.date} events={props.events} labels={props.labels} />
1213
+ <div class="mt-4 border-t border-zinc-100 pt-3 dark:border-zinc-800/70">
1214
+ <div class="mb-2 text-sm font-semibold text-primary">
1215
+ {props.selectedDate.toLocaleDateString(dateConfig().locale ?? "en", {
1216
+ weekday: "long",
1217
+ month: "long",
1218
+ day: "numeric",
1219
+ timeZone: dateConfig().timeZone,
1220
+ })}
1221
+ </div>
1222
+ <Show when={selectedEvents().length > 0} fallback={<div class="py-6 text-center text-xs text-dimmed">{props.labels.noEvents}</div>}>
1223
+ <div class="flex flex-col gap-1">
1224
+ <For each={selectedEvents()}>
1225
+ {(event) => <EventChip event={event} owner={props.owner} href={eventHref(props.owner, event)} />}
1226
+ </For>
1227
+ </div>
1228
+ </Show>
1229
+ </div>
1230
+ </div>
1231
+ );
1232
+ };
1233
+
1234
+ const CalendarBody = (props: { children: JSX.Element }): JSX.Element => (
1235
+ <div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">{props.children}</div>
1236
+ );
1237
+
1238
+ const Calendar = (props: CalendarProps): JSX.Element => {
1239
+ const view = () => props.view ?? "month";
1240
+ const dateConfig = () => ownerDateConfig(props);
1241
+ const date = () => parseDate(props.date);
1242
+ const selectedDate = () => parseDate(props.selectedDate ?? props.date);
1243
+ const normalizedEvents = () => normalizeEvents(props.events, dateConfig());
1244
+ const mergedLabels = () => ({ ...labels, ...props.labels });
1245
+ const days = () => {
1246
+ if (view() === "day") return [date()];
1247
+ return calendar.getWeekDays(date(), dateConfig());
1248
+ };
1249
+
1250
+ return (
1251
+ <section class={`paper flex min-h-0 flex-col overflow-hidden ${props.class ?? ""}`}>
1252
+ <CalendarHeader date={date()} view={view()} labels={mergedLabels()} owner={props} />
1253
+ <Show
1254
+ when={view() !== "month"}
1255
+ fallback={
1256
+ <CalendarBody>
1257
+ <MonthView owner={props} date={date()} events={normalizedEvents()} labels={mergedLabels()} />
1258
+ </CalendarBody>
1259
+ }
1260
+ >
1261
+ <Show
1262
+ when={view() !== "year"}
1263
+ fallback={
1264
+ <CalendarBody>
1265
+ <YearView owner={props} date={date()} events={normalizedEvents()} />
1266
+ </CalendarBody>
1267
+ }
1268
+ >
1269
+ <Show
1270
+ when={view() !== "mobile-month"}
1271
+ fallback={
1272
+ <CalendarBody>
1273
+ <MobileMonthView
1274
+ owner={props}
1275
+ date={date()}
1276
+ selectedDate={selectedDate()}
1277
+ events={normalizedEvents()}
1278
+ labels={mergedLabels()}
1279
+ />
1280
+ </CalendarBody>
1281
+ }
1282
+ >
1283
+ <TimeGridView owner={props} date={date()} events={normalizedEvents()} labels={mergedLabels()} days={days()} />
1284
+ </Show>
1285
+ </Show>
1286
+ </Show>
1287
+ </section>
1288
+ );
1289
+ };
1290
+
1291
+ export default Calendar;