From 86f1486b77cb345f3cfdacecbc676692cd0d89f7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 7 Sep 2026 07:07:32 +0000 Subject: Split timeline width across lanes instead of stacking overlapping events The CSS gives every .calendar-event `left: 8px; right: 8px`, and the positioning code only ever overrode `left` (by 16px per column). So two events occupying the same slot both stayed full-width to the right edge and the later one simply covered the earlier one, leaving a 16px sliver. The column assignment was computing a correct answer that the layout then threw away. Reported as two yard-work chores rendering on top of each other. They sit at byte-identical timestamps because a bucket cycle stamps every item it activates with the same due_date, which makes this the maximally-bad case (identical top and height, so the overlap is total). Events now split the available width across the number of lanes in their overlap cluster, and a lone event keeps the full-width behaviour it had. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- web/static/js/app.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) (limited to 'web/static/js/app.js') diff --git a/web/static/js/app.js b/web/static/js/app.js index 6071ce3..0a74b46 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -54,7 +54,21 @@ function initTimelineCalendar(calendarId, nowLineId, untimedSectionId) { ev.column = col; } - // Position events with column-based indentation + // How many lanes each event has to share its slot with. The CSS gives every + // event `left: 8px; right: 8px` (full width); previously only `left` was + // overridden, by 16px per column, so two events at the SAME time stayed + // full-width and the later one simply covered the earlier one except for a + // 16px sliver -- reported as two bucket chores "rendered on top of each + // other". They land on identical timestamps because a bucket cycle stamps + // every item it activates with the same due_date. + eventData.forEach(function(ev) { + const overlapping = eventData.filter(function(other) { + return other.endMin > ev.startMin && other.startMin < ev.endMin; + }); + ev.lanes = Math.max.apply(null, overlapping.map(function(o) { return o.column; })) + 1; + }); + + // Position events, splitting the available width across lanes eventData.forEach(function(ev) { const el = ev.el; const hour = parseInt(el.dataset.hour); @@ -65,7 +79,16 @@ function initTimelineCalendar(calendarId, nowLineId, untimedSectionId) { el.style.top = top + 'px'; el.style.height = height + 'px'; - el.style.left = (8 + ev.column * 16) + 'px'; + if (ev.lanes > 1) { + const lane = '((100% - 16px) / ' + ev.lanes + ')'; + el.style.left = 'calc(8px + ' + ev.column + ' * ' + lane + ')'; + el.style.width = 'calc(' + lane + ' - 4px)'; + el.style.right = 'auto'; + } else { + el.style.left = '8px'; + el.style.right = '8px'; + el.style.width = ''; + } el.style.display = 'block'; // Debounced hover effect (100ms delay) -- cgit v1.2.3