fix(rcal): eliminate event overlap at non-standard zoom resolutions

Week view: replace calc()-based absolute positioning with a mirror grid
overlay (grid-template-columns: 44px repeat(7,1fr)) so event columns
align pixel-perfectly with the underlying CSS grid at any browser zoom.

Fix min-interval padding to match min display size in week view
(20→23 min for 18px height) and day-horizontal view (30→45 min for
60px width), preventing visual bleed between adjacent short events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-15 15:24:06 -04:00
parent 03b1bdf2f1
commit 9eca86f83b
1 changed files with 23 additions and 11 deletions

View File

@ -1689,11 +1689,12 @@ class FolkCalendarView extends HTMLElement {
}
// Compute row assignments for overlapping horizontal events
const MIN_DH_INTERVAL = Math.ceil(60 / HOUR_WIDTH * 60); // min display width (60px) → min interval
const dhIntervals = timed.map(ev => {
const s = new Date(ev.start_time), e = new Date(ev.end_time);
const startMin = s.getHours() * 60 + s.getMinutes();
const endMin = e.getHours() * 60 + e.getMinutes();
return { startMin, endMin: Math.max(endMin, startMin + 30) };
return { startMin, endMin: Math.max(endMin, startMin + MIN_DH_INTERVAL) };
});
const dhLayout = this.computeEventColumns(dhIntervals);
const DH_ROW_HEIGHT = 56; // min-height 48 + 8px gap
@ -2152,7 +2153,9 @@ class FolkCalendarView extends HTMLElement {
}
}
let eventsOverlay = "";
// Build per-day event overlays (grid-aligned to avoid calc() rounding vs 1fr drift)
const MIN_INTERVAL_WEEK = Math.ceil(18 / HOUR_HEIGHT * 60); // min display height → min interval
const dayOverlays: string[] = [];
for (let i = 0; i < 7; i++) {
const ds = this.dateStr(days[i]);
const dayTimed = this.getEventsForDate(ds).filter(ev => {
@ -2160,15 +2163,15 @@ class FolkCalendarView extends HTMLElement {
return (en.getTime() - s.getTime()) < 86400000;
}).sort((a, b) => a.start_time.localeCompare(b.start_time));
// Compute sub-columns for overlapping events within this day
const intervals = dayTimed.map(ev => {
const s = new Date(ev.start_time), e = new Date(ev.end_time);
const startMin = s.getHours() * 60 + s.getMinutes();
const endMin = e.getHours() * 60 + e.getMinutes();
return { startMin, endMin: Math.max(endMin, startMin + 20) };
return { startMin, endMin: Math.max(endMin, startMin + MIN_INTERVAL_WEEK) };
});
const dayLayout = this.computeEventColumns(intervals);
let dayHtml = "";
for (let j = 0; j < dayTimed.length; j++) {
const ev = dayTimed[j];
const { startMin, endMin } = intervals[j];
@ -2177,21 +2180,28 @@ class FolkCalendarView extends HTMLElement {
const topPx = ((startMin - START_HOUR * 60) / 60) * HOUR_HEIGHT;
const heightPx = Math.max((duration / 60) * HOUR_HEIGHT, 18);
const es = this.getEventStyles(ev);
const colLeft = `calc(44px + ${i} * ((100% - 44px) / 7) + 2px + ${col} * ((100% - 44px) / 7 - 4px) / ${totalCols})`;
const colWidth = `calc(((100% - 44px) / 7 - 4px) / ${totalCols} - 1px)`;
const evLeft = totalCols > 1 ? `calc(${(col / totalCols * 100).toFixed(2)}% + 1px)` : "1px";
const evWidth = totalCols > 1 ? `calc(${(100 / totalCols).toFixed(2)}% - 2px)` : "calc(100% - 2px)";
const showMeta = heightPx >= 36;
const timeStr = `${this.formatTime(ev.start_time)}\u2013${this.formatTime(ev.end_time)}`;
const locName = ev.location_name ? this.esc(ev.location_name) : "";
const virtualBadge = ev.is_virtual ? `<span class="wk-virtual" title="Virtual">\u{1F4BB}</span>` : "";
eventsOverlay += `<div class="week-event" data-event-id="${ev.id}" style="
top:${topPx}px;height:${heightPx}px;left:${colLeft};width:${colWidth};
dayHtml += `<div class="week-event" data-event-id="${ev.id}" style="
top:${topPx}px;height:${heightPx}px;left:${evLeft};width:${evWidth};
background:${es.bgColor};border-left-color:${ev.source_color || "#6366f1"};border-left-style:${es.borderStyle};opacity:${es.opacity}">
<div class="week-event-title">${virtualBadge}${this.esc(ev.title)}</div>
${showMeta ? `<div class="week-event-meta"><span class="week-event-time">${timeStr}</span>${locName ? `<span class="week-event-loc">${locName}</span>` : ""}</div>` : ""}
</div>`;
}
dayOverlays.push(dayHtml);
}
// Grid-aligned overlay mirrors week-grid columns exactly (no calc drift)
const overlayDivs = dayOverlays.map(h =>
`<div style="position:relative;pointer-events:auto;">${h}</div>`
).join("");
const eventsOverlay = `<div style="position:absolute;inset:0;display:grid;grid-template-columns:44px repeat(7,1fr);pointer-events:none;"><div></div>${overlayDivs}</div>`;
let nowHtml = "";
const nowDay = days.findIndex(day => this.dateStr(day) === todayStr);
if (nowDay >= 0) {
@ -2205,10 +2215,12 @@ class FolkCalendarView extends HTMLElement {
return `
<div class="week-view">
<div class="week-header">${headerHtml}</div>
<div style="position:relative;overflow-y:auto;max-height:600px;">
<div class="week-grid" style="position:relative;height:${totalHeight}px">${gridHtml}</div>
<div style="overflow-y:auto;max-height:600px;">
<div style="position:relative;height:${totalHeight}px;">
<div class="week-grid" style="height:100%">${gridHtml}</div>
${eventsOverlay}${nowHtml}
</div>
</div>
</div>`;
}