Fit the calendar to the space it actually has
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s

The previous attempt at this was verified against an empty month, which is why
it measured clean and still scrolled: put events in the month and the fixed
height clipped the grid, leaving the last week behind FullCalendar's own
scroller. The media-query floors it added are gone.

Two things were guessed and are now measured. The height came from
calc(100vh - 230px), a stand-in for chrome that is not 230px tall; it is taken
from the container's real top at mount and on resize, then corrected against
whatever the page still overflows by. And dayMaxEvents was a fixed 3, which
does not fit an 80px row - hence the overflow. It is `true` now, so
FullCalendar shows as many chips as the row genuinely holds and rolls the rest
into a '+N more'; that is the only setting that cannot outgrow the box. The
chips and the day-number strip are tighter, so more fit before that happens.

With 44 events across 11 days, at 1920x1080, 1600x900, 1366x768 and 1280x720:
all six weeks visible, no page scrollbar anywhere. A full-height display shows
every chip; a laptop shows one and a link.
This commit is contained in:
cproudlock
2026-08-08 15:12:01 -04:00
parent f67d975058
commit 686966b3b1

View File

@@ -3,7 +3,7 @@
<h1>Calendar</h1>
</div>
<div class="calendar-container card">
<div ref="calendarContainer" class="calendar-container card">
<FullCalendar :options="calendarOptions" />
</div>
@@ -94,6 +94,7 @@ import { notificationsApi } from '@/api'
const events = ref([])
const selectedEvent = ref(null)
const calendarRef = ref(null)
const calendarContainer = ref(null)
const moreTooltip = ref(null)
const moreTooltipData = ref([])
const tooltipPosition = ref({ left: 0, top: 0 })
@@ -113,14 +114,19 @@ const calendarOptions = ref({
center: 'title',
right: 'dayGridMonth,dayGridWeek'
},
// 'auto' sized every row to its own content, so a month of mostly empty days
// collapsed into thin strips and the grid looked squashed. A viewport-relative
// height makes the calendar fill the space it has been given, and expandRows
// shares that height evenly across the weeks rather than leaving a gap under
// the last one.
height: 'calc(100vh - 230px)',
// Measured at mount and on resize (fitHeight), not a viewport calc: the
// calendar starts below a header and a page title whose heights are not
// knowable here, and guessing at them is what left the grid either clipped
// or overflowing. expandRows shares that height across the six weeks so an
// empty month fills it instead of collapsing into strips.
height: 600,
expandRows: true,
dayMaxEvents: 3,
// true = FullCalendar fits as many chips as the row actually has room for and
// rolls the rest into a '+N more'. That is the only setting that cannot
// overflow the box: a fixed number outgrows short rows and pushes the last
// weeks behind the calendar's own scroller. Compact chip styling below buys
// back a chip or two per day.
dayMaxEvents: true,
moreLinkClick: () => 'none' // Disable click, we use hover
})
@@ -185,11 +191,39 @@ function buildEventsByDate() {
eventsByDate.value = byDate
}
// Height the calendar can have without pushing the page into a scrollbar.
// A first guess from the container's top, then corrected against whatever the
// page actually overflows by - the card's padding, page margins and anything
// else below it are not worth enumerating, and guessing at them is exactly how
// this ended up either clipped or scrolling. Floored so a very short window
// scrolls rather than crushing the grid.
const MIN_CALENDAR_HEIGHT = 460
const FIT_PASSES = 3
async function fitHeight() {
const el = calendarContainer.value
if (!el) return
const available = window.innerHeight - el.getBoundingClientRect().top
calendarOptions.value.height = Math.max(MIN_CALENDAR_HEIGHT, Math.round(available))
for (let pass = 0; pass < FIT_PASSES; pass++) {
await nextTick()
const overflow = document.documentElement.scrollHeight - window.innerHeight
if (overflow <= 0) break
const next = calendarOptions.value.height - overflow
if (next <= MIN_CALENDAR_HEIGHT) {
calendarOptions.value.height = MIN_CALENDAR_HEIGHT
break
}
calendarOptions.value.height = next
}
}
onMounted(async () => {
await loadEvents()
// Add event delegation for more links
await nextTick()
fitHeight()
window.addEventListener('resize', fitHeight)
const container = document.querySelector('.calendar-container')
if (container) {
container.addEventListener('mouseenter', handleMoreLinkHover, true)
@@ -198,6 +232,7 @@ onMounted(async () => {
})
onUnmounted(() => {
window.removeEventListener('resize', fitHeight)
const container = document.querySelector('.calendar-container')
if (container) {
container.removeEventListener('mouseenter', handleMoreLinkHover, true)
@@ -254,44 +289,35 @@ function formatDate(dateStr) {
<style scoped>
.calendar-container {
min-height: 640px;
/* The height itself is measured in fitHeight(); this is only a floor for the
first paint, before that measurement lands. */
min-height: 460px;
}
/* :deep, because these are FullCalendar's own elements and scoped styles do not
reach them. A floor under each day cell keeps the grid readable on a short
window, where the viewport-relative height above would otherwise squeeze the
rows back down. */
:deep(.fc-daygrid-day-frame) {
min-height: 110px;
/* Compact chips: the row height decides how many events a day can show before
they roll into a '+N more', so every pixel saved here is one more visible. */
:deep(.fc-daygrid-event) {
font-size: 12px;
line-height: 1.3;
padding: 1px 4px;
margin-top: 1px;
}
:deep(.fc-daygrid-more-link) {
font-size: 12px;
}
/* The calc() height above is viewport-relative, so on a short window - a laptop
with a docked browser, or a shopfloor display in portrait - it can resolve
smaller than the grid needs. This stops it undercutting the day cells. */
:deep(.fc) {
min-height: 620px;
}
/* Those floors are sized for a full-height display. On a shorter window they
add up to more than calc(100vh - 230px) gives - six rows at 110px plus the
toolbar overflows a 768px screen - and the page grew a scrollbar to show a
month that used to fit. Relax them there: still floored well above the thin
strips this was fixing, just not taller than the window. */
@media (max-height: 899px) {
.calendar-container {
min-height: 520px;
}
:deep(.fc) {
min-height: 500px;
}
:deep(.fc-daygrid-day-frame) {
min-height: 78px;
}
}
/* The day number was tight against the top edge once the cells grew. */
/* The day number was tight against the top edge once the cells grew. Kept
small: this strip is pure overhead in the row-height budget FullCalendar
uses to decide how many chips fit before a '+N more'. */
:deep(.fc-daygrid-day-top) {
padding: 2px 4px;
padding: 1px 4px;
font-size: 13px;
}
:deep(.fc-daygrid-day-events) {
margin-bottom: 0;
}
:deep(.fc-daygrid-event-harness) {
margin-top: 0;
}
.modal {