From 05538b0e9ef1780a20cffb5ead66b4759408463e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:52:00 +0000 Subject: [PATCH 1/5] fix(macos): put hour gutter labels on the 1 am track origin like web Cards, hour lines and the now line sit on web's 1 AM timed-track origin (timedGridTopPx), but the gutter still used the old web layout: a calc(100% / 13 - 5px) offset plus an empty first slot. Every label drew two hours below its events, so a 5 PM event sat on the 3 PM row. Web #4686 dropped that offset; lay labels out one slot per hour from 1 AM at the top of the track. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr --- .../CompassUI/TimeGrid/TimeGridView.swift | 15 +++++++------- .../TimeGridCivilDayTests.swift | 20 +++++++++++++++++++ docs/features/desktop-web-parity-checklist.md | 1 + 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index 22a6e41869..248619cebf 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -390,18 +390,19 @@ 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] : "") + // Web `HourLabelColumn` (#4686): one hour slot per label from 1 AM, + // starting at the top of the track, the same 1 AM origin cards, hour + // lines and the now line use (`timedGridTopPx`). The old + // `calc(100% / TIMED_VISIBLE_HOURS - 5px)` offset plus an empty first + // slot drew every label two hours below its events. + for (index, text) in labels.enumerated() { let label = NSTextField(labelWithString: text) label.font = CompassFont.nsFont(size: 10) - let isCurrentHour = hour > 0 && hour == currentHour - label.textColor = isCurrentHour ? themeAccentColor : palette.textMuted + label.textColor = index + 1 == currentHour ? themeAccentColor : palette.textMuted label.alignment = .center label.frame = NSRect( x: xOffset + 4, - y: labelColumnTop + Double(hour) * hourHeight + 4, + y: Double(index) * hourHeight + 1, width: width - 8, height: 14) hourGutterView.addSubview(label) diff --git a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridCivilDayTests.swift b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridCivilDayTests.swift index 5e69fa098b..74fff0d5ec 100644 --- a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridCivilDayTests.swift +++ b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/TimeGridCivilDayTests.swift @@ -28,6 +28,26 @@ final class TimeGridCivilDayTests: XCTestCase { } } + /// Web #4686: the 5 PM label sits at the top of the 5 PM slot, where a + /// 5 PM event starts (1 AM track origin), not two hours lower. + func testHourLabelsShareTheEventTrackOrigin() throws { + let savedZone = EffectiveTimeZone.identifier + defer { EffectiveTimeZone.identifier = savedZone } + EffectiveTimeZone.identifier = "UTC" + let state = TimeGridState(layoutMode: .day, + scenario: GridLayoutScenario(layoutMode: .day, + referenceNow: GridLayoutSnapshotFixtures.referenceNow, visibleDateKeys: ["2026-05-20"]), + effectiveTimeZone: "UTC") + let view = TimeGridView(state: state, theme: .lightBeach) + view.frame = NSRect(x: 0, y: 0, width: 800, height: 600) + view.layoutSubtreeIfNeeded() + let document = try XCTUnwrap(descendants(view).compactMap { $0 as? NSScrollView }.first?.documentView) + let hourHeight = (document.frame.height - GridTimeConstants.gridPaddingBottom) / state.civilDayHours + let label = try XCTUnwrap(descendants(view).compactMap { $0 as? NSTextField }.first { $0.stringValue == "5 PM" }) + let eventTop = GridTimeConstants.timedGridTopPx(minutesFromCivilMidnight: 17 * 60, hourHeight: hourHeight) + XCTAssertEqual(label.frame.minY, eventTop, accuracy: 3, "5 PM label at \(label.frame.minY), 5 PM event top at \(eventTop)") + } + private func descendants(_ view: NSView) -> [NSView] { view.subviews.flatMap { [$0] + descendants($0) } } diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index c5a70fb3b7..9339032a51 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -341,6 +341,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu | 2026-10-21 | Hold-Mod sidebar chips: month picker and Up next chips render, calendar list chip moved onto the list (compare run 39634795849) | | 2026-10-21 | Per-account page-jump targets (web `calendar-account:` ids) | | 2026-10-21 | Event jump chips use web day-prefix labels (W1, SU2) and digits after a day pick | +| 2026-10-22 | Week/day hour gutter labels use web's 1 AM track origin (#4686); they sat two hours below their events (a 5 PM event drew on the 3 PM row) | --- From 081fe2218ec1d4193d369721d3c23602381c55f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:08:48 +0000 Subject: [PATCH 2/5] fix(macos): name welcome buttons by their title, not title plus keycap The welcome modal's buttons pair a title with a keycap chip and had no explicit label, so accessibility read "Get started for free, Enter". The onboarding parity capture added in #4707 clicks buttons["Get started for free"] and fails on every run since. Label each welcome button with its title; the keycap is a visual hint. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 5d42363f902f34d41b36dc5b6fd00f17efcba748) --- .../Sources/CompassUI/Onboarding/WelcomeModalView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift index c8321b888d..dff2f371ea 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift @@ -179,6 +179,8 @@ struct WelcomeModalView: View { .shadow(color: .black.opacity(0.15), radius: 6, y: 3) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func secondaryButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -195,6 +197,8 @@ struct WelcomeModalView: View { .overlay(Capsule().stroke(theme.borderColor, lineWidth: 1)) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func quietTextButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -208,6 +212,8 @@ struct WelcomeModalView: View { .frame(maxWidth: .infinity, minHeight: 32) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } private func pillButton(title: String, hint: String, action: @escaping () -> Void) -> some View { @@ -225,5 +231,7 @@ struct WelcomeModalView: View { .overlay(Capsule().stroke(theme.borderColor, lineWidth: 1)) } .buttonStyle(.plain) + // The keycap is a visual hint; the title alone names the button. + .accessibilityLabel(title) } } From 28047216a6938a20e51b782caa4f43933f0597e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:18:26 +0000 Subject: [PATCH 3/5] test(macos): scope the welcome capture's button clicks to the welcome panel With the welcome buttons named by title, buttons["Next"] also matched the grid's week "Next" button behind the modal, so the click failed with multiple matches. Query the buttons inside compass-native-welcome-modal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 77e87f4e309cfb77c501973831d1afc29f33c405) --- .../CompassUITests/NativeParityCaptureTests.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index f7b9668808..0abc77cb00 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -260,14 +260,16 @@ final class NativeParityCaptureTests: XCTestCase { app.launch() let window = app.windows["Compass"] XCTAssertTrue(window.waitForExistence(timeout: 15)) - XCTAssertTrue( - window.descendants(matching: .any)["compass-native-welcome-modal"].waitForExistence(timeout: 15)) + let welcome = window.descendants(matching: .any)["compass-native-welcome-modal"] + XCTAssertTrue(welcome.waitForExistence(timeout: 15)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome") - window.buttons["Get started for free"].click() - window.buttons["Next"].click() + // Scope to the panel: the grid's own "Next" (week) button is still in + // the window's accessibility tree behind the modal. + welcome.buttons["Get started for free"].click() + welcome.buttons["Next"].click() XCTAssertTrue( - window.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) + welcome.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome-account-choice") } From 144c1a783d07b29e4bdc3b741d48607c5bdf94bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 03:54:23 +0000 Subject: [PATCH 4/5] test(macos): wait for the local-only choice on the welcome account step Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit b031ea7615b6c8a37b21f6900fd31c7c1c164767) --- .../CompassUITests/NativeParityCaptureTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 0abc77cb00..0a04d41297 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -268,8 +268,9 @@ final class NativeParityCaptureTests: XCTestCase { // the window's accessibility tree behind the modal. welcome.buttons["Get started for free"].click() welcome.buttons["Next"].click() - XCTAssertTrue( - welcome.buttons["Start free trial with Google"].waitForExistence(timeout: 8)) + // The provider buttons depend on the backend config; the local-only + // choice is always on the account step. + XCTAssertTrue(welcome.buttons["On this device only"].waitForExistence(timeout: 8)) sleep(1) NativeImageSnapshotSupport.captureWindow(window, named: "onboarding-welcome-account-choice") } From 5284803c17d67d75cd2dc7dab6a37d849a82a865 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 04:41:53 +0000 Subject: [PATCH 5/5] fix(macos): register the welcome account-choice parity surface The onboarding capture exports onboarding-welcome-account-choice, but the name was not a ParityCaptureSurface, so the debug URL never parsed and the export timed out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKHjv2HDx1DFAnG4cfSxcr (cherry picked from commit 07b3cce29ab32d86c7672b047c28eb04b6bec98e) --- .../Sources/CompassKit/Layout/ParityCaptureSurface.swift | 1 + .../CompassKitTests/DesktopDebugCommandTests.swift | 4 ++++ docs/features/desktop-web-parity-checklist.md | 1 + 3 files changed, 6 insertions(+) diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift index e4ea81e82b..b656dedb05 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureSurface.swift @@ -15,6 +15,7 @@ public enum ParityCaptureSurface: String, CaseIterable, Sendable { case commandPalette = "command-palette" case settings = "settings" case onboardingWelcome = "onboarding-welcome" + case onboardingWelcomeAccountChoice = "onboarding-welcome-account-choice" public static func isValid(_ name: String) -> Bool { ParityCaptureSurface(rawValue: name) != nil diff --git a/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift b/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift index 0825b9d98d..ed6367d82f 100644 --- a/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift +++ b/apps/calendar-macos/CompassKitTests/DesktopDebugCommandTests.swift @@ -59,6 +59,10 @@ final class DesktopDebugCommandTests: XCTestCase { XCTAssertEqual( DesktopDebugCommand.parse("compass://debug/parity-export-title-bar-reference"), .parityExportTitleBarReference) + // Every name NativeParityCaptureTests exports must parse, or the export times out. + XCTAssertEqual( + DesktopDebugCommand.parse("compass://debug/parity-export?name=onboarding-welcome-account-choice"), + .parityExport(surface: "onboarding-welcome-account-choice")) XCTAssertNil(DesktopDebugCommand.parse("compass://debug/parity-export?name=not-a-surface")) XCTAssertNil(DesktopDebugCommand.parse("compass://debug/unknown")) diff --git a/docs/features/desktop-web-parity-checklist.md b/docs/features/desktop-web-parity-checklist.md index 9339032a51..45d6de6eef 100644 --- a/docs/features/desktop-web-parity-checklist.md +++ b/docs/features/desktop-web-parity-checklist.md @@ -342,6 +342,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu | 2026-10-21 | Per-account page-jump targets (web `calendar-account:` ids) | | 2026-10-21 | Event jump chips use web day-prefix labels (W1, SU2) and digits after a day pick | | 2026-10-22 | Week/day hour gutter labels use web's 1 AM track origin (#4686); they sat two hours below their events (a 5 PM event drew on the 3 PM row) | +| 2026-10-22 | macOS capture: register `onboarding-welcome-account-choice` so the welcome account step exports instead of timing out | ---