nuxt-ui-elements-pro 0.1.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/README.md +1 -0
- package/dist/module.d.mts +14 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +178 -0
- package/dist/runtime/components/EventCalendar.d.vue.ts +97 -0
- package/dist/runtime/components/EventCalendar.vue +646 -0
- package/dist/runtime/components/EventCalendar.vue.d.ts +97 -0
- package/dist/runtime/composables/useEventCalendarDragDrop.d.ts +24 -0
- package/dist/runtime/composables/useEventCalendarDragDrop.js +116 -0
- package/dist/runtime/index.css +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/types/event-calendar.d.ts +92 -0
- package/dist/runtime/types/event-calendar.js +0 -0
- package/dist/runtime/types/index.d.ts +2 -0
- package/dist/runtime/types/index.js +2 -0
- package/dist/runtime/utils/event-calendar.d.ts +16 -0
- package/dist/runtime/utils/event-calendar.js +67 -0
- package/dist/runtime/utils/tv.d.ts +1 -0
- package/dist/runtime/utils/tv.js +2 -0
- package/dist/types.d.mts +5 -0
- package/package.json +68 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import theme from "#build/ui-elements-pro/event-calendar";
|
|
3
|
+
import { toCalendarDate } from "@internationalized/date";
|
|
4
|
+
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
|
5
|
+
import { tv } from "../utils/tv";
|
|
6
|
+
import { hasTimeComponent, layoutTimedEvents } from "../utils/event-calendar";
|
|
7
|
+
import { useEventCalendarDragDrop } from "../composables/useEventCalendarDragDrop";
|
|
8
|
+
import {
|
|
9
|
+
today,
|
|
10
|
+
startOf,
|
|
11
|
+
add,
|
|
12
|
+
subtract,
|
|
13
|
+
format,
|
|
14
|
+
isSameMonth,
|
|
15
|
+
isToday as checkIsToday,
|
|
16
|
+
isWeekend as checkIsWeekend,
|
|
17
|
+
asCalendarDate,
|
|
18
|
+
asCalendarDateTime,
|
|
19
|
+
getLocalTimeZone,
|
|
20
|
+
getWeekStartLocale
|
|
21
|
+
} from "#std/date";
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
const {
|
|
26
|
+
events = [],
|
|
27
|
+
view = "month",
|
|
28
|
+
locale = "en-US",
|
|
29
|
+
weekStartsOn = 0,
|
|
30
|
+
editable = true,
|
|
31
|
+
color = "primary",
|
|
32
|
+
monthOptions: monthOpts,
|
|
33
|
+
weekOptions: weekOpts,
|
|
34
|
+
dayOptions: dayOpts,
|
|
35
|
+
ui: propUi,
|
|
36
|
+
...props
|
|
37
|
+
} = defineProps({
|
|
38
|
+
events: { type: Array, required: false },
|
|
39
|
+
modelValue: { type: null, required: false },
|
|
40
|
+
view: { type: String, required: false },
|
|
41
|
+
locale: { type: String, required: false },
|
|
42
|
+
weekStartsOn: { type: Number, required: false },
|
|
43
|
+
editable: { type: Boolean, required: false },
|
|
44
|
+
color: { type: null, required: false },
|
|
45
|
+
monthOptions: { type: Object, required: false },
|
|
46
|
+
weekOptions: { type: Object, required: false },
|
|
47
|
+
dayOptions: { type: Object, required: false },
|
|
48
|
+
ui: { type: Object, required: false }
|
|
49
|
+
});
|
|
50
|
+
const emit = defineEmits(["update:modelValue", "update:view", "dateClick", "eventClick", "eventDrop"]);
|
|
51
|
+
defineSlots();
|
|
52
|
+
const monthConfig = computed(() => ({
|
|
53
|
+
maxEvents: monthOpts?.maxEvents ?? 3,
|
|
54
|
+
fixedWeeks: monthOpts?.fixedWeeks ?? true
|
|
55
|
+
}));
|
|
56
|
+
const weekConfig = computed(() => ({
|
|
57
|
+
startHour: weekOpts?.startHour ?? 7,
|
|
58
|
+
endHour: weekOpts?.endHour ?? 22,
|
|
59
|
+
slotDuration: weekOpts?.slotDuration ?? 30
|
|
60
|
+
}));
|
|
61
|
+
const dayConfig = computed(() => ({
|
|
62
|
+
startHour: dayOpts?.startHour ?? 7,
|
|
63
|
+
endHour: dayOpts?.endHour ?? 22,
|
|
64
|
+
slotDuration: dayOpts?.slotDuration ?? 30
|
|
65
|
+
}));
|
|
66
|
+
const ui = computed(
|
|
67
|
+
() => tv({
|
|
68
|
+
extend: tv(theme)
|
|
69
|
+
})({
|
|
70
|
+
color
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
const displayDate = computed(() => {
|
|
74
|
+
if (props.modelValue) {
|
|
75
|
+
return asCalendarDate(props.modelValue);
|
|
76
|
+
}
|
|
77
|
+
return today();
|
|
78
|
+
});
|
|
79
|
+
const weekLocale = computed(() => getWeekStartLocale(weekStartsOn));
|
|
80
|
+
function goToPrev() {
|
|
81
|
+
let target;
|
|
82
|
+
if (view === "month") {
|
|
83
|
+
target = startOf(subtract(displayDate.value, 1, "month"), "month");
|
|
84
|
+
} else if (view === "week") {
|
|
85
|
+
target = asCalendarDate(subtract(displayDate.value, 1, "week"));
|
|
86
|
+
} else {
|
|
87
|
+
target = asCalendarDate(subtract(displayDate.value, 1, "day"));
|
|
88
|
+
}
|
|
89
|
+
emit("update:modelValue", target);
|
|
90
|
+
}
|
|
91
|
+
function goToNext() {
|
|
92
|
+
let target;
|
|
93
|
+
if (view === "month") {
|
|
94
|
+
target = startOf(add(displayDate.value, 1, "month"), "month");
|
|
95
|
+
} else if (view === "week") {
|
|
96
|
+
target = asCalendarDate(add(displayDate.value, 1, "week"));
|
|
97
|
+
} else {
|
|
98
|
+
target = asCalendarDate(add(displayDate.value, 1, "day"));
|
|
99
|
+
}
|
|
100
|
+
emit("update:modelValue", target);
|
|
101
|
+
}
|
|
102
|
+
function goToToday() {
|
|
103
|
+
emit("update:modelValue", today());
|
|
104
|
+
}
|
|
105
|
+
function setView(v) {
|
|
106
|
+
emit("update:view", v);
|
|
107
|
+
}
|
|
108
|
+
const headerTitle = computed(() => {
|
|
109
|
+
if (view === "month") {
|
|
110
|
+
return format(displayDate.value, "MMMM YYYY", locale);
|
|
111
|
+
}
|
|
112
|
+
if (view === "week") {
|
|
113
|
+
const weekStart = startOf(displayDate.value, "week", weekLocale.value);
|
|
114
|
+
const weekEnd = asCalendarDate(add(weekStart, 6, "day"));
|
|
115
|
+
if (weekStart.month === weekEnd.month) {
|
|
116
|
+
return `${format(weekStart, "MMM D", locale)} \u2013 ${format(weekEnd, "D, YYYY", locale)}`;
|
|
117
|
+
}
|
|
118
|
+
return `${format(weekStart, "MMM D", locale)} \u2013 ${format(weekEnd, "MMM D, YYYY", locale)}`;
|
|
119
|
+
}
|
|
120
|
+
return format(displayDate.value, "dddd, MMMM D, YYYY", locale);
|
|
121
|
+
});
|
|
122
|
+
function parseDateInput(input) {
|
|
123
|
+
if (typeof input === "object" && "calendar" in input) {
|
|
124
|
+
return input;
|
|
125
|
+
}
|
|
126
|
+
if (input instanceof Date) {
|
|
127
|
+
const iso = input.toISOString();
|
|
128
|
+
if (iso.endsWith("T00:00:00.000Z")) {
|
|
129
|
+
return asCalendarDate(input);
|
|
130
|
+
}
|
|
131
|
+
return asCalendarDateTime(input);
|
|
132
|
+
}
|
|
133
|
+
if (typeof input === "string") {
|
|
134
|
+
if (input.includes("T") || input.includes(" ")) {
|
|
135
|
+
try {
|
|
136
|
+
return asCalendarDateTime(input);
|
|
137
|
+
} catch {
|
|
138
|
+
return asCalendarDate(input);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return asCalendarDate(input);
|
|
142
|
+
}
|
|
143
|
+
return today();
|
|
144
|
+
}
|
|
145
|
+
const normalizedEvents = computed(() => {
|
|
146
|
+
return events.map((event) => {
|
|
147
|
+
const start = parseDateInput(event.start);
|
|
148
|
+
const end = event.end ? parseDateInput(event.end) : start;
|
|
149
|
+
const allDay = event.allDay ?? !hasTimeComponent(start);
|
|
150
|
+
return {
|
|
151
|
+
id: event.id,
|
|
152
|
+
title: event.title,
|
|
153
|
+
start,
|
|
154
|
+
end,
|
|
155
|
+
color: event.color ?? "primary",
|
|
156
|
+
allDay,
|
|
157
|
+
draggable: event.draggable ?? true,
|
|
158
|
+
original: event
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const eventsByDate = computed(() => {
|
|
163
|
+
const map = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const event of normalizedEvents.value) {
|
|
165
|
+
const startDate = toCalendarDate(event.start);
|
|
166
|
+
const endDate = toCalendarDate(event.end);
|
|
167
|
+
let current = startDate;
|
|
168
|
+
while (current.compare(endDate) <= 0) {
|
|
169
|
+
const key = current.toString();
|
|
170
|
+
const existing = map.get(key) ?? [];
|
|
171
|
+
existing.push(event);
|
|
172
|
+
map.set(key, existing);
|
|
173
|
+
current = asCalendarDate(add(current, 1, "day"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
});
|
|
178
|
+
function getEventsForDate(date) {
|
|
179
|
+
return eventsByDate.value.get(date.toString()) ?? [];
|
|
180
|
+
}
|
|
181
|
+
const weekdayLabels = computed(() => {
|
|
182
|
+
const labels = [];
|
|
183
|
+
const refSunday = asCalendarDate("2025-01-05");
|
|
184
|
+
for (let i = 0; i < 7; i++) {
|
|
185
|
+
const day = asCalendarDate(add(refSunday, i + weekStartsOn, "day"));
|
|
186
|
+
labels.push(format(day, "ddd", locale));
|
|
187
|
+
}
|
|
188
|
+
return labels;
|
|
189
|
+
});
|
|
190
|
+
const monthWeeks = computed(() => {
|
|
191
|
+
const monthStart = startOf(displayDate.value, "month");
|
|
192
|
+
const gridStart = startOf(monthStart, "week", weekLocale.value);
|
|
193
|
+
const rowCount = monthConfig.value.fixedWeeks ? 6 : 5;
|
|
194
|
+
const result = [];
|
|
195
|
+
let current = gridStart;
|
|
196
|
+
for (let week = 0; week < rowCount; week++) {
|
|
197
|
+
const days = [];
|
|
198
|
+
for (let day = 0; day < 7; day++) {
|
|
199
|
+
const date = asCalendarDate(current);
|
|
200
|
+
days.push({
|
|
201
|
+
date,
|
|
202
|
+
isCurrentMonth: isSameMonth(date, monthStart),
|
|
203
|
+
isToday: checkIsToday(date, getLocalTimeZone()),
|
|
204
|
+
isWeekend: checkIsWeekend(date, weekLocale.value),
|
|
205
|
+
events: getEventsForDate(date)
|
|
206
|
+
});
|
|
207
|
+
current = asCalendarDate(add(current, 1, "day"));
|
|
208
|
+
}
|
|
209
|
+
result.push({ days });
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
});
|
|
213
|
+
const weekDays = computed(() => {
|
|
214
|
+
const weekStart = startOf(displayDate.value, "week", weekLocale.value);
|
|
215
|
+
const days = [];
|
|
216
|
+
for (let i = 0; i < 7; i++) {
|
|
217
|
+
const date = asCalendarDate(add(weekStart, i, "day"));
|
|
218
|
+
days.push({
|
|
219
|
+
date,
|
|
220
|
+
isCurrentMonth: true,
|
|
221
|
+
isToday: checkIsToday(date, getLocalTimeZone()),
|
|
222
|
+
isWeekend: checkIsWeekend(date, weekLocale.value),
|
|
223
|
+
events: getEventsForDate(date)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return days;
|
|
227
|
+
});
|
|
228
|
+
const dayViewDate = computed(() => {
|
|
229
|
+
const date = asCalendarDate(displayDate.value);
|
|
230
|
+
return {
|
|
231
|
+
date,
|
|
232
|
+
isCurrentMonth: true,
|
|
233
|
+
isToday: checkIsToday(date, getLocalTimeZone()),
|
|
234
|
+
isWeekend: checkIsWeekend(date, weekLocale.value),
|
|
235
|
+
events: getEventsForDate(date)
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
function generateTimeSlots(startHour, endHour, slotDuration) {
|
|
239
|
+
const slots = [];
|
|
240
|
+
const totalMinutes = (endHour - startHour) * 60;
|
|
241
|
+
for (let m = 0; m < totalMinutes; m += slotDuration) {
|
|
242
|
+
const hour = startHour + Math.floor(m / 60);
|
|
243
|
+
const minute = m % 60;
|
|
244
|
+
const h12 = hour % 12 || 12;
|
|
245
|
+
const ampm = hour >= 12 ? "pm" : "am";
|
|
246
|
+
const label = minute === 0 ? `${h12}${ampm}` : "";
|
|
247
|
+
slots.push({ hour, minute, label });
|
|
248
|
+
}
|
|
249
|
+
return slots;
|
|
250
|
+
}
|
|
251
|
+
const weekTimeSlots = computed(
|
|
252
|
+
() => generateTimeSlots(weekConfig.value.startHour, weekConfig.value.endHour, weekConfig.value.slotDuration)
|
|
253
|
+
);
|
|
254
|
+
const dayTimeSlots = computed(
|
|
255
|
+
() => generateTimeSlots(dayConfig.value.startHour, dayConfig.value.endHour, dayConfig.value.slotDuration)
|
|
256
|
+
);
|
|
257
|
+
const SLOT_HEIGHT = 48;
|
|
258
|
+
const timeGridDays = computed(() => {
|
|
259
|
+
if (view !== "week" && view !== "day") return [];
|
|
260
|
+
const days = view === "week" ? weekDays.value : [dayViewDate.value];
|
|
261
|
+
const config = view === "week" ? weekConfig.value : dayConfig.value;
|
|
262
|
+
return days.map((day) => ({
|
|
263
|
+
...day,
|
|
264
|
+
dateKey: day.date.toString(),
|
|
265
|
+
allDayEvents: day.events.filter((e) => e.allDay || !hasTimeComponent(e.start)),
|
|
266
|
+
timedEvents: layoutTimedEvents(getEventsForDate(day.date), config, SLOT_HEIGHT)
|
|
267
|
+
}));
|
|
268
|
+
});
|
|
269
|
+
const timeGridSlots = computed(() => {
|
|
270
|
+
if (view === "week") return weekTimeSlots.value;
|
|
271
|
+
if (view === "day") return dayTimeSlots.value;
|
|
272
|
+
return [];
|
|
273
|
+
});
|
|
274
|
+
function formatTime(d) {
|
|
275
|
+
if (!hasTimeComponent(d)) return "";
|
|
276
|
+
const dt = d;
|
|
277
|
+
const h12 = dt.hour % 12 || 12;
|
|
278
|
+
const ampm = dt.hour >= 12 ? "pm" : "am";
|
|
279
|
+
const min = dt.minute > 0 ? `:${String(dt.minute).padStart(2, "0")}` : "";
|
|
280
|
+
return `${h12}${min}${ampm}`;
|
|
281
|
+
}
|
|
282
|
+
function getEventStyle(event, bgOpacity = 10) {
|
|
283
|
+
const c = event.color;
|
|
284
|
+
const cssVar = c === "neutral" ? "var(--ui-bg-inverted)" : `var(--ui-${c})`;
|
|
285
|
+
return {
|
|
286
|
+
"--event-color": cssVar,
|
|
287
|
+
backgroundColor: `color-mix(in oklch, ${cssVar} ${bgOpacity}%, transparent)`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function isEventStart(event, date) {
|
|
291
|
+
const startDate = toCalendarDate(event.start);
|
|
292
|
+
return date.compare(startDate) === 0;
|
|
293
|
+
}
|
|
294
|
+
function handleDateClick(day) {
|
|
295
|
+
emit("dateClick", day.date);
|
|
296
|
+
}
|
|
297
|
+
function handleEventClick(event, e) {
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
emit("eventClick", event.original);
|
|
300
|
+
}
|
|
301
|
+
const expandedDay = ref(null);
|
|
302
|
+
function openExpandDay(dateKey, e) {
|
|
303
|
+
e.stopPropagation();
|
|
304
|
+
expandedDay.value = dateKey;
|
|
305
|
+
}
|
|
306
|
+
function closeExpandDay() {
|
|
307
|
+
expandedDay.value = null;
|
|
308
|
+
}
|
|
309
|
+
function isExpanded(dateKey) {
|
|
310
|
+
return expandedDay.value === dateKey;
|
|
311
|
+
}
|
|
312
|
+
onMounted(() => {
|
|
313
|
+
document.addEventListener("click", closeExpandDay);
|
|
314
|
+
});
|
|
315
|
+
onBeforeUnmount(() => {
|
|
316
|
+
document.removeEventListener("click", closeExpandDay);
|
|
317
|
+
});
|
|
318
|
+
const {
|
|
319
|
+
draggedEventId,
|
|
320
|
+
dropTargetKey,
|
|
321
|
+
dragSnapSlot,
|
|
322
|
+
onDragStart,
|
|
323
|
+
onDragEnd,
|
|
324
|
+
onDragOver,
|
|
325
|
+
onDragLeave,
|
|
326
|
+
onTimeGridDragOver,
|
|
327
|
+
onSlotDragOver,
|
|
328
|
+
onDropMonthCell,
|
|
329
|
+
onDropTimeGrid
|
|
330
|
+
} = useEventCalendarDragDrop({
|
|
331
|
+
editable: () => editable,
|
|
332
|
+
normalizedEvents,
|
|
333
|
+
onEventDrop: (payload) => emit("eventDrop", payload)
|
|
334
|
+
});
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<template>
|
|
338
|
+
<div data-slot="root" :class="ui.root({ class: propUi?.root })">
|
|
339
|
+
<!-- Header -->
|
|
340
|
+
<slot
|
|
341
|
+
name="header"
|
|
342
|
+
:title="headerTitle"
|
|
343
|
+
:prev="goToPrev"
|
|
344
|
+
:next="goToNext"
|
|
345
|
+
:today="goToToday"
|
|
346
|
+
:current-date="displayDate"
|
|
347
|
+
:view="view"
|
|
348
|
+
:set-view="setView">
|
|
349
|
+
<div data-slot="header" :class="ui.header({ class: propUi?.header })">
|
|
350
|
+
<span data-slot="headerTitle" :class="ui.headerTitle({ class: propUi?.headerTitle })">
|
|
351
|
+
{{ headerTitle }}
|
|
352
|
+
</span>
|
|
353
|
+
|
|
354
|
+
<div data-slot="headerActions" :class="ui.headerActions({ class: propUi?.headerActions })">
|
|
355
|
+
<!-- View switcher -->
|
|
356
|
+
<div data-slot="viewSwitcher" :class="ui.viewSwitcher({ class: propUi?.viewSwitcher })">
|
|
357
|
+
<UButton
|
|
358
|
+
v-for="v in ['month', 'week', 'day']"
|
|
359
|
+
:key="v"
|
|
360
|
+
:label="v"
|
|
361
|
+
size="xs"
|
|
362
|
+
:color="view === v ? color : 'neutral'"
|
|
363
|
+
:variant="view === v ? 'subtle' : 'ghost'"
|
|
364
|
+
class="capitalize"
|
|
365
|
+
@click="setView(v)" />
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<UButton icon="i-lucide-chevron-left" color="neutral" variant="ghost" size="xs" square @click="goToPrev" />
|
|
369
|
+
<UButton label="Today" color="neutral" variant="ghost" size="xs" @click="goToToday" />
|
|
370
|
+
<UButton icon="i-lucide-chevron-right" color="neutral" variant="ghost" size="xs" square @click="goToNext" />
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</slot>
|
|
374
|
+
|
|
375
|
+
<!-- ── Month View ──────────────────────────────────────────────── -->
|
|
376
|
+
<template v-if="view === 'month'">
|
|
377
|
+
<!-- Weekday headers -->
|
|
378
|
+
<div data-slot="weekdayRow" :class="ui.weekdayRow({ class: propUi?.weekdayRow })">
|
|
379
|
+
<div
|
|
380
|
+
v-for="(day, index) in weekdayLabels"
|
|
381
|
+
:key="day"
|
|
382
|
+
data-slot="weekdayCell"
|
|
383
|
+
:class="ui.weekdayCell({ class: propUi?.weekdayCell })">
|
|
384
|
+
<slot name="day-header" :day="day" :index="index">
|
|
385
|
+
{{ day }}
|
|
386
|
+
</slot>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<!-- Month grid -->
|
|
391
|
+
<div data-slot="monthBody" :class="ui.monthBody({ class: propUi?.monthBody })">
|
|
392
|
+
<template v-for="week in monthWeeks" :key="week.days[0].date.toString()">
|
|
393
|
+
<div
|
|
394
|
+
v-for="day in week.days"
|
|
395
|
+
:key="day.date.toString()"
|
|
396
|
+
data-slot="dayCell"
|
|
397
|
+
:class="[
|
|
398
|
+
ui.dayCell({ class: propUi?.dayCell }),
|
|
399
|
+
!day.isCurrentMonth && ui.dayCellOutside({ class: propUi?.dayCellOutside }),
|
|
400
|
+
dropTargetKey === `month-${day.date.toString()}` && ui.dropTarget({ class: propUi?.dropTarget }),
|
|
401
|
+
isExpanded(day.date.toString()) && 'relative z-10'
|
|
402
|
+
]"
|
|
403
|
+
@click="handleDateClick(day)"
|
|
404
|
+
@dragover="onDragOver(`month-${day.date.toString()}`, $event)"
|
|
405
|
+
@dragleave="onDragLeave"
|
|
406
|
+
@drop="onDropMonthCell(day.date, $event)">
|
|
407
|
+
<slot name="day" :day="day">
|
|
408
|
+
<!-- Day number -->
|
|
409
|
+
<div
|
|
410
|
+
data-slot="dayNumber"
|
|
411
|
+
:class="[
|
|
412
|
+
!day.isToday && ui.dayNumber({ class: propUi?.dayNumber }),
|
|
413
|
+
day.isToday && ui.dayNumberToday({ class: propUi?.dayNumberToday }),
|
|
414
|
+
!day.isCurrentMonth && !day.isToday && ui.dayNumberOutside({ class: propUi?.dayNumberOutside })
|
|
415
|
+
]">
|
|
416
|
+
{{ day.date.day }}
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<!-- Event chips -->
|
|
420
|
+
<div data-slot="eventList" :class="ui.eventList({ class: propUi?.eventList })">
|
|
421
|
+
<template v-for="(event, idx) in day.events" :key="event.id">
|
|
422
|
+
<div
|
|
423
|
+
v-if="idx < monthConfig.maxEvents"
|
|
424
|
+
data-slot="eventChip"
|
|
425
|
+
:draggable="editable && event.draggable && isEventStart(event, day.date)"
|
|
426
|
+
:class="[ui.eventChip({ class: propUi?.eventChip }), !isEventStart(event, day.date) && 'border-l-0!']"
|
|
427
|
+
:style="getEventStyle(event)"
|
|
428
|
+
@click="handleEventClick(event, $event)"
|
|
429
|
+
@dragstart="onDragStart(event, $event)"
|
|
430
|
+
@dragend="onDragEnd">
|
|
431
|
+
<slot name="event" :event="event.original" :view="view">
|
|
432
|
+
<template v-if="isEventStart(event, day.date)">
|
|
433
|
+
<span v-if="!event.allDay" data-slot="eventTime" :class="ui.eventTime({ class: propUi?.eventTime })">{{
|
|
434
|
+
formatTime(event.start)
|
|
435
|
+
}}</span>
|
|
436
|
+
{{ event.title }}
|
|
437
|
+
</template>
|
|
438
|
+
<span v-else class="opacity-0">{{ event.title }}</span>
|
|
439
|
+
</slot>
|
|
440
|
+
</div>
|
|
441
|
+
</template>
|
|
442
|
+
|
|
443
|
+
<!-- "+N more" trigger -->
|
|
444
|
+
<div
|
|
445
|
+
v-if="day.events.length > monthConfig.maxEvents"
|
|
446
|
+
data-slot="moreEvents"
|
|
447
|
+
:class="ui.moreEvents({ class: propUi?.moreEvents })"
|
|
448
|
+
@click="openExpandDay(day.date.toString(), $event)">
|
|
449
|
+
<slot
|
|
450
|
+
name="more-events"
|
|
451
|
+
:events="day.events.slice(monthConfig.maxEvents).map((e) => e.original)"
|
|
452
|
+
:count="day.events.length - monthConfig.maxEvents"
|
|
453
|
+
:day="day">
|
|
454
|
+
+{{ day.events.length - monthConfig.maxEvents }} more
|
|
455
|
+
</slot>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<!-- Expanded overlay with ALL events -->
|
|
460
|
+
<div
|
|
461
|
+
v-if="isExpanded(day.date.toString()) && day.events.length > monthConfig.maxEvents"
|
|
462
|
+
data-slot="expandedOverlay"
|
|
463
|
+
:class="ui.expandedOverlay({ class: propUi?.expandedOverlay })"
|
|
464
|
+
@click.stop>
|
|
465
|
+
<div
|
|
466
|
+
data-slot="dayNumber"
|
|
467
|
+
:class="[
|
|
468
|
+
!day.isToday && ui.dayNumber({ class: propUi?.dayNumber }),
|
|
469
|
+
day.isToday && ui.dayNumberToday({ class: propUi?.dayNumberToday })
|
|
470
|
+
]">
|
|
471
|
+
{{ day.date.day }}
|
|
472
|
+
</div>
|
|
473
|
+
<div data-slot="eventList" :class="ui.eventList({ class: propUi?.eventList })">
|
|
474
|
+
<div
|
|
475
|
+
v-for="event in day.events"
|
|
476
|
+
:key="`expanded-${event.id}`"
|
|
477
|
+
data-slot="eventChip"
|
|
478
|
+
:draggable="editable && event.draggable"
|
|
479
|
+
:class="ui.eventChip({ class: propUi?.eventChip })"
|
|
480
|
+
:style="getEventStyle(event)"
|
|
481
|
+
@click="handleEventClick(event, $event)"
|
|
482
|
+
@dragstart="onDragStart(event, $event)"
|
|
483
|
+
@dragend="onDragEnd">
|
|
484
|
+
<slot name="event" :event="event.original" :view="view">
|
|
485
|
+
<span v-if="!event.allDay" data-slot="eventTime" :class="ui.eventTime({ class: propUi?.eventTime })">{{
|
|
486
|
+
formatTime(event.start)
|
|
487
|
+
}}</span
|
|
488
|
+
>{{ event.title }}
|
|
489
|
+
</slot>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</slot>
|
|
494
|
+
</div>
|
|
495
|
+
</template>
|
|
496
|
+
</div>
|
|
497
|
+
</template>
|
|
498
|
+
|
|
499
|
+
<!-- ── Week / Day View (unified) ───────────────────────────────── -->
|
|
500
|
+
<template v-else-if="view === 'week' || view === 'day'">
|
|
501
|
+
<!-- Column headers -->
|
|
502
|
+
<div data-slot="columnHeaders" :class="ui.columnHeaders({ class: propUi?.columnHeaders })">
|
|
503
|
+
<div data-slot="timeGutterSpacer" :class="ui.timeGutterSpacer({ class: propUi?.timeGutterSpacer })" />
|
|
504
|
+
<div
|
|
505
|
+
v-for="day in timeGridDays"
|
|
506
|
+
:key="day.dateKey"
|
|
507
|
+
data-slot="dayColumnHeader"
|
|
508
|
+
:class="ui.dayColumnHeader({ class: propUi?.dayColumnHeader })">
|
|
509
|
+
<div :class="ui.dayColumnHeaderLabel({ class: propUi?.dayColumnHeaderLabel })">
|
|
510
|
+
{{ format(day.date, timeGridDays.length === 1 ? "dddd" : "ddd", locale) }}
|
|
511
|
+
</div>
|
|
512
|
+
<div
|
|
513
|
+
:class="[
|
|
514
|
+
day.isToday ? ui.dayColumnHeaderToday({ class: propUi?.dayColumnHeaderToday }) : ui.dayColumnHeaderNumber({ class: propUi?.dayColumnHeaderNumber })
|
|
515
|
+
]">
|
|
516
|
+
{{ day.date.day }}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<!-- All-day events row -->
|
|
522
|
+
<div data-slot="allDayRow" :class="ui.allDayRow({ class: propUi?.allDayRow })">
|
|
523
|
+
<div data-slot="allDayLabel" :class="ui.allDayLabel({ class: propUi?.allDayLabel })">all-day</div>
|
|
524
|
+
<div
|
|
525
|
+
v-for="day in timeGridDays"
|
|
526
|
+
:key="`allday-${day.dateKey}`"
|
|
527
|
+
data-slot="allDayCell"
|
|
528
|
+
:class="[
|
|
529
|
+
ui.allDayCell({ class: propUi?.allDayCell }),
|
|
530
|
+
dropTargetKey === `allday-${day.dateKey}` && ui.dropTarget({ class: propUi?.dropTarget })
|
|
531
|
+
]"
|
|
532
|
+
@dragover="onDragOver(`allday-${day.dateKey}`, $event)"
|
|
533
|
+
@dragleave="onDragLeave"
|
|
534
|
+
@drop="onDropMonthCell(day.date, $event)">
|
|
535
|
+
<!-- Day view exposes the all-day slot; week view renders chips directly -->
|
|
536
|
+
<template v-if="timeGridDays.length === 1">
|
|
537
|
+
<slot name="all-day" :events="day.allDayEvents.map((e) => e.original)" :date="day.date">
|
|
538
|
+
<div
|
|
539
|
+
v-for="event in day.allDayEvents"
|
|
540
|
+
:key="event.id"
|
|
541
|
+
:draggable="editable && event.draggable"
|
|
542
|
+
:class="[ui.eventChip({ class: propUi?.eventChip }), 'w-full']"
|
|
543
|
+
:style="getEventStyle(event)"
|
|
544
|
+
@click="handleEventClick(event, $event)"
|
|
545
|
+
@dragstart="onDragStart(event, $event)"
|
|
546
|
+
@dragend="onDragEnd">
|
|
547
|
+
<slot name="event" :event="event.original" :view="view">
|
|
548
|
+
{{ event.title }}
|
|
549
|
+
</slot>
|
|
550
|
+
</div>
|
|
551
|
+
</slot>
|
|
552
|
+
</template>
|
|
553
|
+
<template v-else>
|
|
554
|
+
<div
|
|
555
|
+
v-for="event in day.allDayEvents"
|
|
556
|
+
:key="event.id"
|
|
557
|
+
:draggable="editable && event.draggable"
|
|
558
|
+
:class="[ui.eventChip({ class: propUi?.eventChip }), 'w-full']"
|
|
559
|
+
:style="getEventStyle(event)"
|
|
560
|
+
@click="handleEventClick(event, $event)"
|
|
561
|
+
@dragstart="onDragStart(event, $event)"
|
|
562
|
+
@dragend="onDragEnd">
|
|
563
|
+
<slot name="event" :event="event.original" :view="view">
|
|
564
|
+
{{ event.title }}
|
|
565
|
+
</slot>
|
|
566
|
+
</div>
|
|
567
|
+
</template>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- Time grid -->
|
|
572
|
+
<div data-slot="timeGrid" :class="ui.timeGrid({ class: propUi?.timeGrid })">
|
|
573
|
+
<!-- Time gutter -->
|
|
574
|
+
<div data-slot="timeGutter" :class="ui.timeGutter({ class: propUi?.timeGutter })">
|
|
575
|
+
<div
|
|
576
|
+
v-for="slot in timeGridSlots"
|
|
577
|
+
:key="`gutter-${slot.hour}-${slot.minute}`"
|
|
578
|
+
data-slot="timeGutterSlot"
|
|
579
|
+
:class="ui.timeGutterSlot({ class: propUi?.timeGutterSlot })"
|
|
580
|
+
:style="{ height: `${SLOT_HEIGHT}px` }">
|
|
581
|
+
<div v-if="slot.label" data-slot="timeLabel" :class="ui.timeLabel({ class: propUi?.timeLabel })">
|
|
582
|
+
<slot name="time-label" :hour="slot.hour" :label="slot.label">
|
|
583
|
+
{{ slot.label }}
|
|
584
|
+
</slot>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
<!-- Day columns -->
|
|
590
|
+
<div
|
|
591
|
+
v-for="day in timeGridDays"
|
|
592
|
+
:key="`col-${day.dateKey}`"
|
|
593
|
+
data-slot="dayColumn"
|
|
594
|
+
:class="ui.dayColumn({ class: propUi?.dayColumn })"
|
|
595
|
+
@dragover="onTimeGridDragOver(`time-${day.dateKey}`, $event)"
|
|
596
|
+
@dragleave="onDragLeave"
|
|
597
|
+
@drop="onDropTimeGrid(day.date, $event)">
|
|
598
|
+
<!-- Time slot rows (grid lines + drag targets) -->
|
|
599
|
+
<div
|
|
600
|
+
v-for="slot in timeGridSlots"
|
|
601
|
+
:key="`row-${slot.hour}-${slot.minute}`"
|
|
602
|
+
data-slot="timeSlotRow"
|
|
603
|
+
:class="[
|
|
604
|
+
ui.timeSlotRow({ class: propUi?.timeSlotRow }),
|
|
605
|
+
dragSnapSlot === `time-${day.dateKey}-${slot.hour}-${slot.minute}` && ui.dragSnapSlot({ class: propUi?.dragSnapSlot })
|
|
606
|
+
]"
|
|
607
|
+
:style="{ height: `${SLOT_HEIGHT}px` }"
|
|
608
|
+
@click="handleDateClick(day)"
|
|
609
|
+
@dragover="onSlotDragOver(`time-${day.dateKey}-${slot.hour}-${slot.minute}`, $event)" />
|
|
610
|
+
|
|
611
|
+
<!-- Timed events (absolutely positioned) -->
|
|
612
|
+
<div
|
|
613
|
+
v-for="event in day.timedEvents"
|
|
614
|
+
:key="`ev-${event.id}`"
|
|
615
|
+
data-slot="timeEvent"
|
|
616
|
+
:draggable="editable && event.draggable"
|
|
617
|
+
:class="[
|
|
618
|
+
ui.timeEvent({ class: propUi?.timeEvent }),
|
|
619
|
+
draggedEventId != null && draggedEventId !== event.id && 'pointer-events-none'
|
|
620
|
+
]"
|
|
621
|
+
:style="{
|
|
622
|
+
...getEventStyle(event, 15),
|
|
623
|
+
top: `${event.topPx}px`,
|
|
624
|
+
height: `${event.heightPx}px`,
|
|
625
|
+
minHeight: '20px',
|
|
626
|
+
left: `calc(${event.column / event.totalColumns * 100}% + 2px)`,
|
|
627
|
+
width: `calc(${1 / event.totalColumns * 100}% - 4px)`
|
|
628
|
+
}"
|
|
629
|
+
@click="handleEventClick(event, $event)"
|
|
630
|
+
@dragstart="onDragStart(event, $event)"
|
|
631
|
+
@dragend="onDragEnd">
|
|
632
|
+
<slot name="event" :event="event.original" :view="view">
|
|
633
|
+
<div data-slot="timeEventTitle" :class="ui.timeEventTitle({ class: propUi?.timeEventTitle })">
|
|
634
|
+
{{ event.title }}
|
|
635
|
+
</div>
|
|
636
|
+
<div data-slot="timeEventTime" :class="ui.timeEventTime({ class: propUi?.timeEventTime })">
|
|
637
|
+
{{ formatTime(event.start) }}
|
|
638
|
+
<template v-if="timeGridDays.length === 1"> – {{ formatTime(event.end) }}</template>
|
|
639
|
+
</div>
|
|
640
|
+
</slot>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</template>
|
|
645
|
+
</div>
|
|
646
|
+
</template>
|