From bfdb320b3e6e0914763469cb6bc7e6727479a39b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 17:37:35 -0700 Subject: [PATCH] fix(rcal): prevent multi-day event spans from displacing day cells Multi-day event span bars used grid-row/grid-column inside the same CSS grid as auto-placed day cells. The grid auto-placement algorithm skipped cells occupied by explicitly-placed spans, pushing day numbers to wrong positions. Fix: make .ev-span position:absolute with .grid position:relative. Absolutely-positioned grid children still use grid-row/column for their containing block but don't participate in layout flow. Also account for expanded day-detail rows when calculating span grid rows. Co-Authored-By: Claude Opus 4.6 --- modules/rcal/components/folk-calendar-view.ts | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 106d66c..f007d39 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -1201,7 +1201,8 @@ class FolkCalendarView extends HTMLElement { html += this.renderDayDetail(this.expandedDay, dayEvents); } - // Multi-day event span bars + // Multi-day event span bars (position:absolute so they don't + // displace auto-placed day cells in the CSS grid) html += this.renderMultiDaySpans(year, month, firstDay, daysInMonth); return html; @@ -1224,6 +1225,24 @@ class FolkCalendarView extends HTMLElement { const totalRows = totalCells / 7; let html = ""; + // If a day detail is expanded, figure out which grid row it inserted + // so we can offset span bars below it by +1 + let detailAfterGridRow = -1; + if (this.expandedDay) { + const ed = new Date(this.expandedDay + "T00:00:00"); + if (ed.getFullYear() === year && ed.getMonth() === month) { + const cellIdx = firstDay + ed.getDate() - 1; + const posInRow = cellIdx % 7; + // Detail is emitted when posInRow===6 or last day of month + if (posInRow === 6 || ed.getDate() === daysInMonth) { + detailAfterGridRow = Math.floor(cellIdx / 7) + 1; + } else { + // Detail is emitted after all days via the outer render + detailAfterGridRow = totalRows + 1; + } + } + } + // Helper: date string → cell index (clamped to grid) const dateToCellIdx = (ds: string): number => { const d = new Date(ds); @@ -1260,7 +1279,10 @@ class FolkCalendarView extends HTMLElement { if (!placed) overflow++; } - const gridRow = row + 1; + // Offset grid row if a detail panel was inserted above this row + let gridRow = row + 1; + if (detailAfterGridRow > 0 && gridRow > detailAfterGridRow) gridRow++; + for (let l = 0; l < lanes.length; l++) { for (const re of lanes[l]) { const color = re.ev.source_color || "#6366f1"; @@ -1270,11 +1292,11 @@ class FolkCalendarView extends HTMLElement { const contR = realEnd > rowEnd; const rL = contL ? "0" : "4px"; const rR = contR ? "0" : "4px"; - html += `
${this.esc(re.ev.title)}
`; + html += `
${this.esc(re.ev.title)}
`; } } if (overflow > 0) { - html += `
+${overflow} more
`; + html += `
+${overflow} more
`; } } return html; @@ -2749,7 +2771,7 @@ class FolkCalendarView extends HTMLElement { /* ── Month Grid ── */ .weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 4px; } .wd { text-align: center; font-size: 11px; color: var(--rs-text-muted); padding: 4px; font-weight: 600; } - .grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; } + .grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; position: relative; } .day { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 6px; min-height: 80px; padding: 6px; cursor: pointer; position: relative; -webkit-tap-highlight-color: transparent; } .day:hover { border-color: var(--rs-border-strong); } .day.today { border-color: var(--rs-primary-hover); background: var(--rs-bg-active); } @@ -2770,10 +2792,10 @@ class FolkCalendarView extends HTMLElement { .dot--tentative { border: 1px dashed; background: transparent !important; width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin: 1px; } /* ── Multi-day Span Bars ── */ - .ev-span { height: 16px; font-size: 10px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 4px; cursor: pointer; z-index: 2; align-self: start; pointer-events: auto; } + .ev-span { position: absolute; height: 16px; font-size: 10px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 4px; cursor: pointer; z-index: 2; pointer-events: auto; } .ev-span:hover { filter: brightness(1.15); } .ev-span-text { font-weight: 500; } - .ev-span-more { height: 14px; font-size: 9px; line-height: 14px; color: var(--rs-text-muted); z-index: 2; align-self: start; pointer-events: none; padding: 0 4px; } + .ev-span-more { position: absolute; height: 14px; font-size: 9px; line-height: 14px; color: var(--rs-text-muted); z-index: 2; pointer-events: none; padding: 0 4px; } /* ── Drop Target ── */ .day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed var(--rs-warning); }