fix: month view events list

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-03 19:37:47 -04:00
parent 37fa7b911e
commit 47c9a9027b
+139 -38
View File
@@ -6,11 +6,18 @@ import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
import type { CalendarInstance } from '@/types/instance';
const EVENT_HEIGHT = 22; // Height of each event row in pixels
const MAX_VISIBLE_EVENTS = 3; // Maximum event rows to show before "+N more"
const DATE_ROW_HEIGHT = 34; // Space reserved at the top of each cell for the date label
interface DayCell {
date: Date;
currentMonth: boolean;
visibleInstances: CalendarInstance[];
hiddenCount: number;
}
interface WeekData {
startDate: Date;
days: { date: Date; currentMonth: boolean }[];
days: DayCell[];
multiDaySegments: MultiDaySegment[];
laneCount: number;
}
@@ -49,23 +56,22 @@ const monthLabel = computed(() => {
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
});
// Track container size for dynamic calculations
const containerRef = ref<HTMLElement | null>(null);
const containerHeight = ref(600);
function updateContainerHeight() {
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight;
}
}
// Track the calendar body size so each week row shows as many event rows as fit
const bodyRef = ref<HTMLElement | null>(null);
const bodyHeight = ref(600);
let bodyResizeObserver: ResizeObserver | null = null;
onMounted(() => {
updateContainerHeight();
window.addEventListener('resize', updateContainerHeight);
bodyResizeObserver = new ResizeObserver(entries => {
bodyHeight.value = entries[0].contentRect.height;
});
if (bodyRef.value) {
bodyResizeObserver.observe(bodyRef.value);
}
});
onUnmounted(() => {
window.removeEventListener('resize', updateContainerHeight);
bodyResizeObserver?.disconnect();
});
// Calculate how many weeks are needed for this month
@@ -82,6 +88,12 @@ const weeksNeeded = computed(() => {
return Math.ceil((daysBeforeFirst + daysInMonth) / 7);
});
// Event rows that fit in one week row, given the available height
const maxEventRows = computed(() => {
const weekHeight = bodyHeight.value / weeksNeeded.value;
return Math.max(1, Math.floor((weekHeight - DATE_ROW_HEIGHT) / EVENT_HEIGHT));
});
// Build weeks with multiday event segments
const weeks = computed<WeekData[]>(() => {
const year = props.currentDate.getFullYear();
@@ -100,15 +112,15 @@ const weeks = computed<WeekData[]>(() => {
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const days: { date: Date; currentMonth: boolean }[] = [];
const baseDays: { date: Date; currentMonth: boolean }[] = [];
for (let d = 0; d < 7; d++) {
days.push({
baseDays.push({
date: new Date(current),
currentMonth: current.getMonth() === month,
});
current.setDate(current.getDate() + 1);
}
// Get multiday segments for this week
const instances = instancesStore.instancesForRange({
startMs: weekStart.getTime(),
@@ -125,15 +137,40 @@ const weeks = computed<WeekData[]>(() => {
7,
date => date.getDay(),
);
// When lanes overflow, drop the last one(s) so a "+N more" row fits below
const maxRows = maxEventRows.value;
const visibleLaneCount = laneCount > maxRows ? Math.max(0, maxRows - 1) : laneCount;
const visibleSegments = segments.filter(segment => segment.lane < visibleLaneCount);
const hiddenSegments = segments.filter(segment => segment.lane >= visibleLaneCount);
const availableRows = Math.max(0, maxRows - visibleLaneCount);
const days = baseDays.map((day, dayIndex) => {
const singleDayEvents = getSingleDayEvents(day.date);
const hiddenMultiDay = hiddenSegments.filter(
segment => dayIndex >= segment.startCol && dayIndex < segment.startCol + segment.span,
).length;
if (hiddenMultiDay === 0 && singleDayEvents.length <= availableRows) {
return { ...day, visibleInstances: singleDayEvents, hiddenCount: 0 };
}
const visibleInstances = singleDayEvents.slice(0, Math.max(0, availableRows - 1));
return {
...day,
visibleInstances,
hiddenCount: hiddenMultiDay + singleDayEvents.length - visibleInstances.length,
};
});
weeksData.push({
startDate: weekStart,
days,
multiDaySegments: segments,
laneCount: Math.min(laneCount, MAX_VISIBLE_EVENTS),
multiDaySegments: visibleSegments,
laneCount: visibleLaneCount,
});
}
return weeksData;
});
@@ -142,15 +179,36 @@ function getSingleDayEvents(date: Date): CalendarInstance[] {
return instancesStore.instancesForDay(date).filter(instance => !instance.multiDay);
}
// Count hidden events (multiday beyond MAX + single day beyond remaining space)
function getHiddenCount(cell: { date: Date }, weekLaneCount: number): number {
const singleDayEvents = getSingleDayEvents(cell.date);
const visibleSingleDay = Math.max(0, MAX_VISIBLE_EVENTS - weekLaneCount);
const hiddenSingleDay = Math.max(0, singleDayEvents.length - visibleSingleDay);
// Also count multiday events in lanes beyond MAX_VISIBLE_EVENTS for this day
// (This would need more complex tracking - simplified for now)
return hiddenSingleDay;
// "+N more" popover listing all of a day's events
const morePopover = ref<{ open: boolean; target: HTMLElement | null; date: Date | null }>({
open: false,
target: null,
date: null,
});
function showMorePopover(event: MouseEvent, date: Date) {
morePopover.value = {
open: true,
target: event.currentTarget as HTMLElement,
date,
};
}
const morePopoverInstances = computed<CalendarInstance[]>(() => {
return morePopover.value.date ? instancesStore.instancesForDay(morePopover.value.date) : [];
});
const morePopoverLabel = computed(() => {
return morePopover.value.date?.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
}) ?? '';
});
function onPopoverEventClick(instance: CalendarInstance) {
morePopover.value.open = false;
emit('event-click', instance.entity);
}
function isToday(date: Date): boolean {
@@ -166,7 +224,7 @@ function getEventColor(instance: CalendarInstance): string {
</script>
<template>
<div class="month-view" ref="containerRef">
<div class="month-view">
<!-- Navigation Controls -->
<div class="view-controls">
<div class="view-navigation">
@@ -185,7 +243,7 @@ function getEventColor(instance: CalendarInstance): string {
</div>
<!-- Week rows -->
<div class="calendar-body">
<div class="calendar-body" ref="bodyRef">
<div v-for="(week, weekIndex) in weeks" :key="weekIndex" class="calendar-week">
<!-- Base layer: Day cells grid -->
<div class="days-grid">
@@ -208,7 +266,7 @@ function getEventColor(instance: CalendarInstance): string {
<!-- Single-day events -->
<div class="cell-events">
<div
v-for="instance in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
v-for="instance in cell.visibleInstances"
:key="instance.key"
class="single-day-event"
:style="{ backgroundColor: getEventColor(instance) }"
@@ -218,12 +276,12 @@ function getEventColor(instance: CalendarInstance): string {
>
{{ instance.label || 'Untitled' }}
</div>
<div
v-if="getHiddenCount(cell, week.laneCount) > 0"
<div
v-if="cell.hiddenCount > 0"
class="more-events"
@click.stop="$emit('date-click', cell.date)"
@click.stop="showMorePopover($event, cell.date)"
>
+{{ getHiddenCount(cell, week.laneCount) }} more
+{{ cell.hiddenCount }} more
</div>
</div>
</div>
@@ -243,7 +301,7 @@ function getEventColor(instance: CalendarInstance): string {
backgroundColor: getEventColor(segment.instance),
left: `calc(${segment.startCol} / 7 * 100% + 4px)`,
width: `calc(${segment.span} / 7 * 100% - 8px)`,
top: `${34 + segment.lane * EVENT_HEIGHT}px`,
top: `${DATE_ROW_HEIGHT + segment.lane * EVENT_HEIGHT}px`,
}"
@click.stop="$emit('event-click', segment.instance.entity)"
@mouseenter="$emit('event-hover', { event: $event, entity: segment.instance.entity })"
@@ -256,6 +314,34 @@ function getEventColor(instance: CalendarInstance): string {
</div>
</div>
</div>
<!-- "+N more" popover: all events for the selected day -->
<v-menu
v-if="morePopover.target"
v-model="morePopover.open"
:activator="morePopover.target"
location="bottom"
:offset="4"
>
<v-card min-width="220" max-width="320">
<div class="more-popover-header">{{ morePopoverLabel }}</div>
<v-list density="compact">
<v-list-item
v-for="instance in morePopoverInstances"
:key="instance.key"
@click="onPopoverEventClick(instance)"
>
<template #prepend>
<span
class="more-popover-dot"
:style="{ backgroundColor: getEventColor(instance) }"
></span>
</template>
<v-list-item-title>{{ instance.label || 'Untitled' }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</div>
</template>
@@ -481,4 +567,19 @@ function getEventColor(instance: CalendarInstance): string {
overflow: hidden;
text-overflow: ellipsis;
}
/* "+N more" popover */
.more-popover-header {
padding: 8px 16px 0;
font-size: 13px;
font-weight: 600;
}
.more-popover-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
flex-shrink: 0;
}
</style>