From 6cd126faa7f1e79c0e2f8b3ae9a7c80c115724cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 01:06:00 +0000 Subject: [PATCH] fix(macos): align week-grid parity scroll and gutter with web Match web hour-gutter label placement and timed-grid top rule, resync scroll-to-now after the viewport hour height settles in parity capture, instant-scroll web timed grid to the same target, and skip the in-viewport web header in grid-region compare crops. Co-authored-by: Tyler Dane --- .../scripts/compose-desktop-parity-shots.py | 6 +++++- .../CompassUI/TimeGrid/TimeGridView.swift | 18 ++++++++++++++++-- .../NativeParityCaptureTests.swift | 6 ++++++ e2e/parity/seed-demo-fixture.util.ts | 15 +++++++++++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/scripts/compose-desktop-parity-shots.py b/.github/scripts/compose-desktop-parity-shots.py index c8c9c5c6d6..3cf8719a6a 100644 --- a/.github/scripts/compose-desktop-parity-shots.py +++ b/.github/scripts/compose-desktop-parity-shots.py @@ -24,6 +24,10 @@ ) # Web keeps calendar header inside the 1280×820 viewport; macOS uses a unified title bar. WEB_CALENDAR_HEADER_HEIGHT = 48 +# Web week view keeps CalendarHeader inside the 1280×820 viewport; macOS uses a +# unified title bar above the content track. Grid-region crops skip the web header +# so both sides start at the demo banner / day-label band. +WEB_GRID_REGION_TOP = WEB_CALENDAR_HEADER_HEIGHT + 48 MAC_MAIN_CONTENT_TOP = 52 GRID_CROP = (0, 0, CONTENT_LEADING_INSET + CONTENT_TRACK_WIDTH, VIEWPORT_HEIGHT) SIDEBAR_CROP = (CONTENT_LEADING_INSET + CONTENT_TRACK_WIDTH, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT) @@ -31,7 +35,7 @@ def grid_region_crop(*, macos: bool) -> tuple[int, int, int, int]: left, _, right, bottom = GRID_CROP - top = MAC_MAIN_CONTENT_TOP if macos else WEB_CALENDAR_HEADER_HEIGHT + top = MAC_MAIN_CONTENT_TOP if macos else WEB_GRID_REGION_TOP return (left, top, right, bottom) SURFACES = ( diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index 13c6ed39ab..4be9e7ff1c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -390,6 +390,8 @@ public final class TimeGridView: NSView { palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor), currentHour: Int ) { + // Web `CalendarTimeColumn`: `top: calc(100% / TIMED_VISIBLE_HOURS - 5px)`. + let labelColumnTop = max(0, hourHeight - 5) for hour in 0 ..< Int(ceil(state.civilDayHours)) { let text = hour == 0 ? "" : (labels.indices.contains(hour - 1) ? labels[hour - 1] : "") let label = NSTextField(labelWithString: text) @@ -399,7 +401,7 @@ public final class TimeGridView: NSView { label.alignment = .center label.frame = NSRect( x: xOffset + 4, - y: Double(hour) * hourHeight + 4, + y: labelColumnTop + Double(hour) * hourHeight + 4, width: width - 8, height: 14) hourGutterView.addSubview(label) @@ -575,8 +577,20 @@ public final class TimeGridView: NSView { timedContentView.layer?.backgroundColor = palette.background.cgColor let todayKey = CompassDateParsing.formatCalendarDay(state.referenceNow) + let timedTopRule = NSView( + frame: NSRect( + x: snapshot.metrics.marginLeft, + y: 0, + width: timedContentView.frame.width - snapshot.metrics.marginLeft, + height: 1 + ) + ) + timedTopRule.wantsLayer = true + timedTopRule.layer?.backgroundColor = palette.border.cgColor + addGridBackground(timedTopRule, to: timedContentView) + for column in snapshot.columns { - // Web `TimedGrid`: only past days receive the surface tone. + // Web `TimedGrid` `data-[past=true]:bg-surface` on past day columns. if state.layoutMode == .week, column.key < todayKey { let tint = NSView(frame: NSRect(x: column.left, y: 0, width: column.width, height: gridHeight)) tint.wantsLayer = true diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 2de24844ab..18b894d0b0 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -298,6 +298,9 @@ final class NativeParityCaptureTests: XCTestCase { waitForGridColumnCount(window, atLeast: 5, timeout: 20), "Expected five day columns in parity week grid") window.typeKey("t", modifierFlags: []) + RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.6)) + // Re-run today/scroll after the timed viewport height settles (viewport-based hourHeight). + window.typeKey("t", modifierFlags: []) let sundayColumn = window.descendants(matching: .any)["compass-grid-column-2026-06-07"] XCTAssertTrue( sundayColumn.waitForExistence(timeout: 20), @@ -313,6 +316,9 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") + XCTAssertTrue( + window.buttons["Exercise"].waitForExistence(timeout: 20), + "Expected Wednesday Exercise demo event after scroll-to-now parity setup") let dismissScrollHint = window.buttons["Dismiss"] if dismissScrollHint.waitForExistence(timeout: 2) { dismissScrollHint.tap() diff --git a/e2e/parity/seed-demo-fixture.util.ts b/e2e/parity/seed-demo-fixture.util.ts index 79621b373e..2d1bd3d1d6 100644 --- a/e2e/parity/seed-demo-fixture.util.ts +++ b/e2e/parity/seed-demo-fixture.util.ts @@ -85,8 +85,19 @@ export async function prepareParityDemoWeek( } await page.locator("#mainGrid").focus(); await page.keyboard.press("t"); - // Web scroll-to-now uses smooth scrolling; wait for parity PNG alignment with macOS. - await page.waitForTimeout(800); + // Match macOS instant scroll-to-now during parity capture (no mid-animation PNGs). + await page.evaluate(() => { + const timedGrid = document.getElementById("timedColumns"); + if (!(timedGrid instanceof HTMLElement)) return; + const timedVisibleHours = 13; + const scrollToNowBufferPx = 150; + const minuteHeight = timedGrid.clientHeight / timedVisibleHours / 60; + const now = new Date(); + const minutes = now.getUTCHours() * 60 + now.getUTCMinutes(); + const targetTop = Math.max(0, minutes * minuteHeight - scrollToNowBufferPx); + timedGrid.scrollTo({ top: targetTop, behavior: "instant" }); + }); + await page.waitForTimeout(400); await page .getByRole("button", { name: /Exercise|Morning standup/i }) .first()