Skip to content

fix(calendar): lay out overlapping events in side-by-side lanes - #296

Merged
h4yfans merged 1 commit into
mainfrom
funny-wescoff-37863a
Apr 20, 2026
Merged

fix(calendar): lay out overlapping events in side-by-side lanes#296
h4yfans merged 1 commit into
mainfrom
funny-wescoff-37863a

Conversation

@h4yfans

@h4yfans h4yfans commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix day and week view rendering so overlapping events get dedicated lanes instead of stacking at full column width with identical z-index.

The bug: when two events overlapped in time, the later DOM sibling captured every click across the whole column — the earlier chip was visually peeking above/below but completely unclickable for real users. E2E tests had been working around this with `.first()` and `{ force: true }`.

The fix: standard interval graph coloring / greedy lane packing. Sort events by start (longer-first as tiebreak), greedy-assign each to the first lane whose last event has already ended. Within each overlap cluster, render at `width = 100/laneCount` with a lane-indexed `left` offset. Single events still render at full column width — no regression for the common case.

What's in the diff

  • `overlap-layout.ts` (new): pure `assignLanes()` with 11 unit tests covering empty, single, non-overlapping, boundary-touching, two-overlap, chain overlap, multi-cluster, three-way, null-endAt, input-order stability, and nested events.
  • `calendar-day-view.tsx`, `calendar-week-view.tsx`: wired to `assignLanes` with percentage-based `left`/`width` and `px-0.5` (`@xl:px-1`) inner padding for lane gutters.
  • `test-hooks.ts`: optional `overlapMemryTitle` field on `CalendarProjectionSeedInput` that seeds a 9:00–10:00 Memry event overlapping the existing 9:30–10:30 external seed. Backwards compatible — existing callers that omit the field see zero behavior change.
  • `calendar-overlap.e2e.ts` (new): 2 Playwright tests covering day and week views. Each asserts chips have disjoint horizontal bounding boxes; the day-view test additionally verifies clicking the external chip opens the promote-external dialog with a real Playwright click (no `dispatchEvent`, no `{ force: true }`, no `.first()`).

Why this approach

Column lane packing is what Google Calendar, Apple Calendar, and Outlook all do — it's the standard UX. The alternative (stack indicator with "+N events" popover) would be inferior on busy days because titles become invisible at a glance.

Cluster-wide `laneCount` (rather than per-event widths based on personal overlap set) keeps the column grid visually consistent — chips align vertically like a mini CSS grid instead of jumping widths.

Test plan

  • `vitest run overlap-layout.test.ts` — 11/11 passing
  • `vitest run` (full desktop suite) — 7123/7124 passing (1 pre-existing skip, no new failures)
  • `typecheck:node` — clean
  • `typecheck:web` — clean
  • `pnpm lint` — 0 new warnings
  • `playwright test calendar-overlap.e2e.ts` — 2/2 passing
  • Manual repro: create a 9:00 Memry event that overlaps the 9:30 imported event, confirm both are independently clickable in day and week views

Notes for the reviewer

  • Pre-existing failures (not mine). Running the full comprehensive e2e surfaced 5 failures in `Event creation — full editor` / `Event editing` where the "New Event" drawer doesn't open in the built bundle. I stashed my changes, rebuilt, and reproduced the same 5 failures on the branch baseline — they're unrelated to this PR.

Before this change, day and week views rendered every timed event at
full column width with identical z-index. When events overlapped in
time, the later DOM sibling captured every click across the full column
— the earlier chip was visible (poking above/below the overlay) but
completely unclickable for real users.

Fix applies standard interval graph coloring / greedy lane packing:
sort events by start (tiebreak longer-first), scan through assigning
each to the first lane whose last event has already ended. Within an
overlap cluster every event renders at width = 100/laneCount with a
lane-indexed left offset, so each chip has a dedicated hit region.

- apps/desktop/src/renderer/src/components/calendar/overlap-layout.ts:
  new pure function assignLanes() with 11 unit tests covering empty,
  single, non-overlapping, boundary-touching, two-overlap, chain
  overlap, multi-cluster, three-way, null-endAt, input-order stability,
  and nested events.
- apps/desktop/src/renderer/src/components/calendar/calendar-day-view.tsx,
  calendar-week-view.tsx: wired to assignLanes with percentage-based
  left/width and px-0.5 (@XL:px-1) inner padding for lane gutters.
- apps/desktop/src/main/test-hooks.ts: optional overlapMemryTitle field
  on CalendarProjectionSeedInput that seeds a 9:00–10:00 Memry event
  overlapping the existing 9:30–10:30 external seed.
- apps/desktop/tests/e2e/calendar-overlap.e2e.ts: Playwright regression
  coverage for day and week views — asserts chips have disjoint
  horizontal bounding boxes and that clicking the external chip opens
  the promote-external dialog without force/dispatch tricks.
@h4yfans
h4yfans force-pushed the funny-wescoff-37863a branch from 82b6670 to 1089878 Compare April 20, 2026 09:51
@h4yfans
h4yfans merged commit 133618e into main Apr 20, 2026
2 checks passed
@h4yfans
h4yfans deleted the funny-wescoff-37863a branch May 6, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant