From 47c9a9027ba584ba77ee6f018bc8ce5fe95d726d Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 3 Jul 2026 19:37:47 -0400 Subject: [PATCH] fix: month view events list Signed-off-by: Sebastian Krupinski --- src/components/MonthView.vue | 177 +++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 38 deletions(-) diff --git a/src/components/MonthView.vue b/src/components/MonthView.vue index 77984f2..d190107 100644 --- a/src/components/MonthView.vue +++ b/src/components/MonthView.vue @@ -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(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(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(() => { const year = props.currentDate.getFullYear(); @@ -100,15 +112,15 @@ const weeks = computed(() => { 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(() => { 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(() => { + 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 { @@ -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; +}