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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 17:37:35 -07:00
parent 245d2ec3b4
commit bfdb320b3e
1 changed files with 29 additions and 7 deletions

View File

@ -1201,7 +1201,8 @@ class FolkCalendarView extends HTMLElement {
html += this.renderDayDetail(this.expandedDay, dayEvents); 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); html += this.renderMultiDaySpans(year, month, firstDay, daysInMonth);
return html; return html;
@ -1224,6 +1225,24 @@ class FolkCalendarView extends HTMLElement {
const totalRows = totalCells / 7; const totalRows = totalCells / 7;
let html = ""; 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) // Helper: date string → cell index (clamped to grid)
const dateToCellIdx = (ds: string): number => { const dateToCellIdx = (ds: string): number => {
const d = new Date(ds); const d = new Date(ds);
@ -1260,7 +1279,10 @@ class FolkCalendarView extends HTMLElement {
if (!placed) overflow++; 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 (let l = 0; l < lanes.length; l++) {
for (const re of lanes[l]) { for (const re of lanes[l]) {
const color = re.ev.source_color || "#6366f1"; const color = re.ev.source_color || "#6366f1";
@ -1270,11 +1292,11 @@ class FolkCalendarView extends HTMLElement {
const contR = realEnd > rowEnd; const contR = realEnd > rowEnd;
const rL = contL ? "0" : "4px"; const rL = contL ? "0" : "4px";
const rR = contR ? "0" : "4px"; const rR = contR ? "0" : "4px";
html += `<div class="ev-span" style="grid-row:${gridRow};grid-column:${re.c0 + 1}/${re.c1 + 2};margin-top:${22 + l * 18}px;background:${color}25;border-left:2px solid ${color};border-radius:${rL} ${rR} ${rR} ${rL}" data-event-id="${re.ev.id}" title="${this.esc(re.ev.title)}"><span class="ev-span-text" style="color:${color}">${this.esc(re.ev.title)}</span></div>`; html += `<div class="ev-span" style="grid-row:${gridRow};grid-column:${re.c0 + 1}/${re.c1 + 2};left:0;right:0;margin-top:${22 + l * 18}px;background:${color}25;border-left:2px solid ${color};border-radius:${rL} ${rR} ${rR} ${rL}" data-event-id="${re.ev.id}" title="${this.esc(re.ev.title)}"><span class="ev-span-text" style="color:${color}">${this.esc(re.ev.title)}</span></div>`;
} }
} }
if (overflow > 0) { if (overflow > 0) {
html += `<div class="ev-span-more" style="grid-row:${gridRow};grid-column:1;margin-top:${22 + MAX_LANES * 18}px">+${overflow} more</div>`; html += `<div class="ev-span-more" style="grid-row:${gridRow};grid-column:1;left:0;margin-top:${22 + MAX_LANES * 18}px">+${overflow} more</div>`;
} }
} }
return html; return html;
@ -2749,7 +2771,7 @@ class FolkCalendarView extends HTMLElement {
/* ── Month Grid ── */ /* ── Month Grid ── */
.weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 4px; } .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; } .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 { 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:hover { border-color: var(--rs-border-strong); }
.day.today { border-color: var(--rs-primary-hover); background: var(--rs-bg-active); } .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; } .dot--tentative { border: 1px dashed; background: transparent !important; width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin: 1px; }
/* ── Multi-day Span Bars ── */ /* ── 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:hover { filter: brightness(1.15); }
.ev-span-text { font-weight: 500; } .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 ── */ /* ── Drop Target ── */
.day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed var(--rs-warning); } .day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed var(--rs-warning); }