From 9b069adba1b5ebb0dbeb1304d96b5c1d1a60ce1a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 01:42:15 +0000 Subject: [PATCH 01/25] fix(macos): keep parity week gutter and Sunday window pinned Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 15 ++++- ...ndarRootModelParityPresentationTests.swift | 59 +++++++++++++++++++ .../CompassUI/TimeGrid/TimeGridView.swift | 20 +++++++ .../NativeParityCaptureTests.swift | 12 ++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index ddbb68a21..702c424eb 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -219,7 +219,11 @@ public final class NativeCalendarRootModel { overlayStores.legend.onOpenChanged = { [weak self] open in self?.onShortcutsLegendAccessibilityProbeChanged?(open) } - rebuildPresentation() + if UITestLaunchPolicy.isParityCapture { + applyParityCapturePresentationIfNeeded() + } else { + rebuildPresentation() + } } public func closeSettingsIfAllowed() { @@ -919,7 +923,7 @@ public final class NativeCalendarRootModel { let calendar = EffectiveTimeZone.calendar if viewStore.visibleDayCount == 1 { viewStore.setAnchorDate(today) - } else { + } else if !isReferenceDayInVisibleWindow(today) { viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) } monthPickerMonth = today @@ -928,6 +932,13 @@ public final class NativeCalendarRootModel { pendingScroll = .scrollToNowOrPulse } + /// Matches web `useWeek` `isCurrentWeek` before `goToToday` moves the anchor. + private func isReferenceDayInVisibleWindow(_ day: Date) -> Bool { + let calendar = EffectiveTimeZone.calendar + let dayStart = calendar.startOfDay(for: day) + return dayStart >= startOfView && dayStart <= endOfView + } + private func shiftMonth(by months: Int) { let calendar = EffectiveTimeZone.calendar if let next = calendar.date(byAdding: .month, value: months, to: monthPickerMonth) { diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift new file mode 100644 index 000000000..5807adde9 --- /dev/null +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift @@ -0,0 +1,59 @@ +import CompassData +import CompassKit +import XCTest + +@MainActor +final class NativeCalendarRootModelParityPresentationTests: XCTestCase { + private var priorParityEnv: String? + + override func setUp() { + super.setUp() + priorParityEnv = ProcessInfo.processInfo.environment["PARITY_CAPTURE"] + setenv("PARITY_CAPTURE", "1", 1) + } + + override func tearDown() { + if let priorParityEnv { + setenv("PARITY_CAPTURE", priorParityEnv, 1) + } else { + unsetenv("PARITY_CAPTURE") + } + super.tearDown() + } + + func testParityCaptureUsesDemoSundayWeekAndFiveColumns() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + + XCTAssertEqual( + CompassDateParsing.formatCalendarDay(model.viewStore.anchorDate), + "2026-06-07" + ) + XCTAssertEqual(model.viewStore.visibleDayCount, ParityCaptureGeometry.visibleDayCount) + XCTAssertEqual( + Double(model.contentTrackWidth), + ParityCaptureGeometry.contentTrackWidth, + accuracy: 0.5 + ) + XCTAssertEqual( + model.timeGridState.scenario.visibleDateKeys.first, + "2026-06-07" + ) + XCTAssertEqual(model.timeGridState.scenario.visibleDateKeys.count, 5) + } + + func testGoToTodayDoesNotShiftAnchorWhenDemoDayAlreadyVisible() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + model.applyParityCapturePresentationIfNeeded() + let anchorBefore = model.viewStore.anchorDate + + model.handleShortcut(.navToday) + + XCTAssertEqual(model.viewStore.anchorDate, anchorBefore) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index b52a8c04a..abc783b79 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -266,9 +266,27 @@ public final class TimeGridView: NSView { } private func syncPinnedHeaderScrollX() { + guard state.layoutMode == .day else { + pinnedHeaderView.setBoundsOrigin(.zero) + return + } pinnedHeaderView.setBoundsOrigin(NSPoint(x: scrollView.contentView.bounds.origin.x, y: 0)) } + /// Week view keeps the timezone header and hour gutter pinned at x=0. + private func resetWeekHorizontalScrollIfNeeded() { + guard state.layoutMode == .week else { return } + var origin = scrollView.contentView.bounds.origin + guard origin.x != 0 else { + pinnedHeaderView.setBoundsOrigin(.zero) + return + } + origin.x = 0 + scrollView.contentView.scroll(to: origin) + scrollView.reflectScrolledClipView(scrollView.contentView) + pinnedHeaderView.setBoundsOrigin(.zero) + } + private func relayout() { let colWidths = state.resolvedColumnWidths() let snapshot = state.snapshot(colWidths: colWidths) @@ -308,6 +326,8 @@ public final class TimeGridView: NSView { renderTimedGrid(snapshot: snapshot, hourHeight: hourHeight, gridHeight: gridHeight) renderCards(snapshot: snapshot) updateNowLine() + resetWeekHorizontalScrollIfNeeded() + syncPinnedHeaderScrollX() } private func renderHourGutter( diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 0075506e9..e7d25a5a5 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -113,7 +113,19 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( waitForGridColumnCount(window, atLeast: 5, timeout: 20), "Expected five day columns in parity week grid") + let timedGrid = window.descendants(matching: .any)["compass-grid-timed"] + if timedGrid.waitForExistence(timeout: 5) { + timedGrid.click() + } window.typeKey("t", modifierFlags: []) + let sundayColumn = window.descendants(matching: .any)["compass-grid-column-2026-06-07"] + XCTAssertTrue( + sundayColumn.waitForExistence(timeout: 20), + "Expected Sunday demo week column (2026-06-07) after nav today") + XCTAssertTrue( + window.staticTexts["Calendar timezone: UTC"].waitForExistence(timeout: 5) + || window.buttons["Calendar timezone: UTC"].waitForExistence(timeout: 5), + "Expected UTC timezone header on parity week grid") XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") From c1bf4090686adc6947f69f50578e0ac63d13371e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:23:35 +0000 Subject: [PATCH 02/25] fix(macos): align command palette navigation rows and icons with web Co-authored-by: Tyler Dane --- .../CommandPaletteCatalog.swift | 82 ++++++++++++++----- .../CommandPalette/CommandPaletteTypes.swift | 5 +- .../CompassData/NativeCalendarOverlays.swift | 10 ++- .../CompassData/Stores/LevelsStore.swift | 15 +++- .../Stores/ShortcutUsageStorage.swift | 1 + .../CommandPaletteOverlay.swift | 8 +- 6 files changed, 95 insertions(+), 26 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift index e609ca474..d18302976 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift @@ -10,7 +10,9 @@ public enum CommandPaletteCatalog { isSignedIn: Bool, isTrialing: Bool, eventHits: [CommandPaletteEventHit], - recentIds: [String] + recentIds: [String], + tipsMuted: Bool, + levelHidden: Bool ) -> [CommandPaletteSection] { var built: [CommandPaletteSection] = [] @@ -18,7 +20,10 @@ public enum CommandPaletteCatalog { built.append(CommandPaletteSection(id: "go-to-date", heading: "", items: [goToDate])) } - let navigation = navigationItems(view: view) + let navigation = navigationItems( + view: view, + tipsMuted: tipsMuted, + levelHidden: levelHidden) if !navigation.isEmpty { built.append(CommandPaletteSection(id: "navigation", heading: "Navigation", items: navigation)) } @@ -89,14 +94,17 @@ public enum CommandPaletteCatalog { } private static func navigationItems( - view: CalendarGridView + view: CalendarGridView, + tipsMuted: Bool, + levelHidden: Bool ) -> [CommandPaletteItem] { var items: [CommandPaletteItem] = [ CommandPaletteItem( id: "today", label: "Go to Today", shortcut: "t", - keywords: ["now", "current", "jump"]), + keywords: ["now", "current", "jump"], + systemImage: "arrow.uturn.backward"), ] if view != .day { items.append( @@ -104,7 +112,8 @@ public enum CommandPaletteCatalog { id: "go-to-day", label: "Go to Day view", shortcut: "d", - keywords: ["day view", "daily"])) + keywords: ["day view", "daily"], + systemImage: "calendar")) } if view != .week { items.append( @@ -112,7 +121,8 @@ public enum CommandPaletteCatalog { id: "go-to-week", label: "Go to Week view", shortcut: "w", - keywords: ["week view", "weekly"])) + keywords: ["week view", "weekly"], + systemImage: "calendar.day.timeline.left")) } if view != .life { items.append( @@ -120,14 +130,50 @@ public enum CommandPaletteCatalog { id: "go-to-life", label: "Go to Life view", shortcut: "l", - keywords: ["life view", "years"])) + keywords: ["life view", "years"], + systemImage: "hourglass")) } items.append( CommandPaletteItem( id: "show-shortcuts", label: "Show shortcuts", shortcut: "?", - keywords: ["keyboard", "legend", "help"])) + keywords: ["keyboard", "legend", "help"], + systemImage: "keyboard")) + items.append( + CommandPaletteItem( + id: "practice-shortcuts", + label: "Play Block Party (practice shortcuts)", + keywords: [ + "onboarding", "tour", "intro", "help", "tutorial", "coach", "sandbox", + "practice", "shortcuts", "game", "play", "block party", + ], + systemImage: "safari")) + items.append( + CommandPaletteItem( + id: "show-welcome-guide", + label: "Show welcome guide", + keywords: ["onboarding", "tour", "intro", "help", "faq"], + systemImage: "safari")) + items.append( + CommandPaletteItem( + id: "toggle-shortcut-tips", + label: tipsMuted ? "Show shortcut tips" : "Hide shortcut tips", + keywords: ["sidebar", "hint", "tips", "teaching", "shortcuts", "keyboard"], + systemImage: tipsMuted ? "eye" : "eye.slash")) + items.append( + CommandPaletteItem( + id: "toggle-shortcut-level", + label: levelHidden ? "Show shortcut level" : "Hide shortcut level", + keywords: ["level", "progress", "badge", "shortcuts", "keyboard"], + systemImage: levelHidden ? "eye" : "eye.slash")) + items.append( + CommandPaletteItem( + id: "toggle-sidebar", + label: "Toggle sidebar", + shortcut: "]", + keywords: ["panel", "hide sidebar", "show sidebar"], + systemImage: "sidebar.left")) return items } @@ -142,7 +188,8 @@ public enum CommandPaletteCatalog { id: "recent-" + id, label: item.label, shortcut: item.shortcut, - keywords: item.keywords) + keywords: item.keywords, + systemImage: item.systemImage) } } @@ -180,26 +227,17 @@ public enum CommandPaletteCatalog { label: "Redo", shortcut: "mod+shift+z", keywords: ["repeat", "forward"]), - CommandPaletteItem( - id: "show-welcome-guide", - label: "Show welcome guide", - keywords: ["onboarding", "tour", "intro", "help", "faq"]), - CommandPaletteItem( - id: "practice-shortcuts", - label: "Play Block Party (practice shortcuts)", - keywords: [ - "onboarding", "tour", "intro", "help", "tutorial", "coach", "sandbox", - "practice", "shortcuts", "game", "play", "block party", - ]), CommandPaletteItem( id: "open-settings", label: "Settings", shortcut: ",", - keywords: ["preferences", "account"]), + keywords: ["preferences", "account"], + systemImage: "gearshape"), CommandPaletteItem( id: "about", label: "About Compass", - keywords: ["version", "info"]), + keywords: ["version", "info"], + systemImage: "info.circle"), ] if isTrialing { items.insert( diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteTypes.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteTypes.swift index 3a4fbdda6..e5131c8e6 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteTypes.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteTypes.swift @@ -6,19 +6,22 @@ public struct CommandPaletteItem: Identifiable, Hashable, Sendable { public let shortcut: String? public let keywords: [String] public let subtitle: String? + public let systemImage: String? public init( id: String, label: String, shortcut: String? = nil, keywords: [String] = [], - subtitle: String? = nil + subtitle: String? = nil, + systemImage: String? = nil ) { self.id = id self.label = label self.shortcut = shortcut self.keywords = keywords self.subtitle = subtitle + self.systemImage = systemImage } } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift index 5972a530e..b046f36f7 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift @@ -30,7 +30,9 @@ extension NativeCalendarRootModel { isSignedIn: isSignedIn, isTrialing: billingStore.status?.subscriptionStatus == .trialing, eventHits: mergedHits, - recentIds: commandPaletteStore.recentCommandIds) + recentIds: commandPaletteStore.recentCommandIds, + tipsMuted: levelsStore.tipsMuted, + levelHidden: levelsStore.badgeHidden) } public func filteredPaletteSections() -> [CommandPaletteSection] { @@ -120,6 +122,12 @@ extension NativeCalendarRootModel { blockPartyStore.replayFromPalette() case "show-welcome-guide": onboardingStore.openWelcomeGuide() + case "toggle-shortcut-tips": + levelsStore.setTipsMuted(!levelsStore.tipsMuted) + case "toggle-shortcut-level": + levelsStore.setBadgeHidden(!levelsStore.badgeHidden) + case "toggle-sidebar": + handleShortcut(.otherSidebar) case "sign-up": authStore.openModal(.signUp) case "log-in": diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/LevelsStore.swift b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/LevelsStore.swift index d89c7b4a2..b1509980a 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/LevelsStore.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/LevelsStore.swift @@ -37,6 +37,7 @@ public struct LevelUpToastSnapshot: Hashable, Sendable, Identifiable { public final class LevelsStore { public private(set) var levelSnapshot: ShortcutLevelSnapshot public private(set) var badgeHidden: Bool + public private(set) var tipsMuted: Bool public private(set) var badgePulsing = false public private(set) var currentTip: ShortcutTipSnapshot? public private(set) var pendingLevelUpToast: LevelUpToastSnapshot? @@ -63,6 +64,7 @@ public final class LevelsStore { let initialUsedIds = ShortcutUsagePersistence.usedShortcutIds(from: profile) usedIds = initialUsedIds badgeHidden = defaults.bool(forKey: ShortcutUsageStorageKeys.levelHidden) + tipsMuted = defaults.bool(forKey: ShortcutUsageStorageKeys.tipsMuted) tipRotationIndex = defaults.integer(forKey: ShortcutUsageStorageKeys.tipRotationIndex) levelSnapshot = ShortcutLevelMath.compute( usedIds: initialUsedIds, @@ -104,6 +106,17 @@ public final class LevelsStore { advanceTipRotation() } + public func setTipsMuted(_ muted: Bool) { + guard muted != tipsMuted else { return } + tipsMuted = muted + defaults.set(muted, forKey: ShortcutUsageStorageKeys.tipsMuted) + if muted { + currentTip = nil + } else { + refreshTip() + } + } + public func setBadgeHidden(_ hidden: Bool) { guard hidden != badgeHidden else { return } badgeHidden = hidden @@ -176,7 +189,7 @@ public final class LevelsStore { } private func refreshTip() { - guard !badgeHidden else { + guard !tipsMuted, !badgeHidden else { currentTip = nil return } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/ShortcutUsageStorage.swift b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/ShortcutUsageStorage.swift index 1f1e7ac45..0672d1042 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/ShortcutUsageStorage.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/ShortcutUsageStorage.swift @@ -3,6 +3,7 @@ import Foundation enum ShortcutUsageStorageKeys { static let personalization = "compass.shortcuts.personalization" static let levelHidden = "compass.shortcuts.level-hidden" + static let tipsMuted = "compass.shortcuts.tips-muted" static let levelCelebrated = "compass.shortcuts.level-celebrated" static let tipRotationIndex = "compass.shortcuts.tip-rotation-index" } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift index 14c9b7eba..520870b3c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift @@ -100,7 +100,13 @@ public struct CommandPaletteOverlay: View { Button { model.runPaletteCommand(id: item.id) } label: { - HStack { + HStack(spacing: 10) { + if let systemImage = item.systemImage { + Image(systemName: systemImage) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(theme.textMutedColor) + .frame(width: 18, alignment: .center) + } VStack(alignment: .leading, spacing: 2) { CommandPaletteHighlightedLabel(label: item.label, query: query) if let subtitle = item.subtitle { From 6cd18a02b1780a87c5da90c2d35ef78d4fcffedb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:33:33 +0000 Subject: [PATCH 03/25] fix(macos): quick-time sidebar status and current-hour gutter accent Co-authored-by: Tyler Dane --- .../CommandPaletteCatalog.swift | 14 ++++++-- .../CompassData/NativeCalendarOverlays.swift | 7 +++- .../NativeCalendarRootModel+Draft.swift | 3 ++ .../CompassKit/Calendar/QuickTime.swift | 9 +++++ .../Sources/CompassUI/RootView.swift | 9 ++++- .../Sidebar/QuickTimeStatusLine.swift | 35 +++++++++++++++++++ .../Sidebar/ShortcutSidebarFooter.swift | 22 +++++++++++- .../CompassUI/Sidebar/UpNextCard.swift | 8 +++++ .../CompassUI/TimeGrid/TimeGridView.swift | 13 ++++--- 9 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/QuickTimeStatusLine.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift index d18302976..90affa787 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift @@ -12,7 +12,8 @@ public enum CommandPaletteCatalog { eventHits: [CommandPaletteEventHit], recentIds: [String], tipsMuted: Bool, - levelHidden: Bool + levelHidden: Bool, + theme: CompassThemeName ) -> [CommandPaletteSection] { var built: [CommandPaletteSection] = [] @@ -48,7 +49,7 @@ public enum CommandPaletteCatalog { } built.append(contentsOf: eventActionSections()) - built.append(contentsOf: moreSections(isTrialing: isTrialing)) + built.append(contentsOf: moreSections(isTrialing: isTrialing, theme: theme)) var accountItems = [CommandPaletteItem( id: "export-data", @@ -214,8 +215,10 @@ public enum CommandPaletteCatalog { } private static func moreSections( - isTrialing: Bool + isTrialing: Bool, + theme: CompassThemeName ) -> [CommandPaletteSection] { + let isDark = theme == .darkAbyss var items: [CommandPaletteItem] = [ CommandPaletteItem( id: "undo", @@ -227,6 +230,11 @@ public enum CommandPaletteCatalog { label: "Redo", shortcut: "mod+shift+z", keywords: ["repeat", "forward"]), + CommandPaletteItem( + id: "toggle-theme", + label: isDark ? "Switch to light theme" : "Switch to dark theme", + keywords: ["theme", "dark mode", "light mode", "appearance"], + systemImage: isDark ? "sun.max" : "moon.stars"), CommandPaletteItem( id: "open-settings", label: "Settings", diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift index b046f36f7..4c35c0f2c 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift @@ -32,7 +32,8 @@ extension NativeCalendarRootModel { eventHits: mergedHits, recentIds: commandPaletteStore.recentCommandIds, tipsMuted: levelsStore.tipsMuted, - levelHidden: levelsStore.badgeHidden) + levelHidden: levelsStore.badgeHidden, + theme: settingsStore.theme) } public func filteredPaletteSections() -> [CommandPaletteSection] { @@ -116,6 +117,10 @@ extension NativeCalendarRootModel { handleShortcut(.navLifeView) case "show-shortcuts": shortcutsLegendStore.open() + case "toggle-theme": + let next: CompassThemeName = + settingsStore.theme == .darkAbyss ? .lightBeach : .darkAbyss + settingsStore.setTheme(next) case "open-settings": handleShortcut(.otherSettings) case "practice-shortcuts": diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift index 33d8ab657..0288ac7e0 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift @@ -6,7 +6,10 @@ extension NativeCalendarRootModel { func draftTargetDay() -> Date { let calendar = EffectiveTimeZone.calendar let now = referenceNow + let jumpColumnDay = QuickTime.quickTimeFocusedColumnDay( + activeDayKeys: eventJumpSelection.activeDayKeys) let focusedDay: Date? = { + if let jumpColumnDay { return jumpColumnDay } guard let focusedId = focusStore.focusedEventId?.rawValue, let event = loadedEvents.first(where: { $0.id.rawValue == focusedId }) else { return nil } diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift index b023b60f6..805a3db93 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift @@ -8,6 +8,15 @@ public enum QuickTime { digits.count < maxDigits } + /// Jump-selected or parked-click day column (web `quickTimeFocusedColumnDay`). + public static func quickTimeFocusedColumnDay(activeDayKeys: [String]) -> Date? { + guard activeDayKeys.count == 1, + let key = activeDayKeys.first, + let date = CompassDateParsing.calendarDateInEffectiveTimeZone(key) + else { return nil } + return EffectiveTimeZone.calendar.startOfDay(for: date) + } + public static func quickTimeTargetDay( startOfView: Date, endOfView: Date, diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index 6ae7fa5b8..a45c8e4fa 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -187,13 +187,20 @@ public struct RootView: View { } else { sidebarBody } + QuickTimeStatusLine(model: model) + .padding(.horizontal, 16) + .padding(.bottom, 2) EventJumpStatusLine(model: model) .padding(.horizontal, 16) .padding(.bottom, 2) SidebarSyncStatusLine(model: model) .padding(.horizontal, 16) .padding(.bottom, 4) - ShortcutSidebarFooter(levelsStore: model.levelsStore) + ShortcutSidebarFooter( + levelsStore: model.levelsStore, + isEventFormVisible: model.isEventFormVisible, + suppressTips: model.eventJumpStatusText != nil, + suppressForQuickTime: !model.draftStore.quickTimeDigits.isEmpty) .padding(.horizontal, 16) .padding(.bottom, 8) sidebarActions diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/QuickTimeStatusLine.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/QuickTimeStatusLine.swift new file mode 100644 index 000000000..9d6499b61 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/QuickTimeStatusLine.swift @@ -0,0 +1,35 @@ +import CompassData +import SwiftUI + +/// Web `QuickTimeIndicator`: echoes half-typed grid quick-time digits. +struct QuickTimeStatusLine: View { + @Environment(\.nativeWebTheme) private var theme + @Bindable var model: NativeCalendarRootModel + + var body: some View { + let digits = model.draftStore.quickTimeDigits + if !digits.isEmpty { + HStack(spacing: 4) { + Text("New event at") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + Text(paddedQuickTimeDigits(digits)) + .font(.rubik(11, weight: .medium)) + .foregroundStyle(theme.textColor) + Text("·") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: "Esc") + } + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + .accessibilityIdentifier("compass-native-quick-time-status") + } + } + + private func paddedQuickTimeDigits(_ digits: String) -> String { + var padded = digits + while padded.count < 4 { padded.append("_") } + return padded + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift index 44d1f72a3..3c53bd964 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift @@ -4,6 +4,9 @@ import SwiftUI struct ShortcutSidebarFooter: View { @Environment(\.nativeWebTheme) private var theme @Bindable var levelsStore: LevelsStore + var isEventFormVisible: Bool = false + var suppressTips: Bool = false + var suppressForQuickTime: Bool = false var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -21,7 +24,11 @@ struct ShortcutSidebarFooter: View { } } - if !levelsStore.badgeHidden, let tip = levelsStore.currentTip { + if isEventFormVisible, !levelsStore.tipsMuted, !suppressTips, !suppressForQuickTime { + eventFormSaveHint + } else if !levelsStore.badgeHidden, !suppressTips, !suppressForQuickTime, + let tip = levelsStore.currentTip + { HStack(spacing: 8) { Text("Try \(tip.label)") .font(.rubik(11)) @@ -37,4 +44,17 @@ struct ShortcutSidebarFooter: View { } } } + + private var eventFormSaveHint: some View { + HStack(spacing: 6) { + Text("Enter saves · hold") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: "Mod") + Text("to jump fields") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + } + .accessibilityIdentifier("compass-native-shortcut-tip") + } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextCard.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextCard.swift index ff7b82478..bbde87956 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextCard.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextCard.swift @@ -6,6 +6,7 @@ struct UpNextCard: View { @Environment(\.nativeWebTheme) private var theme @Bindable var model: NativeCalendarRootModel let onOpen: () -> Void + @State private var isHovered = false var body: some View { let state = model.upNextState @@ -49,6 +50,13 @@ struct UpNextCard: View { .accessibilityLabel( "\(snapshot.isCurrentEvent ? "Now" : "Up next"): \(upNext.title). \(countdown).") } + .overlay(alignment: .topTrailing) { + ShortcutHintChip(label: state.conferenceURL != nil ? "V" : "N") + .opacity(isHovered ? 1 : 0) + .animation(.easeOut(duration: 0.15), value: isHovered) + .allowsHitTesting(false) + } + .onHover { isHovered = $0 } .accessibilityIdentifier("compass-native-up-next-card") } else { Text("All clear") diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index abc783b79..0d106ae16 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -338,6 +338,7 @@ public final class TimeGridView: NSView { let palette = themePalette hourGutterView.layer?.backgroundColor = palette.background.cgColor let columnWidth = GridMetrics.gridTimeColumnWidth + let currentHour = EffectiveTimeZone.calendar.component(.hour, from: state.hourLabelDate) if state.hasSecondaryTimeZone, let travelZone = state.timeTravelTimeZone { let mapped = MappedHourLabels.labels( @@ -349,7 +350,8 @@ public final class TimeGridView: NSView { xOffset: 0, width: columnWidth, hourHeight: hourHeight, - palette: palette) + palette: palette, + currentHour: currentHour) } let primaryLabels = MappedHourLabels.labels( @@ -362,7 +364,8 @@ public final class TimeGridView: NSView { xOffset: primaryX, width: columnWidth, hourHeight: hourHeight, - palette: palette) + palette: palette, + currentHour: currentHour) } private func renderHourColumn( @@ -370,13 +373,15 @@ public final class TimeGridView: NSView { xOffset: Double, width: Double, hourHeight: Double, - palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor) + palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor), + currentHour: Int ) { 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) label.font = CompassFont.nsFont(size: 10) - label.textColor = palette.textMuted + let isCurrentHour = hour > 0 && hour == currentHour + label.textColor = isCurrentHour ? themeAccentColor : palette.textMuted label.alignment = .center label.frame = NSRect( x: xOffset + 4, From 80c560f2c7a490bc75c0f170f1ff3c17e5928531 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:51:10 +0000 Subject: [PATCH 04/25] fix(macos): quick-time on jump day, settings hint, onboarding keycap spacing Co-authored-by: Tyler Dane --- .../CommandPaletteActionsTests.swift | 8 ++++++++ .../NativeCalendarRootModelEventJumpTests.swift | 17 +++++++++++++++++ .../Tests/CompassDataTests/QuickTimeTests.swift | 7 +++++++ .../CompassUI/Onboarding/WelcomeModalView.swift | 6 +++--- .../Settings/SettingsWindowOverlay.swift | 12 ++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift index a92377fc3..003257239 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift @@ -107,6 +107,14 @@ final class CommandPaletteActionsTests: XCTestCase { let ids = Set(model.filteredPaletteSections().flatMap(\.items).map(\.id)) XCTAssertTrue(ids.contains("undo")) XCTAssertTrue(ids.contains("redo")) + XCTAssertTrue(ids.contains("toggle-theme")) + } + + func testToggleThemeFlipsStoredTheme() async throws { + let model = try await makeModel() + let initial = model.settingsStore.theme + model.runPaletteCommand(id: "toggle-theme") + XCTAssertNotEqual(model.settingsStore.theme, initial) } func testAboutInvokesNativePresentation() async throws { diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift index 4783c2639..d8702dd40 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift @@ -32,4 +32,21 @@ final class NativeCalendarRootModelEventJumpTests: XCTestCase { model.handleShiftDayJump(letter: "A") XCTAssertEqual(model.eventJumpSelection.announcement, "Saturday selected") } + + func testQuickTimeUsesJumpSelectedDay() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + model.viewStore.view = .week + model.handleShiftDayJump(letter: "M") + model.handleQuickTimeDigit("1") + model.handleQuickTimeDigit("1") + model.commitQuickTimeIfBuffered() + let draft = try XCTUnwrap(model.draftStore.gridDraft) + let start = try XCTUnwrap(draft.schedule.start) + let calendar = EffectiveTimeZone.calendar + XCTAssertEqual(calendar.component(.hour, from: start), 11) + XCTAssertEqual(CompassDateParsing.formatCalendarDay(start), model.eventJumpSelection.activeDayKeys.first) + } } diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift index 10ff7bbc4..2b12ee148 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift @@ -7,6 +7,13 @@ final class QuickTimeTests: XCTestCase { EffectiveTimeZone.identifier = "UTC" } + func testFocusedColumnDayFromJumpSelection() { + let day = QuickTime.quickTimeFocusedColumnDay(activeDayKeys: ["2026-05-18"]) + XCTAssertNotNil(day) + XCTAssertEqual(CompassDateParsing.formatCalendarDay(day!), "2026-05-18") + XCTAssertNil(QuickTime.quickTimeFocusedColumnDay(activeDayKeys: [])) + } + func testResolveNoonFromTwoDigits() { let day = CompassDateParsing.calendarDateInEffectiveTimeZone("2026-05-20")! let start = QuickTime.resolveQuickTimeStart(digits: "12", targetDay: day) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift index 7a3af21cf..8cc198808 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift @@ -160,7 +160,7 @@ struct WelcomeModalView: View { private func primaryButton(title: String, hint: String, action: @escaping () -> Void) -> some View { Button(action: action) { - HStack(spacing: 8) { + HStack(spacing: 6) { Text(title) .font(.rubik(14, weight: .medium)) ShortcutHintChip(label: hint) @@ -176,7 +176,7 @@ struct WelcomeModalView: View { private func secondaryButton(title: String, hint: String, action: @escaping () -> Void) -> some View { Button(action: action) { - HStack(spacing: 8) { + HStack(spacing: 6) { Text(title) .font(.rubik(14, weight: .medium)) ShortcutHintChip(label: hint) @@ -192,7 +192,7 @@ struct WelcomeModalView: View { private func pillButton(title: String, hint: String, action: @escaping () -> Void) -> some View { Button(action: action) { - HStack(spacing: 8) { + HStack(spacing: 6) { Text(title) .font(.rubik(12, weight: .medium)) ShortcutHintChip(label: hint) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index 7cdc64567..076a48711 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -50,6 +50,18 @@ public struct SettingsWindowOverlay: View { if model.settingsHasBilling { navButton("Billing", page: .billing) } + if !model.focusStore.pageJumpHintsVisible { + HStack(spacing: 4) { + Text("Hold") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: "Mod") + Text("to see shortcuts.") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + } + .padding(.top, 4) + } Spacer() Button("Close") { model.closeSettingsIfAllowed() } .buttonStyle(.plain) From 91b2c745e6967420cfdb7daebc688863b629ac87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 01:20:59 +0000 Subject: [PATCH 05/25] fix(macos): event jump mode and quick-time slot stack like web Co-authored-by: Tyler Dane --- .../Compass/NativeGridKeyboardRouter.swift | 37 +-------- .../NativeCalendarRootModel+Draft.swift | 2 + .../NativeCalendarRootModel+EventJump.swift | 79 ++++++++++++++++++- .../CompassData/NativeCalendarRootModel.swift | 24 +++++- .../Stores/EventJumpSelectionStore.swift | 17 +++- ...ativeCalendarRootModelEventJumpTests.swift | 13 +++ .../CompassDataTests/QuickTimeTests.swift | 11 +++ .../CompassKit/Calendar/QuickTime.swift | 69 ++++++++++++++++ .../CompassKit/Layout/QuickTimeSlotHint.swift | 12 +++ .../CompassKit/Layout/TimeGridState.swift | 3 + .../Chips/QuickTimeSlotsOverlay.swift | 35 ++++++++ .../Sources/CompassUI/RootView.swift | 5 ++ 12 files changed, 264 insertions(+), 43 deletions(-) create mode 100644 apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/QuickTimeSlotHint.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/QuickTimeSlotsOverlay.swift diff --git a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift index 1164c8961..62c5f33b4 100644 --- a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift +++ b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift @@ -9,7 +9,6 @@ final class NativeGridKeyboardRouter { private let model: NativeCalendarRootModel private let modHold: ModHoldDetector private let formModHold: ModHoldDetector - private let eventJumpHold: HoldModifierDetector private let viewSwitchIds: Set = [.navDayView, .navWeekView, .navLifeView] init(model: NativeCalendarRootModel, registry: ShortcutRegistry) { @@ -25,12 +24,6 @@ final class NativeGridKeyboardRouter { model?.jumpEventFormField(digit: digit) } } - eventJumpHold = HoldModifierDetectorFactory.eventJumpHold { [weak model] digit in - Task { @MainActor in - model?.focusEventJump(digit: digit) - } - } - let focusIds: Set = [ .editFocusPrev, .editFocusNext, @@ -347,26 +340,10 @@ final class NativeGridKeyboardRouter { return false } - if case .character(let char) = keyEvent.key, char == "f", keyEvent.modifiers.isEmpty { - eventJumpHold.handleModifierDown(isRepeat: keyEvent.isRepeat) - if !keyEvent.isRepeat { - Task { @MainActor [weak self] in - try? await Task.sleep(for: .milliseconds(280)) - self?.syncEventJumpHints() - } - } - } - - if eventJumpHold.handleKeyDown(keyEvent) { - syncEventJumpHints() - return true - } if modHold.handleKeyDown(keyEvent) { return true } - syncEventJumpHints() - if keyEvent.modifiers == .shift, case .character(let letter) = keyEvent.key { model.handleShiftDayJump(letter: letter) return true @@ -534,6 +511,9 @@ final class NativeGridKeyboardRouter { } if keyEvent.modifiers.isEmpty, case .character(let digit) = keyEvent.key, digit.isNumber { + if model.eventJumpSelection.modeActive, model.focusEventJumpIfAvailable(digit: digit) { + return true + } model.handleQuickTimeDigit(digit) if !model.draftStore.quickTimeDigits.isEmpty { return true @@ -798,17 +778,6 @@ final class NativeGridKeyboardRouter { return false } - func handleKeyUp(_ event: NSEvent) { - if event.keyCode == 4 { - eventJumpHold.handleModifierUp() - model.setEventJumpHintsVisible(false) - } - } - - private func syncEventJumpHints() { - model.setEventJumpHintsVisible(eventJumpHold.phase == .hintsVisible) - } - private func performUndo() { if model.draftStore.gridDraft != nil, model.draftStore.status.activity == .keyboardPlace diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift index 0288ac7e0..4395ff13f 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+Draft.swift @@ -118,6 +118,7 @@ extension NativeCalendarRootModel { public func handleQuickTimeDigit(_ digit: Character) { guard viewStore.view != .life, draftStore.gridDraft == nil else { return } + guard eventJumpSelection.modeActive || eventJumpSelection.isActive else { return } guard digit.isNumber else { return } let next = draftStore.quickTimeDigits + String(digit) @@ -150,6 +151,7 @@ extension NativeCalendarRootModel { calendarId: defaultTargetCalendarId() ) draftStore.startGridDraft(activity: .keyboardPlace, draft: draft) + clearEventJumpSelection() rebuildPresentation() focusDraftCard() } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift index d2216f8bc..8b119f0f8 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift @@ -6,6 +6,13 @@ extension NativeCalendarRootModel { public var eventJumpStatusText: String? { guard eventJumpSelection.isActive else { return nil } + if eventJumpSelection.modeActive, + eventJumpSelection.activeDayKeys.isEmpty, + eventJumpSelection.dayJumpBuffer.isEmpty, + eventJumpSelection.announcement == nil + { + return "Event jump on · Esc" + } if eventJumpSelection.dayJumpBuffer == "s", eventJumpSelection.activeDayKeys.isEmpty { return "Shift+U Sunday · Shift+A Saturday · Esc" } @@ -18,8 +25,33 @@ extension NativeCalendarRootModel { return "Event jump · Esc" } + public func toggleEventJumpMode() { + if eventJumpSelection.modeActive { + clearEventJumpSelection() + draftStore.setQuickTimeDigits("") + return + } + eventJumpSelection.setModeActive(true) + recordShortcut(.focusShiftHold) + refreshEventJumpPresentation() + } + + func refreshEventJumpPresentation() { + if eventJumpSelection.modeActive { + eventJumpHintLabels = buildEventJumpHints() + quickTimeSlotHintLabels = buildQuickTimeSlotHints() + } else { + eventJumpHintLabels = [] + quickTimeSlotHintLabels = [] + } + applyFocusPresentation() + } + public func handleShiftDayJump(letter: Character) { guard viewStore.view == .week || viewStore.view == .day else { return } + if !eventJumpSelection.modeActive { + eventJumpSelection.setModeActive(true) + } let lower = String(letter).lowercased() if eventJumpSelection.dayJumpBuffer == "s" { @@ -36,7 +68,7 @@ extension NativeCalendarRootModel { if lower == "s" { eventJumpSelection.setWeekendDisambiguation(announcement: "Weekend") recordShortcut(.focusWeekDay) - applyFocusPresentation() + refreshEventJumpPresentation() return } @@ -62,16 +94,59 @@ extension NativeCalendarRootModel { }) { focusEvent(eventId: first.id.rawValue) } - applyFocusPresentation() + refreshEventJumpPresentation() return } } public func clearEventJumpSelection() { eventJumpSelection.clear() + eventJumpHintLabels = [] + quickTimeSlotHintLabels = [] applyFocusPresentation() } + func buildQuickTimeSlotHints() -> [QuickTimeSlotHint] { + guard eventJumpSelection.modeActive else { return [] } + let focusedDay = QuickTime.quickTimeFocusedColumnDay( + activeDayKeys: eventJumpSelection.activeDayKeys) + let targetDay = QuickTime.quickTimeTargetDay( + startOfView: startOfView, + endOfView: endOfView, + now: referenceNow, + focusedDay: focusedDay + ) + let timedStarts: [Date] = loadedEvents.compactMap { event in + guard case .timed(let timed) = event.schedule else { return nil } + return CompassDateParsing.parseInEffectiveTimeZone(timed.start.rawValue) + } + let timedEnds: [Date] = loadedEvents.compactMap { event in + guard case .timed(let timed) = event.schedule else { return nil } + return CompassDateParsing.parseInEffectiveTimeZone(timed.end.rawValue) + } + let busy = QuickTime.timedEventsToBusyIntervals(startDates: timedStarts, endDates: timedEnds) + let slots = QuickTime.buildQuickTimeSlots(busy: busy, targetDay: targetDay) + let visibleDates = GridVisibleDate.fromKeys(timeGridState.scenario.visibleDateKeys) + let colWidths = timeGridState.resolvedColumnWidths() + let metrics = timeGridState.snapshot(colWidths: colWidths).metrics + let columnIndex: Int? = { + let key = CompassDateParsing.formatCalendarDay(targetDay) + return visibleDates.firstIndex(where: { $0.key == key }) + }() + return slots.compactMap { slot in + let frame = EventPositionCalculator.getBusyPeriodPosition( + segmentStart: slot.start, + segmentEnd: slot.end, + measurements: metrics, + visibleDates: visibleDates, + columnIndex: columnIndex, + hasSecondaryTimeZone: timeGridState.hasSecondaryTimeZone + ) + guard frame.width > 0, frame.height > 0 else { return nil } + return QuickTimeSlotHint(sequence: slot.sequence, frame: frame) + } + } + private func eventJumpDayKey(for event: Event) -> String { switch event.schedule { case .timed(let timed): diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 702c424eb..a2f3243f1 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -114,7 +114,8 @@ public final class NativeCalendarRootModel { } private var refreshTask: Task? private var focusLayoutCards: [FocusLayoutCard] = [] - private var eventJumpHintLabels: [EventJumpChipHint] = [] + var eventJumpHintLabels: [EventJumpChipHint] = [] + var quickTimeSlotHintLabels: [QuickTimeSlotHint] = [] public var focusedDayColumnCalendarId: String? public var pendingFocusDayColumnCalendarId: String? private var didApplyDemoFixtureScroll = false @@ -616,6 +617,8 @@ public final class NativeCalendarRootModel { focusFirstCalendarEvent() case .focusNotice: focusNextNotice() + case .focusShiftHold: + toggleEventJumpMode() case .otherSubscribe: openUpgradeConfirmation() case .navGoToDate: @@ -643,7 +646,7 @@ public final class NativeCalendarRootModel { public func setEventJumpHintsVisible(_ visible: Bool) { if visible { eventJumpHintLabels = buildEventJumpHints() - } else { + } else if !eventJumpSelection.modeActive { eventJumpHintLabels = [] } applyFocusPresentation() @@ -724,6 +727,15 @@ public final class NativeCalendarRootModel { focusEvent(eventId: hint.eventId) } + @discardableResult + public func focusEventJumpIfAvailable(digit: Character) -> Bool { + guard let hint = eventJumpHintLabels.first(where: { $0.label == String(digit) }) else { + return false + } + focusEvent(eventId: hint.eventId) + return true + } + public func consumePendingScroll() -> TimeGridScrollRequest? { defer { pendingScroll = nil } return pendingScroll @@ -854,6 +866,7 @@ public final class NativeCalendarRootModel { focusedEventId: focusStore.focusedEventId?.rawValue, sidebarEditingEventId: sidebarEditingGridEventId(), eventJumpHints: eventJumpHintLabels, + quickTimeSlotHints: quickTimeSlotHintLabels, focusedDayColumnCalendarId: focusedDayColumnCalendarId, pageJumpHintsVisible: focusStore.pageJumpHintsVisible, pageJumpDigitByCalendarId: pageJumpDigitByCalendarId(), @@ -1009,6 +1022,7 @@ public final class NativeCalendarRootModel { focusedEventId: focusStore.focusedEventId?.rawValue, sidebarEditingEventId: sidebarEditingGridEventId(), eventJumpHints: eventJumpHintLabels, + quickTimeSlotHints: quickTimeSlotHintLabels, focusedDayColumnCalendarId: focusedDayColumnCalendarId, pageJumpHintsVisible: focusStore.pageJumpHintsVisible, pageJumpDigitByCalendarId: pageJumpDigitByCalendarId(), @@ -1016,6 +1030,10 @@ public final class NativeCalendarRootModel { effectiveTimeZone: viewStore.effectiveTimeZone, timeTravelTimeZone: viewStore.timeTravelTimeZone ) + if eventJumpSelection.modeActive { + quickTimeSlotHintLabels = buildQuickTimeSlotHints() + timeGridState.quickTimeSlotHints = quickTimeSlotHintLabels + } let snapshot = timeGridState.snapshot(colWidths: timeGridState.resolvedColumnWidths()) syncFocusRegistry(from: snapshot.cards) focusLayoutCards = snapshot.cards.map { card in @@ -1115,7 +1133,7 @@ public final class NativeCalendarRootModel { ) } - private func buildEventJumpHints() -> [EventJumpChipHint] { + func buildEventJumpHints() -> [EventJumpChipHint] { let digits = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let timed = focusLayoutCards.filter { !$0.isAllDay }.sorted { $0.frame.top < $1.frame.top } return timed.prefix(digits.count).enumerated().map { index, card in diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/EventJumpSelectionStore.swift b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/EventJumpSelectionStore.swift index f98cfe027..c50f39da8 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/Stores/EventJumpSelectionStore.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/Stores/EventJumpSelectionStore.swift @@ -3,13 +3,15 @@ import Foundation @MainActor @Observable public final class EventJumpSelectionStore { + /// Web `event-jump.store` `isActive`: bare `f` toggles jump mode for quick-time slots. + public private(set) var modeActive: Bool = false public private(set) var activeDayKeys: [String] = [] public private(set) var announcement: String? /// Typed day-prefix buffer while resolving weekend Shift+S → U/A (web `matchDayJumpKeystroke`). public private(set) var dayJumpBuffer: String = "" public var isActive: Bool { - !activeDayKeys.isEmpty || announcement != nil || !dayJumpBuffer.isEmpty + modeActive || !activeDayKeys.isEmpty || announcement != nil || !dayJumpBuffer.isEmpty } public func selectDay(key: String, announcement: String, buffer: String) { @@ -24,9 +26,16 @@ public final class EventJumpSelectionStore { dayJumpBuffer = "s" } + public func setModeActive(_ active: Bool) { + modeActive = active + if !active { + activeDayKeys = [] + announcement = nil + dayJumpBuffer = "" + } + } + public func clear() { - activeDayKeys = [] - announcement = nil - dayJumpBuffer = "" + setModeActive(false) } } diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift index d8702dd40..a838d2efa 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelEventJumpTests.swift @@ -4,6 +4,19 @@ import XCTest @MainActor final class NativeCalendarRootModelEventJumpTests: XCTestCase { + func testToggleEventJumpModeShowsQuickTimeSlots() async throws { + let model = try await NativeCalendarRootModel.makeForUnitTests() + EffectiveTimeZone.identifier = "UTC" + model.goToDate(CompassDateParsing.calendarDateInEffectiveTimeZone("2026-05-18")!) + XCTAssertFalse(model.eventJumpSelection.modeActive) + model.toggleEventJumpMode() + XCTAssertTrue(model.eventJumpSelection.modeActive) + XCTAssertFalse(model.timeGridState.quickTimeSlotHints.isEmpty) + model.toggleEventJumpMode() + XCTAssertFalse(model.eventJumpSelection.modeActive) + XCTAssertTrue(model.timeGridState.quickTimeSlotHints.isEmpty) + } + func testShiftDayJumpSelectsMondayColumn() async throws { let fixture = try DemoSeedFixture.load() let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift index 2b12ee148..8cc2ea3f5 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/QuickTimeTests.swift @@ -31,4 +31,15 @@ final class QuickTimeTests: XCTestCase { XCTAssertEqual(calendar.component(.hour, from: start!), 11) XCTAssertEqual(calendar.component(.minute, from: start!), 30) } + + func testBuildQuickTimeSlotsSkipsBusyHour() { + let day = CompassDateParsing.calendarDateInEffectiveTimeZone("2026-05-20")! + let calendar = EffectiveTimeZone.calendar + let busyStart = calendar.date(bySettingHour: 11, minute: 0, second: 0, of: day)! + let busyEnd = calendar.date(byAdding: .hour, value: 1, to: busyStart)! + let busy = QuickTime.timedEventsToBusyIntervals(startDates: [busyStart], endDates: [busyEnd]) + let slots = QuickTime.buildQuickTimeSlots(busy: busy, targetDay: day) + XCTAssertFalse(slots.contains(where: { $0.sequence == "1100" })) + XCTAssertTrue(slots.contains(where: { $0.sequence == "1200" })) + } } diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift index 805a3db93..7dd5a7faa 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/QuickTime.swift @@ -1,5 +1,27 @@ import Foundation +public struct QuickTimeBusyInterval: Hashable, Sendable { + public var startMs: TimeInterval + public var endMs: TimeInterval + + public init(startMs: TimeInterval, endMs: TimeInterval) { + self.startMs = startMs + self.endMs = endMs + } +} + +public struct QuickTimeSlot: Hashable, Sendable { + public var start: String + public var end: String + public var sequence: String + + public init(start: String, end: String, sequence: String) { + self.start = start + self.end = end + self.sequence = sequence + } +} + /// Mirrors `apps/calendar-web/src/shortcuts/quick-time/quick-time.util.ts`. public enum QuickTime { public static let maxDigits = 4 @@ -33,6 +55,53 @@ public enum QuickTime { return calendar.startOfDay(for: startOfView) } + public static func quickTimeSequenceForHour(_ hour: Int) -> String? { + guard hour != 0 else { return nil } + return String(format: "%02d00", hour) + } + + public static func timedEventsToBusyIntervals( + startDates: [Date], + endDates: [Date] + ) -> [QuickTimeBusyInterval] { + zip(startDates, endDates).map { start, end in + QuickTimeBusyInterval(startMs: start.timeIntervalSince1970, endMs: end.timeIntervalSince1970) + } + } + + public static func buildQuickTimeSlots( + busy: [QuickTimeBusyInterval], + targetDay: Date + ) -> [QuickTimeSlot] { + let calendar = EffectiveTimeZone.calendar + let dayStart = calendar.startOfDay(for: targetDay) + var slots: [QuickTimeSlot] = [] + + for hour in 1 ..< 24 { + guard let sequence = quickTimeSequenceForHour(hour), + let start = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: dayStart), + let end = calendar.date(byAdding: .hour, value: 1, to: start) + else { continue } + + let startMs = start.timeIntervalSince1970 + let endMs = end.timeIntervalSince1970 + let isTaken = busy.contains { period in + period.startMs < endMs && period.endMs > startMs + } + if isTaken { continue } + + slots.append( + QuickTimeSlot( + start: CompassDateParsing.formatLikeDayjs(start), + end: CompassDateParsing.formatLikeDayjs(end), + sequence: sequence + ) + ) + } + + return slots + } + public static func resolveQuickTimeStart(digits: String, targetDay: Date) -> Date? { guard digits.allSatisfy(\.isNumber), !digits.isEmpty, digits.count <= maxDigits else { return nil diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/QuickTimeSlotHint.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/QuickTimeSlotHint.swift new file mode 100644 index 000000000..a24a6b52a --- /dev/null +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/QuickTimeSlotHint.swift @@ -0,0 +1,12 @@ +import Foundation + +public struct QuickTimeSlotHint: Hashable, Sendable, Identifiable { + public var id: String { sequence } + public var sequence: String + public var frame: EventPosition + + public init(sequence: String, frame: EventPosition) { + self.sequence = sequence + self.frame = frame + } +} diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/TimeGridState.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/TimeGridState.swift index 3f183fa9f..28d019855 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/TimeGridState.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/TimeGridState.swift @@ -9,6 +9,7 @@ public struct TimeGridState: Sendable { /// Grid card whose event is open in the sidebar form (keyboard focus may be elsewhere). public var sidebarEditingEventId: String? public var eventJumpHints: [EventJumpChipHint] + public var quickTimeSlotHints: [QuickTimeSlotHint] public var focusedDayColumnCalendarId: String? public var pageJumpHintsVisible: Bool public var pageJumpDigitByCalendarId: [String: String] @@ -24,6 +25,7 @@ public struct TimeGridState: Sendable { focusedEventId: String? = nil, sidebarEditingEventId: String? = nil, eventJumpHints: [EventJumpChipHint] = [], + quickTimeSlotHints: [QuickTimeSlotHint] = [], focusedDayColumnCalendarId: String? = nil, pageJumpHintsVisible: Bool = false, pageJumpDigitByCalendarId: [String: String] = [:], @@ -38,6 +40,7 @@ public struct TimeGridState: Sendable { self.focusedEventId = focusedEventId self.sidebarEditingEventId = sidebarEditingEventId self.eventJumpHints = eventJumpHints + self.quickTimeSlotHints = quickTimeSlotHints self.focusedDayColumnCalendarId = focusedDayColumnCalendarId self.pageJumpHintsVisible = pageJumpHintsVisible self.pageJumpDigitByCalendarId = pageJumpDigitByCalendarId diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/QuickTimeSlotsOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/QuickTimeSlotsOverlay.swift new file mode 100644 index 000000000..4d036d1c1 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Chips/QuickTimeSlotsOverlay.swift @@ -0,0 +1,35 @@ +import CompassKit +import SwiftUI + +/// Web `QuickTimeSlots`: dashed hour placeholders while event-jump mode is on. +struct QuickTimeSlotsOverlay: View { + @Environment(\.nativeWebTheme) private var theme + let hints: [QuickTimeSlotHint] + let gridYOffset: CGFloat + let visible: Bool + + var body: some View { + if visible, !hints.isEmpty { + GeometryReader { _ in + ForEach(hints) { hint in + RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault) + .strokeBorder( + theme.borderStrongColor, + style: StrokeStyle(lineWidth: 1, dash: [4, 3]) + ) + .frame(width: hint.frame.width, height: hint.frame.height) + .overlay(alignment: .topTrailing) { + ShortcutHintChip(label: hint.sequence) + .padding(2) + } + .position( + x: hint.frame.left + hint.frame.width / 2, + y: gridYOffset + hint.frame.top + hint.frame.height / 2 + ) + } + } + .allowsHitTesting(false) + .accessibilityIdentifier("compass-quick-time-slots") + } + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index a45c8e4fa..fd45081bc 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -347,6 +347,11 @@ public struct RootView: View { gridYOffset: gridChipYOffset, visible: !model.timeGridState.eventJumpHints.isEmpty ) + QuickTimeSlotsOverlay( + hints: model.timeGridState.quickTimeSlotHints, + gridYOffset: gridChipYOffset, + visible: model.eventJumpSelection.modeActive + ) } .background(theme.backgroundColor) .accessibilityIdentifier("compass-native-content") From b19173544b42af90fbd98beb0933446a3a6f846c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 01:42:15 +0000 Subject: [PATCH 06/25] fix(macos): keep parity week gutter and Sunday window pinned Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 15 ++++- ...ndarRootModelParityPresentationTests.swift | 59 +++++++++++++++++++ .../CompassUI/TimeGrid/TimeGridView.swift | 20 +++++++ .../NativeParityCaptureTests.swift | 12 ++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 21460f7e9..869cd6118 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -219,7 +219,11 @@ public final class NativeCalendarRootModel { overlayStores.legend.onOpenChanged = { [weak self] open in self?.onShortcutsLegendAccessibilityProbeChanged?(open) } - rebuildPresentation() + if UITestLaunchPolicy.isParityCapture { + applyParityCapturePresentationIfNeeded() + } else { + rebuildPresentation() + } } public func closeSettingsIfAllowed() { @@ -919,7 +923,7 @@ public final class NativeCalendarRootModel { let calendar = EffectiveTimeZone.calendar if viewStore.visibleDayCount == 1 { viewStore.setAnchorDate(today) - } else { + } else if !isReferenceDayInVisibleWindow(today) { viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) } monthPickerMonth = today @@ -928,6 +932,13 @@ public final class NativeCalendarRootModel { pendingScroll = .scrollToNowOrPulse } + /// Matches web `useWeek` `isCurrentWeek` before `goToToday` moves the anchor. + private func isReferenceDayInVisibleWindow(_ day: Date) -> Bool { + let calendar = EffectiveTimeZone.calendar + let dayStart = calendar.startOfDay(for: day) + return dayStart >= startOfView && dayStart <= endOfView + } + private func shiftMonth(by months: Int) { let calendar = EffectiveTimeZone.calendar if let next = calendar.date(byAdding: .month, value: months, to: monthPickerMonth) { diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift new file mode 100644 index 000000000..5807adde9 --- /dev/null +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift @@ -0,0 +1,59 @@ +import CompassData +import CompassKit +import XCTest + +@MainActor +final class NativeCalendarRootModelParityPresentationTests: XCTestCase { + private var priorParityEnv: String? + + override func setUp() { + super.setUp() + priorParityEnv = ProcessInfo.processInfo.environment["PARITY_CAPTURE"] + setenv("PARITY_CAPTURE", "1", 1) + } + + override func tearDown() { + if let priorParityEnv { + setenv("PARITY_CAPTURE", priorParityEnv, 1) + } else { + unsetenv("PARITY_CAPTURE") + } + super.tearDown() + } + + func testParityCaptureUsesDemoSundayWeekAndFiveColumns() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + + XCTAssertEqual( + CompassDateParsing.formatCalendarDay(model.viewStore.anchorDate), + "2026-06-07" + ) + XCTAssertEqual(model.viewStore.visibleDayCount, ParityCaptureGeometry.visibleDayCount) + XCTAssertEqual( + Double(model.contentTrackWidth), + ParityCaptureGeometry.contentTrackWidth, + accuracy: 0.5 + ) + XCTAssertEqual( + model.timeGridState.scenario.visibleDateKeys.first, + "2026-06-07" + ) + XCTAssertEqual(model.timeGridState.scenario.visibleDateKeys.count, 5) + } + + func testGoToTodayDoesNotShiftAnchorWhenDemoDayAlreadyVisible() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + model.applyParityCapturePresentationIfNeeded() + let anchorBefore = model.viewStore.anchorDate + + model.handleShortcut(.navToday) + + XCTAssertEqual(model.viewStore.anchorDate, anchorBefore) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index 12ab7ee36..00000928a 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -263,9 +263,27 @@ public final class TimeGridView: NSView { } private func syncPinnedHeaderScrollX() { + guard state.layoutMode == .day else { + pinnedHeaderView.setBoundsOrigin(.zero) + return + } pinnedHeaderView.setBoundsOrigin(NSPoint(x: scrollView.contentView.bounds.origin.x, y: 0)) } + /// Week view keeps the timezone header and hour gutter pinned at x=0. + private func resetWeekHorizontalScrollIfNeeded() { + guard state.layoutMode == .week else { return } + var origin = scrollView.contentView.bounds.origin + guard origin.x != 0 else { + pinnedHeaderView.setBoundsOrigin(.zero) + return + } + origin.x = 0 + scrollView.contentView.scroll(to: origin) + scrollView.reflectScrolledClipView(scrollView.contentView) + pinnedHeaderView.setBoundsOrigin(.zero) + } + private func relayout() { let colWidths = state.resolvedColumnWidths() let snapshot = state.snapshot(colWidths: colWidths) @@ -305,6 +323,8 @@ public final class TimeGridView: NSView { renderTimedGrid(snapshot: snapshot, hourHeight: hourHeight, gridHeight: gridHeight) renderCards(snapshot: snapshot) updateNowLine() + resetWeekHorizontalScrollIfNeeded() + syncPinnedHeaderScrollX() } private func renderHourGutter( diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 0075506e9..e7d25a5a5 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -113,7 +113,19 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( waitForGridColumnCount(window, atLeast: 5, timeout: 20), "Expected five day columns in parity week grid") + let timedGrid = window.descendants(matching: .any)["compass-grid-timed"] + if timedGrid.waitForExistence(timeout: 5) { + timedGrid.click() + } window.typeKey("t", modifierFlags: []) + let sundayColumn = window.descendants(matching: .any)["compass-grid-column-2026-06-07"] + XCTAssertTrue( + sundayColumn.waitForExistence(timeout: 20), + "Expected Sunday demo week column (2026-06-07) after nav today") + XCTAssertTrue( + window.staticTexts["Calendar timezone: UTC"].waitForExistence(timeout: 5) + || window.buttons["Calendar timezone: UTC"].waitForExistence(timeout: 5), + "Expected UTC timezone header on parity week grid") XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") From 5545c50a66e772564c02b8583f26e961a108aafc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 01:48:42 +0000 Subject: [PATCH 07/25] fix(macos): keep now-cue on nav today when anchor unchanged Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 14 +++++++++++--- ...eCalendarRootModelParityPresentationTests.swift | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 869cd6118..3c256f9c7 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -921,14 +921,22 @@ public final class NativeCalendarRootModel { private func goToToday() { let today = referenceNow let calendar = EffectiveTimeZone.calendar + var anchorChanged = false if viewStore.visibleDayCount == 1 { - viewStore.setAnchorDate(today) + if !calendar.isDate(viewStore.anchorDate, inSameDayAs: today) { + viewStore.setAnchorDate(today) + anchorChanged = true + } } else if !isReferenceDayInVisibleWindow(today) { viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) + anchorChanged = true } monthPickerMonth = today - rebuildPresentation() - scheduleRefreshVisibleRange() + if anchorChanged { + rebuildPresentation() + scheduleRefreshVisibleRange() + } + // Web `goToTodayInWeek` always runs scroll-to-now / now-cue even when the anchor is unchanged. pendingScroll = .scrollToNowOrPulse } diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift index 5807adde9..b8dcbb675 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift @@ -55,5 +55,6 @@ final class NativeCalendarRootModelParityPresentationTests: XCTestCase { model.handleShortcut(.navToday) XCTAssertEqual(model.viewStore.anchorDate, anchorBefore) + XCTAssertEqual(model.consumePendingScroll(), .scrollToNowOrPulse) } } From d76881274da640a6f6c341e94d99f739d1ca08d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:12:10 +0000 Subject: [PATCH 08/25] fix(macos): match web Enter keycap in palette footer Co-authored-by: Tyler Dane --- .../CompassUI/CommandPalette/CommandPaletteOverlay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift index 520870b3c..e60a717b4 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift @@ -164,7 +164,7 @@ public struct CommandPaletteOverlay: View { Text("Navigate") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) - footerKeycap("↩") + footerKeycap("Enter") Text("Select") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) From 1773277360798f093c503beee3cdf90a34844174 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:29:52 +0000 Subject: [PATCH 09/25] fix(macos): expose grid timezone header for parity capture Co-authored-by: Tyler Dane --- .../Sources/CompassUI/TimeGrid/TimeGridView.swift | 9 +++++++-- .../CompassUITests/NativeParityCaptureTests.swift | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index 00000928a..c8e879a65 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -713,7 +713,8 @@ public final class TimeGridView: NSView { width: state.hasSecondaryTimeZone ? columnWidth : snapshot.metrics.marginLeft, headerHeight: headerHeight, palette: palette, - accessibilityLabel: "Calendar timezone: \(effectiveAbbrev)") + accessibilityLabel: "Calendar timezone: \(effectiveAbbrev)", + accessibilityIdentifier: "compass-grid-timezone-effective") } private func addTimezoneButton( @@ -722,7 +723,8 @@ public final class TimeGridView: NSView { width: Double, headerHeight: Double, palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor), - accessibilityLabel: String + accessibilityLabel: String, + accessibilityIdentifier: String? = nil ) { let button = NSButton(title: title, target: self, action: #selector(openTimeTravel(_:))) button.isBordered = false @@ -730,6 +732,9 @@ public final class TimeGridView: NSView { button.contentTintColor = palette.textMuted button.frame = NSRect(x: x, y: 4, width: width, height: headerHeight - 8) button.setAccessibilityLabel(accessibilityLabel) + if let accessibilityIdentifier { + button.setAccessibilityIdentifier(accessibilityIdentifier) + } headerRowView.addSubview(button) } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index e7d25a5a5..6808ef256 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -122,10 +122,13 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( sundayColumn.waitForExistence(timeout: 20), "Expected Sunday demo week column (2026-06-07) after nav today") + let timezoneHeader = window.descendants(matching: .any)["compass-grid-timezone-effective"] XCTAssertTrue( - window.staticTexts["Calendar timezone: UTC"].waitForExistence(timeout: 5) - || window.buttons["Calendar timezone: UTC"].waitForExistence(timeout: 5), + timezoneHeader.waitForExistence(timeout: 10), "Expected UTC timezone header on parity week grid") + XCTAssertTrue( + timezoneHeader.label.contains("UTC"), + "Expected effective timezone label to include UTC, got: \(timezoneHeader.label)") XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") From bb943f151f357ca4285feb1dcbc6a1465baff78d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:33:29 +0000 Subject: [PATCH 10/25] fix(macos): align demo settings and welcome density with web parity Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 11 ++++++--- .../Onboarding/WelcomeModalView.swift | 4 ++-- .../SettingsUnsignedAccountsSectionView.swift | 23 +++++++++++++++++++ .../Settings/SettingsWindowOverlay.swift | 14 ++++++----- 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 3c256f9c7..072e97e57 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -158,8 +158,12 @@ public final class NativeCalendarRootModel { self.demoPresentation = demoPresentation self.now = now self.overlayStores = overlayStores - let pinnedTimeZone = demoPresentation?.timeZone ?? CompassDevicePreferences.readPinnedTimeZone() - EffectiveTimeZone.identifier = pinnedTimeZone ?? TimeZone.current.identifier + // Demo matches web parity: effective UTC via browser timezone, not a pinned override. + let demoTimeZone = demoPresentation?.timeZone + let pinnedTimeZone = + demoPresentation == nil ? CompassDevicePreferences.readPinnedTimeZone() : nil + EffectiveTimeZone.identifier = + demoTimeZone ?? pinnedTimeZone ?? TimeZone.current.identifier eventsStore = environment.eventsStore hiddenEventsStore = environment.hiddenEventsStore calendarVisibilityStore = CalendarVisibilityStore() @@ -180,7 +184,8 @@ public final class NativeCalendarRootModel { view: .week, anchorDate: anchor, visibleDayCount: CalendarWindowMath.weekDayCount, - pinnedTimeZone: pinnedTimeZone + pinnedTimeZone: pinnedTimeZone, + browserTimeZone: demoTimeZone ?? TimeZone.current.identifier ) if demoPresentation == nil { viewStore.setTimeTravelTimeZone(CompassDevicePreferences.readTimeTravelTimeZone()) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift index 8cc198808..4bcbaa70b 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift @@ -32,7 +32,7 @@ struct WelcomeModalView: View { } private var panel: some View { - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 20) { headerRow stepContent stepDots @@ -40,7 +40,7 @@ struct WelcomeModalView: View { WelcomeLinksView() } } - .padding(32) + .padding(28) .frame(width: 480) .background(theme.surfacePanelColor) .clipShape(RoundedRectangle(cornerRadius: 12)) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift new file mode 100644 index 000000000..7de064e58 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +/// Web `AccountsSection` empty state for signed-out settings. +struct SettingsUnsignedAccountsSectionView: View { + @Environment(\.nativeWebTheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Accounts") + .font(.rubik(15)) + .foregroundStyle(theme.textColor) + Text("No accounts connected yet.") + .font(.rubik(13)) + .foregroundStyle(theme.textMutedColor) + Text( + "Pick Google, Microsoft, or Apple. Or keep using this device only from the calendar list." + ) + .font(.rubik(13)) + .foregroundStyle(theme.textMutedColor) + } + .accessibilityIdentifier("settings-section-accounts") + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index 076a48711..8a17d9902 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -90,7 +90,7 @@ public struct SettingsWindowOverlay: View { private var content: some View { VStack(alignment: .leading, spacing: 0) { ScrollView { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 12) { if model.settingsStore.page == .billing { billingContent } else if model.settingsStore.page == .booking { @@ -99,12 +99,12 @@ public struct SettingsWindowOverlay: View { accountsContent } } - .padding(20) + .padding(16) } if model.settingsStore.page == .accounts { Divider().overlay(theme.borderColor) SettingsDataActionsSectionView(model: model) - .padding(20) + .padding(16) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -119,11 +119,13 @@ public struct SettingsWindowOverlay: View { settingsStore: model.settingsStore, calendars: model.calendars, hasConnectedAccount: !model.syncConnectionsStore.connections.isEmpty) - SettingsThemeSectionView(settingsStore: model.settingsStore) - SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore) - SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore) if model.isSignedIn { + SettingsThemeSectionView(settingsStore: model.settingsStore) + SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore) + SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore) SettingsAccountsSectionView(syncStore: model.syncConnectionsStore) + } else { + SettingsUnsignedAccountsSectionView() } } } From 9ba782a3efafdea5a2fff2670b316f38283665a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:45:22 +0000 Subject: [PATCH 11/25] test(macos): avoid internal parity helper in package tests Co-authored-by: Tyler Dane --- .../NativeCalendarRootModelParityPresentationTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift index b8dcbb675..68325a415 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift @@ -49,7 +49,6 @@ final class NativeCalendarRootModelParityPresentationTests: XCTestCase { let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) await model.start() - model.applyParityCapturePresentationIfNeeded() let anchorBefore = model.viewStore.anchorDate model.handleShortcut(.navToday) From 9dca9d37ec35b00a4b8a69aa5b6dc6a5e82a51eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:47:29 +0000 Subject: [PATCH 12/25] fix(macos): clamp week grid horizontal scroll for gutter parity Co-authored-by: Tyler Dane --- .../CompassUI/TimeGrid/TimeGridView.swift | 16 ++++++++-------- .../NativeParityCaptureTests.swift | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index c8e879a65..5d825911c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -146,6 +146,7 @@ public final class TimeGridView: NSView { } private func scrollToNowOrPulse() { + clampWeekHorizontalScrollOrigin() guard let snapshot, let nowLine = snapshot.nowLine else { return } // The scroll document is the timed grid alone; the header is pinned above it. let targetY = clampedScrollY(GridScrollMath.scrollToNowDocumentY( @@ -159,6 +160,8 @@ public final class TimeGridView: NSView { origin.y = targetY scrollView.contentView.scroll(to: origin) scrollView.reflectScrolledClipView(scrollView.contentView) + clampWeekHorizontalScrollOrigin() + syncPinnedHeaderScrollX() if nowLineVisible { pulseNowCue() } } @@ -259,6 +262,7 @@ public final class TimeGridView: NSView { /// Day view scrolls horizontally; the pinned header follows the clip view's x. @objc private func clipViewBoundsDidChange(_ notification: Notification) { + clampWeekHorizontalScrollOrigin() syncPinnedHeaderScrollX() } @@ -270,18 +274,14 @@ public final class TimeGridView: NSView { pinnedHeaderView.setBoundsOrigin(NSPoint(x: scrollView.contentView.bounds.origin.x, y: 0)) } - /// Week view keeps the timezone header and hour gutter pinned at x=0. - private func resetWeekHorizontalScrollIfNeeded() { + /// Week view never scrolls horizontally; a non-zero x clips the gutter and first column. + private func clampWeekHorizontalScrollOrigin() { guard state.layoutMode == .week else { return } var origin = scrollView.contentView.bounds.origin - guard origin.x != 0 else { - pinnedHeaderView.setBoundsOrigin(.zero) - return - } + guard origin.x != 0 else { return } origin.x = 0 scrollView.contentView.scroll(to: origin) scrollView.reflectScrolledClipView(scrollView.contentView) - pinnedHeaderView.setBoundsOrigin(.zero) } private func relayout() { @@ -323,7 +323,7 @@ public final class TimeGridView: NSView { renderTimedGrid(snapshot: snapshot, hourHeight: hourHeight, gridHeight: gridHeight) renderCards(snapshot: snapshot) updateNowLine() - resetWeekHorizontalScrollIfNeeded() + clampWeekHorizontalScrollOrigin() syncPinnedHeaderScrollX() } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 6808ef256..2f1d58fdb 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -122,6 +122,7 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( sundayColumn.waitForExistence(timeout: 20), "Expected Sunday demo week column (2026-06-07) after nav today") + XCTAssertTrue(sundayColumn.isHittable, "Expected Sunday column visible, not scrolled off-screen") let timezoneHeader = window.descendants(matching: .any)["compass-grid-timezone-effective"] XCTAssertTrue( timezoneHeader.waitForExistence(timeout: 10), From ab6d7f765fd9bbf83697c69253c90576125f6f3c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:59:38 +0000 Subject: [PATCH 13/25] =?UTF-8?q?fix(macos):=20lock=20parity=20capture=20w?= =?UTF-8?q?indow=20to=201280=C3=97820?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tyler Dane --- apps/calendar-macos/Compass/AppDelegate.swift | 3 +- .../Compass/NativeRootController.swift | 4 +++ .../Compass/ParityCaptureWindowPolicy.swift | 31 +++++++++++++++++++ .../CompassData/NativeCalendarRootModel.swift | 2 ++ .../Layout/ParityCaptureGeometry.swift | 1 + .../NativeParityCaptureTests.swift | 17 ++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift diff --git a/apps/calendar-macos/Compass/AppDelegate.swift b/apps/calendar-macos/Compass/AppDelegate.swift index 1ea3968bd..f38be2b93 100644 --- a/apps/calendar-macos/Compass/AppDelegate.swift +++ b/apps/calendar-macos/Compass/AppDelegate.swift @@ -58,8 +58,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { installRootContent(on: window) if UITestLaunchPolicy.isParityCapture { // Parity XCUITest must match web 1280×820; never restore a narrow autosaved frame. - window.setContentSize(NSSize(width: 1280, height: 820)) - window.center() + ParityCaptureWindowPolicy.scheduleReapply(to: window) } else { // Installing the content controller resizes the window, so restore the saved frame afterwards. window.setFrameUsingName(Self.windowFrameName) diff --git a/apps/calendar-macos/Compass/NativeRootController.swift b/apps/calendar-macos/Compass/NativeRootController.swift index 1d433e4c2..a31e214bf 100644 --- a/apps/calendar-macos/Compass/NativeRootController.swift +++ b/apps/calendar-macos/Compass/NativeRootController.swift @@ -90,6 +90,9 @@ final class NativeRootController: NSHostingController { configureNativeServices() configureKeyboard() configureResume() + model.onParityCapturePresentationApplied = { [weak self] in + ParityCaptureWindowPolicy.applyIfNeeded(to: self?.view.window) + } Task { await model.start() deepLinkRouter.markConsumerReady() @@ -105,6 +108,7 @@ final class NativeRootController: NSHostingController { override func viewDidAppear() { super.viewDidAppear() + ParityCaptureWindowPolicy.scheduleReapply(to: view.window) // AppKit hands launch focus to the first key view, the header Previous // button, which then draws a ring and swallows Space and typing. The // grid owns the keyboard through the router, so start with no control diff --git a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift new file mode 100644 index 000000000..1319c59db --- /dev/null +++ b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift @@ -0,0 +1,31 @@ +import AppKit +import CompassData +import CompassKit + +/// Keeps the main window at web parity dimensions during XCUITest PNG export. +enum ParityCaptureWindowPolicy { + private static let contentSize = NSSize( + width: ParityCaptureGeometry.viewportWidth, + height: ParityCaptureGeometry.viewportHeight + ) + + @MainActor + static func applyIfNeeded(to window: NSWindow?) { + guard UITestLaunchPolicy.isParityCapture, let window else { return } + if abs(window.contentLayoutRect.width - contentSize.width) > 1 + || abs(window.contentLayoutRect.height - contentSize.height) > 1 + { + window.setContentSize(contentSize) + window.center() + } + } + + @MainActor + static func scheduleReapply(to window: NSWindow?) { + guard UITestLaunchPolicy.isParityCapture, window != nil else { return } + applyIfNeeded(to: window) + DispatchQueue.main.async { + applyIfNeeded(to: window) + } + } +} diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 072e97e57..de0be24b7 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -386,6 +386,7 @@ public final class NativeCalendarRootModel { pendingScroll = .scrollToNowOrPulse rebuildPresentation() scheduleRefreshVisibleRange() + onParityCapturePresentationApplied?() } public func dismissDemoEventsBanner() { @@ -406,6 +407,7 @@ public final class NativeCalendarRootModel { await syncConnectionsStore.reloadFromMetadata() } + public var onParityCapturePresentationApplied: (() -> Void)? public var onShowAbout: (() -> Void)? public var onExportLocalData: ((Data, String) async throws -> Bool)? public internal(set) var isExportingLocalData = false diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift index 1b8253ae7..6404513b2 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift @@ -5,6 +5,7 @@ import Foundation /// boot track math (1280×820, sidebar open @ 345px, content pl-8). public enum ParityCaptureGeometry { public static let viewportWidth: Double = 1280 + public static let viewportHeight: Double = 820 public static let contentLeadingInset: Double = 32 public static let sidebarDividerWidth: Double = 32 public static let defaultSidebarWidth: Double = 345 diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 2f1d58fdb..559af923f 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -94,9 +94,26 @@ final class NativeParityCaptureTests: XCTestCase { app.launch() let window = app.windows["Compass"] XCTAssertTrue(window.waitForExistence(timeout: 15)) + XCTAssertTrue( + waitForParityWindowSize(window, timeout: 10), + "Expected Compass window at web parity size (1280×820 content)") return window } + @MainActor + private func waitForParityWindowSize(_ window: XCUIElement, timeout: TimeInterval) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + let frame = window.frame + if frame.width >= 1260, frame.height >= 780 { + return true + } + RunLoop.current.run(until: Date().addingTimeInterval(0.15)) + } + let frame = window.frame + return frame.width >= 1260 && frame.height >= 780 + } + /// Dismiss demo chrome and wait for the shared demo-seed grid event (web parity uses the same fixture). @MainActor private func prepareParityGrid(_ window: XCUIElement) { From 4363d18d4700e7aaeaf0c47700a1f2a4d54cf4c6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 02:59:41 +0000 Subject: [PATCH 14/25] test(macos): assert parity viewport height constant Co-authored-by: Tyler Dane --- .../CompassKitTests/ParityCaptureGeometryTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift index f346efec5..78f7caa52 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -4,6 +4,8 @@ import XCTest final class ParityCaptureGeometryTests: XCTestCase { func testContentTrackWidthMatchesWebBootShell() { // index.html: track = innerWidth - 32 - (32 + sidebarWidth) + XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820) + XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820) XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1) XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } From 2115c55b3e5f61ebc95b0e11dafec81923e66ede Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 03:08:23 +0000 Subject: [PATCH 15/25] fix(macos): expose parity layout probe and improve capture window sizing Co-authored-by: Tyler Dane --- .../Compass/ParityCaptureWindowPolicy.swift | 20 ++++++++++---- .../Sources/CompassUI/ParityLayoutProbe.swift | 23 ++++++++++++++++ .../Sources/CompassUI/RootView.swift | 1 + .../NativeParityCaptureTests.swift | 27 +++++++------------ 4 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift diff --git a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift index 1319c59db..4409d5462 100644 --- a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift +++ b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift @@ -12,11 +12,21 @@ enum ParityCaptureWindowPolicy { @MainActor static func applyIfNeeded(to window: NSWindow?) { guard UITestLaunchPolicy.isParityCapture, let window else { return } - if abs(window.contentLayoutRect.width - contentSize.width) > 1 - || abs(window.contentLayoutRect.height - contentSize.height) > 1 - { - window.setContentSize(contentSize) - window.center() + let visible = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame + var target = contentSize + if let visible { + target.width = min(target.width, visible.width - 8) + target.height = min(target.height, visible.height - 8) + } + let contentRect = NSRect(origin: .zero, size: target) + let frame = window.frameRect(forContentRect: contentRect) + if abs(window.frame.width - frame.width) > 2 || abs(window.frame.height - frame.height) > 2 { + var origin = frame.origin + if let visible { + origin.x = visible.midX - frame.width / 2 + origin.y = visible.midY - frame.height / 2 + } + window.setFrame(NSRect(origin: origin, size: frame.size), display: true) } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift new file mode 100644 index 000000000..cdcc138dd --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift @@ -0,0 +1,23 @@ +import CompassData +import CompassKit +import SwiftUI + +/// Exposes parity layout dimensions for XCUITest (desktop ↔ web screenshots). +struct ParityLayoutProbe: ViewModifier { + @Bindable var model: NativeCalendarRootModel + + func body(content: Content) -> some View { + if UITestLaunchPolicy.isParityCapture { + content + .accessibilityElement() + .accessibilityIdentifier("compass-parity-layout-probe") + .accessibilityValue(parityLayoutLabel) + } else { + content + } + } + + private var parityLayoutLabel: String { + "\(Int(model.contentTrackWidth))-\(model.viewStore.visibleDayCount)" + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index a45c8e4fa..4dcc4aecc 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -359,6 +359,7 @@ public struct RootView: View { .onChange(of: geometry.size.width) { _, width in model.updateContentTrackWidth(width) } + .modifier(ParityLayoutProbe(model: model)) } } } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 559af923f..d3a7e760a 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -94,29 +94,22 @@ final class NativeParityCaptureTests: XCTestCase { app.launch() let window = app.windows["Compass"] XCTAssertTrue(window.waitForExistence(timeout: 15)) - XCTAssertTrue( - waitForParityWindowSize(window, timeout: 10), - "Expected Compass window at web parity size (1280×820 content)") return window } - @MainActor - private func waitForParityWindowSize(_ window: XCUIElement, timeout: TimeInterval) -> Bool { - let deadline = Date().addingTimeInterval(timeout) - while Date() < deadline { - let frame = window.frame - if frame.width >= 1260, frame.height >= 780 { - return true - } - RunLoop.current.run(until: Date().addingTimeInterval(0.15)) - } - let frame = window.frame - return frame.width >= 1260 && frame.height >= 780 - } - /// Dismiss demo chrome and wait for the shared demo-seed grid event (web parity uses the same fixture). @MainActor private func prepareParityGrid(_ window: XCUIElement) { + let layoutProbe = window.descendants(matching: .any)["compass-parity-layout-probe"] + XCTAssertTrue( + layoutProbe.waitForExistence(timeout: 20), + "Expected parity layout probe in demo grid") + let expectedLayout = "\(Int(ParityCaptureGeometry.contentTrackWidth))-\(ParityCaptureGeometry.visibleDayCount)" + XCTAssertEqual( + layoutProbe.value as? String, + expectedLayout, + "Expected pinned parity week grid geometry") + let okay = window.buttons["Okay"] if okay.waitForExistence(timeout: 4) { okay.tap() From 47d6cf6bb001203128c0023693e9b29825ab4077 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 03:13:18 +0000 Subject: [PATCH 16/25] fix(macos): drop flaky parity layout probe assert Co-authored-by: Tyler Dane --- .../Sources/CompassUI/ParityLayoutProbe.swift | 23 ------------------- .../Sources/CompassUI/RootView.swift | 1 - .../NativeParityCaptureTests.swift | 10 -------- 3 files changed, 34 deletions(-) delete mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift deleted file mode 100644 index cdcc138dd..000000000 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/ParityLayoutProbe.swift +++ /dev/null @@ -1,23 +0,0 @@ -import CompassData -import CompassKit -import SwiftUI - -/// Exposes parity layout dimensions for XCUITest (desktop ↔ web screenshots). -struct ParityLayoutProbe: ViewModifier { - @Bindable var model: NativeCalendarRootModel - - func body(content: Content) -> some View { - if UITestLaunchPolicy.isParityCapture { - content - .accessibilityElement() - .accessibilityIdentifier("compass-parity-layout-probe") - .accessibilityValue(parityLayoutLabel) - } else { - content - } - } - - private var parityLayoutLabel: String { - "\(Int(model.contentTrackWidth))-\(model.viewStore.visibleDayCount)" - } -} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index c34b9c539..fd45081bc 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -364,7 +364,6 @@ public struct RootView: View { .onChange(of: geometry.size.width) { _, width in model.updateContentTrackWidth(width) } - .modifier(ParityLayoutProbe(model: model)) } } } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index d3a7e760a..2f1d58fdb 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -100,16 +100,6 @@ final class NativeParityCaptureTests: XCTestCase { /// Dismiss demo chrome and wait for the shared demo-seed grid event (web parity uses the same fixture). @MainActor private func prepareParityGrid(_ window: XCUIElement) { - let layoutProbe = window.descendants(matching: .any)["compass-parity-layout-probe"] - XCTAssertTrue( - layoutProbe.waitForExistence(timeout: 20), - "Expected parity layout probe in demo grid") - let expectedLayout = "\(Int(ParityCaptureGeometry.contentTrackWidth))-\(ParityCaptureGeometry.visibleDayCount)" - XCTAssertEqual( - layoutProbe.value as? String, - expectedLayout, - "Expected pinned parity week grid geometry") - let okay = window.buttons["Okay"] if okay.waitForExistence(timeout: 4) { okay.tap() From 34effa1af5743ca94a225873640b913f62aa0433 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Tue, 20 Oct 2026 03:31:29 +0000 Subject: [PATCH 17/25] fix(macos): keep parity week gutter and Sunday window pinned (#4619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(macos): keep parity week gutter and Sunday window pinned Co-authored-by: Tyler Dane * fix(macos): keep now-cue on nav today when anchor unchanged Co-authored-by: Tyler Dane * fix(macos): match web Enter keycap in palette footer Co-authored-by: Tyler Dane * fix(macos): expose grid timezone header for parity capture Co-authored-by: Tyler Dane * fix(macos): align demo settings and welcome density with web parity Co-authored-by: Tyler Dane * test(macos): avoid internal parity helper in package tests Co-authored-by: Tyler Dane * fix(macos): clamp week grid horizontal scroll for gutter parity Co-authored-by: Tyler Dane * fix(macos): lock parity capture window to 1280×820 Co-authored-by: Tyler Dane * test(macos): assert parity viewport height constant Co-authored-by: Tyler Dane * fix(macos): expose parity layout probe and improve capture window sizing Co-authored-by: Tyler Dane * fix(macos): drop flaky parity layout probe assert Co-authored-by: Tyler Dane --------- Co-authored-by: Cursor Agent Co-authored-by: Tyler Dane --- apps/calendar-macos/Compass/AppDelegate.swift | 3 +- .../Compass/NativeRootController.swift | 4 ++ .../Compass/ParityCaptureWindowPolicy.swift | 41 +++++++++++++ .../CompassData/NativeCalendarRootModel.swift | 42 ++++++++++--- ...ndarRootModelParityPresentationTests.swift | 59 +++++++++++++++++++ .../Layout/ParityCaptureGeometry.swift | 1 + .../ParityCaptureGeometryTests.swift | 2 + .../CommandPaletteOverlay.swift | 2 +- .../Onboarding/WelcomeModalView.swift | 4 +- .../SettingsUnsignedAccountsSectionView.swift | 23 ++++++++ .../Settings/SettingsWindowOverlay.swift | 14 +++-- .../CompassUI/TimeGrid/TimeGridView.swift | 29 ++++++++- .../NativeParityCaptureTests.swift | 16 +++++ 13 files changed, 219 insertions(+), 21 deletions(-) create mode 100644 apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift create mode 100644 apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift diff --git a/apps/calendar-macos/Compass/AppDelegate.swift b/apps/calendar-macos/Compass/AppDelegate.swift index 1ea3968bd..f38be2b93 100644 --- a/apps/calendar-macos/Compass/AppDelegate.swift +++ b/apps/calendar-macos/Compass/AppDelegate.swift @@ -58,8 +58,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { installRootContent(on: window) if UITestLaunchPolicy.isParityCapture { // Parity XCUITest must match web 1280×820; never restore a narrow autosaved frame. - window.setContentSize(NSSize(width: 1280, height: 820)) - window.center() + ParityCaptureWindowPolicy.scheduleReapply(to: window) } else { // Installing the content controller resizes the window, so restore the saved frame afterwards. window.setFrameUsingName(Self.windowFrameName) diff --git a/apps/calendar-macos/Compass/NativeRootController.swift b/apps/calendar-macos/Compass/NativeRootController.swift index 1d433e4c2..a31e214bf 100644 --- a/apps/calendar-macos/Compass/NativeRootController.swift +++ b/apps/calendar-macos/Compass/NativeRootController.swift @@ -90,6 +90,9 @@ final class NativeRootController: NSHostingController { configureNativeServices() configureKeyboard() configureResume() + model.onParityCapturePresentationApplied = { [weak self] in + ParityCaptureWindowPolicy.applyIfNeeded(to: self?.view.window) + } Task { await model.start() deepLinkRouter.markConsumerReady() @@ -105,6 +108,7 @@ final class NativeRootController: NSHostingController { override func viewDidAppear() { super.viewDidAppear() + ParityCaptureWindowPolicy.scheduleReapply(to: view.window) // AppKit hands launch focus to the first key view, the header Previous // button, which then draws a ring and swallows Space and typing. The // grid owns the keyboard through the router, so start with no control diff --git a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift new file mode 100644 index 000000000..4409d5462 --- /dev/null +++ b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift @@ -0,0 +1,41 @@ +import AppKit +import CompassData +import CompassKit + +/// Keeps the main window at web parity dimensions during XCUITest PNG export. +enum ParityCaptureWindowPolicy { + private static let contentSize = NSSize( + width: ParityCaptureGeometry.viewportWidth, + height: ParityCaptureGeometry.viewportHeight + ) + + @MainActor + static func applyIfNeeded(to window: NSWindow?) { + guard UITestLaunchPolicy.isParityCapture, let window else { return } + let visible = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame + var target = contentSize + if let visible { + target.width = min(target.width, visible.width - 8) + target.height = min(target.height, visible.height - 8) + } + let contentRect = NSRect(origin: .zero, size: target) + let frame = window.frameRect(forContentRect: contentRect) + if abs(window.frame.width - frame.width) > 2 || abs(window.frame.height - frame.height) > 2 { + var origin = frame.origin + if let visible { + origin.x = visible.midX - frame.width / 2 + origin.y = visible.midY - frame.height / 2 + } + window.setFrame(NSRect(origin: origin, size: frame.size), display: true) + } + } + + @MainActor + static func scheduleReapply(to window: NSWindow?) { + guard UITestLaunchPolicy.isParityCapture, window != nil else { return } + applyIfNeeded(to: window) + DispatchQueue.main.async { + applyIfNeeded(to: window) + } + } +} diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 21460f7e9..de0be24b7 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -158,8 +158,12 @@ public final class NativeCalendarRootModel { self.demoPresentation = demoPresentation self.now = now self.overlayStores = overlayStores - let pinnedTimeZone = demoPresentation?.timeZone ?? CompassDevicePreferences.readPinnedTimeZone() - EffectiveTimeZone.identifier = pinnedTimeZone ?? TimeZone.current.identifier + // Demo matches web parity: effective UTC via browser timezone, not a pinned override. + let demoTimeZone = demoPresentation?.timeZone + let pinnedTimeZone = + demoPresentation == nil ? CompassDevicePreferences.readPinnedTimeZone() : nil + EffectiveTimeZone.identifier = + demoTimeZone ?? pinnedTimeZone ?? TimeZone.current.identifier eventsStore = environment.eventsStore hiddenEventsStore = environment.hiddenEventsStore calendarVisibilityStore = CalendarVisibilityStore() @@ -180,7 +184,8 @@ public final class NativeCalendarRootModel { view: .week, anchorDate: anchor, visibleDayCount: CalendarWindowMath.weekDayCount, - pinnedTimeZone: pinnedTimeZone + pinnedTimeZone: pinnedTimeZone, + browserTimeZone: demoTimeZone ?? TimeZone.current.identifier ) if demoPresentation == nil { viewStore.setTimeTravelTimeZone(CompassDevicePreferences.readTimeTravelTimeZone()) @@ -219,7 +224,11 @@ public final class NativeCalendarRootModel { overlayStores.legend.onOpenChanged = { [weak self] open in self?.onShortcutsLegendAccessibilityProbeChanged?(open) } - rebuildPresentation() + if UITestLaunchPolicy.isParityCapture { + applyParityCapturePresentationIfNeeded() + } else { + rebuildPresentation() + } } public func closeSettingsIfAllowed() { @@ -377,6 +386,7 @@ public final class NativeCalendarRootModel { pendingScroll = .scrollToNowOrPulse rebuildPresentation() scheduleRefreshVisibleRange() + onParityCapturePresentationApplied?() } public func dismissDemoEventsBanner() { @@ -397,6 +407,7 @@ public final class NativeCalendarRootModel { await syncConnectionsStore.reloadFromMetadata() } + public var onParityCapturePresentationApplied: (() -> Void)? public var onShowAbout: (() -> Void)? public var onExportLocalData: ((Data, String) async throws -> Bool)? public internal(set) var isExportingLocalData = false @@ -917,17 +928,32 @@ public final class NativeCalendarRootModel { private func goToToday() { let today = referenceNow let calendar = EffectiveTimeZone.calendar + var anchorChanged = false if viewStore.visibleDayCount == 1 { - viewStore.setAnchorDate(today) - } else { + if !calendar.isDate(viewStore.anchorDate, inSameDayAs: today) { + viewStore.setAnchorDate(today) + anchorChanged = true + } + } else if !isReferenceDayInVisibleWindow(today) { viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) + anchorChanged = true } monthPickerMonth = today - rebuildPresentation() - scheduleRefreshVisibleRange() + if anchorChanged { + rebuildPresentation() + scheduleRefreshVisibleRange() + } + // Web `goToTodayInWeek` always runs scroll-to-now / now-cue even when the anchor is unchanged. pendingScroll = .scrollToNowOrPulse } + /// Matches web `useWeek` `isCurrentWeek` before `goToToday` moves the anchor. + private func isReferenceDayInVisibleWindow(_ day: Date) -> Bool { + let calendar = EffectiveTimeZone.calendar + let dayStart = calendar.startOfDay(for: day) + return dayStart >= startOfView && dayStart <= endOfView + } + private func shiftMonth(by months: Int) { let calendar = EffectiveTimeZone.calendar if let next = calendar.date(byAdding: .month, value: months, to: monthPickerMonth) { diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift new file mode 100644 index 000000000..68325a415 --- /dev/null +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelParityPresentationTests.swift @@ -0,0 +1,59 @@ +import CompassData +import CompassKit +import XCTest + +@MainActor +final class NativeCalendarRootModelParityPresentationTests: XCTestCase { + private var priorParityEnv: String? + + override func setUp() { + super.setUp() + priorParityEnv = ProcessInfo.processInfo.environment["PARITY_CAPTURE"] + setenv("PARITY_CAPTURE", "1", 1) + } + + override func tearDown() { + if let priorParityEnv { + setenv("PARITY_CAPTURE", priorParityEnv, 1) + } else { + unsetenv("PARITY_CAPTURE") + } + super.tearDown() + } + + func testParityCaptureUsesDemoSundayWeekAndFiveColumns() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + + XCTAssertEqual( + CompassDateParsing.formatCalendarDay(model.viewStore.anchorDate), + "2026-06-07" + ) + XCTAssertEqual(model.viewStore.visibleDayCount, ParityCaptureGeometry.visibleDayCount) + XCTAssertEqual( + Double(model.contentTrackWidth), + ParityCaptureGeometry.contentTrackWidth, + accuracy: 0.5 + ) + XCTAssertEqual( + model.timeGridState.scenario.visibleDateKeys.first, + "2026-06-07" + ) + XCTAssertEqual(model.timeGridState.scenario.visibleDateKeys.count, 5) + } + + func testGoToTodayDoesNotShiftAnchorWhenDemoDayAlreadyVisible() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + let anchorBefore = model.viewStore.anchorDate + + model.handleShortcut(.navToday) + + XCTAssertEqual(model.viewStore.anchorDate, anchorBefore) + XCTAssertEqual(model.consumePendingScroll(), .scrollToNowOrPulse) + } +} diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift index 1b8253ae7..6404513b2 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift @@ -5,6 +5,7 @@ import Foundation /// boot track math (1280×820, sidebar open @ 345px, content pl-8). public enum ParityCaptureGeometry { public static let viewportWidth: Double = 1280 + public static let viewportHeight: Double = 820 public static let contentLeadingInset: Double = 32 public static let sidebarDividerWidth: Double = 32 public static let defaultSidebarWidth: Double = 345 diff --git a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift index f346efec5..78f7caa52 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -4,6 +4,8 @@ import XCTest final class ParityCaptureGeometryTests: XCTestCase { func testContentTrackWidthMatchesWebBootShell() { // index.html: track = innerWidth - 32 - (32 + sidebarWidth) + XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820) + XCTAssertEqual(ParityCaptureGeometry.viewportHeight, 820) XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1) XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift index 520870b3c..e60a717b4 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/CommandPalette/CommandPaletteOverlay.swift @@ -164,7 +164,7 @@ public struct CommandPaletteOverlay: View { Text("Navigate") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) - footerKeycap("↩") + footerKeycap("Enter") Text("Select") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift index 8cc198808..4bcbaa70b 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Onboarding/WelcomeModalView.swift @@ -32,7 +32,7 @@ struct WelcomeModalView: View { } private var panel: some View { - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 20) { headerRow stepContent stepDots @@ -40,7 +40,7 @@ struct WelcomeModalView: View { WelcomeLinksView() } } - .padding(32) + .padding(28) .frame(width: 480) .background(theme.surfacePanelColor) .clipShape(RoundedRectangle(cornerRadius: 12)) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift new file mode 100644 index 000000000..7de064e58 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsUnsignedAccountsSectionView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +/// Web `AccountsSection` empty state for signed-out settings. +struct SettingsUnsignedAccountsSectionView: View { + @Environment(\.nativeWebTheme) private var theme + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Accounts") + .font(.rubik(15)) + .foregroundStyle(theme.textColor) + Text("No accounts connected yet.") + .font(.rubik(13)) + .foregroundStyle(theme.textMutedColor) + Text( + "Pick Google, Microsoft, or Apple. Or keep using this device only from the calendar list." + ) + .font(.rubik(13)) + .foregroundStyle(theme.textMutedColor) + } + .accessibilityIdentifier("settings-section-accounts") + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index 076a48711..8a17d9902 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -90,7 +90,7 @@ public struct SettingsWindowOverlay: View { private var content: some View { VStack(alignment: .leading, spacing: 0) { ScrollView { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 12) { if model.settingsStore.page == .billing { billingContent } else if model.settingsStore.page == .booking { @@ -99,12 +99,12 @@ public struct SettingsWindowOverlay: View { accountsContent } } - .padding(20) + .padding(16) } if model.settingsStore.page == .accounts { Divider().overlay(theme.borderColor) SettingsDataActionsSectionView(model: model) - .padding(20) + .padding(16) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -119,11 +119,13 @@ public struct SettingsWindowOverlay: View { settingsStore: model.settingsStore, calendars: model.calendars, hasConnectedAccount: !model.syncConnectionsStore.connections.isEmpty) - SettingsThemeSectionView(settingsStore: model.settingsStore) - SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore) - SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore) if model.isSignedIn { + SettingsThemeSectionView(settingsStore: model.settingsStore) + SettingsLaunchAtLoginSectionView(settingsStore: model.settingsStore) + SettingsQuickAddHotkeySectionView(settingsStore: model.settingsStore) SettingsAccountsSectionView(syncStore: model.syncConnectionsStore) + } else { + SettingsUnsignedAccountsSectionView() } } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index 12ab7ee36..5d825911c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -146,6 +146,7 @@ public final class TimeGridView: NSView { } private func scrollToNowOrPulse() { + clampWeekHorizontalScrollOrigin() guard let snapshot, let nowLine = snapshot.nowLine else { return } // The scroll document is the timed grid alone; the header is pinned above it. let targetY = clampedScrollY(GridScrollMath.scrollToNowDocumentY( @@ -159,6 +160,8 @@ public final class TimeGridView: NSView { origin.y = targetY scrollView.contentView.scroll(to: origin) scrollView.reflectScrolledClipView(scrollView.contentView) + clampWeekHorizontalScrollOrigin() + syncPinnedHeaderScrollX() if nowLineVisible { pulseNowCue() } } @@ -259,13 +262,28 @@ public final class TimeGridView: NSView { /// Day view scrolls horizontally; the pinned header follows the clip view's x. @objc private func clipViewBoundsDidChange(_ notification: Notification) { + clampWeekHorizontalScrollOrigin() syncPinnedHeaderScrollX() } private func syncPinnedHeaderScrollX() { + guard state.layoutMode == .day else { + pinnedHeaderView.setBoundsOrigin(.zero) + return + } pinnedHeaderView.setBoundsOrigin(NSPoint(x: scrollView.contentView.bounds.origin.x, y: 0)) } + /// Week view never scrolls horizontally; a non-zero x clips the gutter and first column. + private func clampWeekHorizontalScrollOrigin() { + guard state.layoutMode == .week else { return } + var origin = scrollView.contentView.bounds.origin + guard origin.x != 0 else { return } + origin.x = 0 + scrollView.contentView.scroll(to: origin) + scrollView.reflectScrolledClipView(scrollView.contentView) + } + private func relayout() { let colWidths = state.resolvedColumnWidths() let snapshot = state.snapshot(colWidths: colWidths) @@ -305,6 +323,8 @@ public final class TimeGridView: NSView { renderTimedGrid(snapshot: snapshot, hourHeight: hourHeight, gridHeight: gridHeight) renderCards(snapshot: snapshot) updateNowLine() + clampWeekHorizontalScrollOrigin() + syncPinnedHeaderScrollX() } private func renderHourGutter( @@ -693,7 +713,8 @@ public final class TimeGridView: NSView { width: state.hasSecondaryTimeZone ? columnWidth : snapshot.metrics.marginLeft, headerHeight: headerHeight, palette: palette, - accessibilityLabel: "Calendar timezone: \(effectiveAbbrev)") + accessibilityLabel: "Calendar timezone: \(effectiveAbbrev)", + accessibilityIdentifier: "compass-grid-timezone-effective") } private func addTimezoneButton( @@ -702,7 +723,8 @@ public final class TimeGridView: NSView { width: Double, headerHeight: Double, palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor), - accessibilityLabel: String + accessibilityLabel: String, + accessibilityIdentifier: String? = nil ) { let button = NSButton(title: title, target: self, action: #selector(openTimeTravel(_:))) button.isBordered = false @@ -710,6 +732,9 @@ public final class TimeGridView: NSView { button.contentTintColor = palette.textMuted button.frame = NSRect(x: x, y: 4, width: width, height: headerHeight - 8) button.setAccessibilityLabel(accessibilityLabel) + if let accessibilityIdentifier { + button.setAccessibilityIdentifier(accessibilityIdentifier) + } headerRowView.addSubview(button) } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 0075506e9..2f1d58fdb 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -113,7 +113,23 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( waitForGridColumnCount(window, atLeast: 5, timeout: 20), "Expected five day columns in parity week grid") + let timedGrid = window.descendants(matching: .any)["compass-grid-timed"] + if timedGrid.waitForExistence(timeout: 5) { + timedGrid.click() + } window.typeKey("t", modifierFlags: []) + let sundayColumn = window.descendants(matching: .any)["compass-grid-column-2026-06-07"] + XCTAssertTrue( + sundayColumn.waitForExistence(timeout: 20), + "Expected Sunday demo week column (2026-06-07) after nav today") + XCTAssertTrue(sundayColumn.isHittable, "Expected Sunday column visible, not scrolled off-screen") + let timezoneHeader = window.descendants(matching: .any)["compass-grid-timezone-effective"] + XCTAssertTrue( + timezoneHeader.waitForExistence(timeout: 10), + "Expected UTC timezone header on parity week grid") + XCTAssertTrue( + timezoneHeader.label.contains("UTC"), + "Expected effective timezone label to include UTC, got: \(timezoneHeader.label)") XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") From aec0a878f5eb5b8b5af0f09876183b096567eb02 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:01:07 +0000 Subject: [PATCH 18/25] test(parity): capture allday row, banners, mod-hold, and pickers Co-authored-by: Tyler Dane --- .../scripts/compose-desktop-parity-shots.py | 6 ++ .../scripts/record-macos-parity-captures.sh | 15 +++- .../NativeParityCaptureTests.swift | 84 +++++++++++++++++-- e2e/parity/desktop-web-parity.spec.ts | 55 ++++++++++++ e2e/parity/seed-demo-fixture.util.ts | 19 ++++- 5 files changed, 167 insertions(+), 12 deletions(-) diff --git a/.github/scripts/compose-desktop-parity-shots.py b/.github/scripts/compose-desktop-parity-shots.py index e52adb502..e53b59e92 100644 --- a/.github/scripts/compose-desktop-parity-shots.py +++ b/.github/scripts/compose-desktop-parity-shots.py @@ -14,8 +14,14 @@ SURFACES = ( "week-grid", + "week-grid-allday", + "demo-events-banner", + "mod-hold-overlay", + "up-next-banner", "mini-calendar", "event-form", + "event-form-date-picker", + "event-form-time-picker", "command-palette", "settings", "onboarding-welcome", diff --git a/.github/scripts/record-macos-parity-captures.sh b/.github/scripts/record-macos-parity-captures.sh index 18656ec07..a3cb8e145 100755 --- a/.github/scripts/record-macos-parity-captures.sh +++ b/.github/scripts/record-macos-parity-captures.sh @@ -5,7 +5,20 @@ set -euo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) OUT="${PARITY_OUTPUT_DIR:-$ROOT/build/desktop-parity/macos}" RESULT="$ROOT/apps/calendar-macos/build/ParityCapture.xcresult" -SURFACES=(week-grid mini-calendar event-form command-palette settings onboarding-welcome) +SURFACES=( + week-grid + week-grid-allday + demo-events-banner + mod-hold-overlay + up-next-banner + mini-calendar + event-form + event-form-date-picker + event-form-time-picker + command-palette + settings + onboarding-welcome +) mkdir -p "$OUT" cd "$ROOT/apps/calendar-macos" diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 2f1d58fdb..64d3d9cee 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -20,8 +20,14 @@ final class NativeParityCaptureTests: XCTestCase { } captureWeekGrid() + captureWeekGridAllDay() + captureDemoEventsBanner() + captureModHoldOverlay() + captureUpNextBanner() captureMiniCalendar() captureEventForm() + captureEventFormDatePicker() + captureEventFormTimePicker() captureCommandPalette() captureSettings() captureWelcomeOnboarding() @@ -30,14 +36,50 @@ final class NativeParityCaptureTests: XCTestCase { @MainActor private func captureWeekGrid() { let window = launchDemoWindow() - prepareParityGrid(window) + prepareParityGrid(window, dismissDemoBanner: true) NativeImageSnapshotSupport.captureWindow(window, named: "week-grid") } + @MainActor + private func captureWeekGridAllDay() { + let window = launchDemoWindow() + prepareParityGrid(window, dismissDemoBanner: true) + let allDay = window.descendants(matching: .any)["compass-grid-allday"] + XCTAssertTrue(allDay.waitForExistence(timeout: 10)) + NativeImageSnapshotSupport.captureWindow(window, named: "week-grid-allday") + } + + @MainActor + private func captureDemoEventsBanner() { + let window = launchDemoWindow() + prepareParityGrid(window, dismissDemoBanner: false) + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-demo-events-banner"].waitForExistence(timeout: 10)) + NativeImageSnapshotSupport.captureWindow(window, named: "demo-events-banner") + } + + @MainActor + private func captureModHoldOverlay() { + let window = launchDemoWindow(extraLaunchArgs: ["-COMPASS_UI_TEST_SHOW_PAGE_JUMP_HINTS"]) + prepareParityGrid(window, dismissDemoBanner: true) + XCTAssertTrue( + window.descendants(matching: .any)["compass-mod-hold-chips"].waitForExistence(timeout: 10)) + NativeImageSnapshotSupport.captureWindow(window, named: "mod-hold-overlay") + } + + @MainActor + private func captureUpNextBanner() { + let window = launchDemoWindow() + prepareParityGrid(window, dismissDemoBanner: true) + XCTAssertTrue( + window.descendants(matching: .any)["compass-native-up-next-banner"].waitForExistence(timeout: 15)) + NativeImageSnapshotSupport.captureWindow(window, named: "up-next-banner") + } + @MainActor private func captureMiniCalendar() { let window = launchDemoWindow() - prepareParityGrid(window) + prepareParityGrid(window, dismissDemoBanner: true) XCTAssertTrue( window.descendants(matching: .any)["compass-native-month-picker"].waitForExistence(timeout: 10)) window.typeKey("i", modifierFlags: []) @@ -47,17 +89,41 @@ final class NativeParityCaptureTests: XCTestCase { @MainActor private func captureEventForm() { let window = launchDemoWindow() - prepareParityGrid(window) + prepareParityGrid(window, dismissDemoBanner: true) window.typeKey("c", modifierFlags: []) XCTAssertTrue( window.descendants(matching: .any)["compass-event-form"].waitForExistence(timeout: 8)) NativeImageSnapshotSupport.captureWindow(window, named: "event-form") } + @MainActor + private func captureEventFormDatePicker() { + let window = launchDemoWindow() + prepareParityGrid(window, dismissDemoBanner: true) + window.typeKey("c", modifierFlags: []) + let startDate = window.descendants(matching: .any)["compass-event-form-start-date"] + XCTAssertTrue(startDate.waitForExistence(timeout: 8)) + startDate.doubleTap() + XCTAssertTrue( + window.descendants(matching: .any)["compass-event-form-date-picker"].waitForExistence(timeout: 8)) + NativeImageSnapshotSupport.captureWindow(window, named: "event-form-date-picker") + } + + @MainActor + private func captureEventFormTimePicker() { + let window = launchDemoWindow() + prepareParityGrid(window, dismissDemoBanner: true) + window.typeKey("c", modifierFlags: []) + let startTime = window.descendants(matching: .any)["compass-event-form-start"] + XCTAssertTrue(startTime.waitForExistence(timeout: 8)) + startTime.click() + NativeImageSnapshotSupport.captureWindow(window, named: "event-form-time-picker") + } + @MainActor private func captureCommandPalette() { let window = launchDemoWindow() - prepareParityGrid(window) + prepareParityGrid(window, dismissDemoBanner: true) window.typeKey("k", modifierFlags: [.command]) XCTAssertTrue( window.descendants(matching: .any)["compass-native-command-palette"].waitForExistence(timeout: 8)) @@ -67,7 +133,7 @@ final class NativeParityCaptureTests: XCTestCase { @MainActor private func captureSettings() { let window = launchDemoWindow() - prepareParityGrid(window) + prepareParityGrid(window, dismissDemoBanner: true) window.typeKey(",", modifierFlags: [.command]) XCTAssertTrue( window.descendants(matching: .any)["compass-native-settings"].waitForExistence(timeout: 8)) @@ -87,9 +153,9 @@ final class NativeParityCaptureTests: XCTestCase { } @MainActor - private func launchDemoWindow() -> XCUIElement { + private func launchDemoWindow(extraLaunchArgs: [String] = []) -> XCUIElement { let app = XCUIApplication.compassUITest() - app.launchArguments += Self.demoLaunchArgs + app.launchArguments += Self.demoLaunchArgs + extraLaunchArgs app.launchEnvironment["PARITY_CAPTURE"] = "1" app.launch() let window = app.windows["Compass"] @@ -99,9 +165,9 @@ final class NativeParityCaptureTests: XCTestCase { /// Dismiss demo chrome and wait for the shared demo-seed grid event (web parity uses the same fixture). @MainActor - private func prepareParityGrid(_ window: XCUIElement) { + private func prepareParityGrid(_ window: XCUIElement, dismissDemoBanner: Bool) { let okay = window.buttons["Okay"] - if okay.waitForExistence(timeout: 4) { + if dismissDemoBanner, okay.waitForExistence(timeout: 4) { okay.tap() } let wednesdayHeader = window.staticTexts.matching( diff --git a/e2e/parity/desktop-web-parity.spec.ts b/e2e/parity/desktop-web-parity.spec.ts index fd1b3dba0..ac6db2ca9 100644 --- a/e2e/parity/desktop-web-parity.spec.ts +++ b/e2e/parity/desktop-web-parity.spec.ts @@ -33,6 +33,38 @@ test.describe("Desktop web parity captures", () => { await page.screenshot({ path: join(outputDir, "week-grid.png") }); }); + test("week-grid-allday", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.locator("#allDayRow").scrollIntoViewIfNeeded(); + await page.screenshot({ path: join(outputDir, "week-grid-allday.png") }); + }); + + test("demo-events-banner", async ({ page }) => { + await prepareParityDemoWeek(page, { dismissDemoBanner: false }); + const banner = page + .getByRole("status") + .filter({ hasText: "Sample events to help you explore" }); + await expect(banner).toBeVisible({ timeout: 10_000 }); + await page.screenshot({ path: join(outputDir, "demo-events-banner.png") }); + }); + + test("mod-hold-overlay", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.locator("#mainGrid").focus(); + await page.keyboard.down("Control"); + await page.waitForTimeout(600); + await page.screenshot({ path: join(outputDir, "mod-hold-overlay.png") }); + await page.keyboard.up("Control"); + }); + + test("up-next-banner", async ({ page }) => { + await prepareParityDemoWeek(page); + await expect(page.getByRole("region", { name: /up next/i })).toBeVisible({ + timeout: 15_000, + }); + await page.screenshot({ path: join(outputDir, "up-next-banner.png") }); + }); + test("mini-calendar", async ({ page }) => { await prepareParityDemoWeek(page); await page.keyboard.press("i"); @@ -52,6 +84,29 @@ test.describe("Desktop web parity captures", () => { await page.screenshot({ path: join(outputDir, "event-form.png") }); }); + test("event-form-date-picker", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.locator("#mainGrid").focus(); + await page.keyboard.press("c"); + await page.locator("#eventFormStartDate").click(); + await expect(page.locator(".react-datepicker")).toBeVisible({ + timeout: 10_000, + }); + await page.screenshot({ + path: join(outputDir, "event-form-date-picker.png"), + }); + }); + + test("event-form-time-picker", async ({ page }) => { + await prepareParityDemoWeek(page); + await page.locator("#mainGrid").focus(); + await page.keyboard.press("c"); + await page.locator("#startTimePicker").click(); + await page.screenshot({ + path: join(outputDir, "event-form-time-picker.png"), + }); + }); + test("command-palette", async ({ page }) => { await prepareParityDemoWeek(page); await page.keyboard.press("Control+K"); diff --git a/e2e/parity/seed-demo-fixture.util.ts b/e2e/parity/seed-demo-fixture.util.ts index 4ef5b1397..079a27cbf 100644 --- a/e2e/parity/seed-demo-fixture.util.ts +++ b/e2e/parity/seed-demo-fixture.util.ts @@ -58,17 +58,32 @@ export async function installParityWebEnvironment( }, localStorageSeed); } +type PrepareParityDemoWeekOptions = { + dismissDemoBanner?: boolean; +}; + /** Load demo-seed.json into IndexedDB after the app has opened compass-local. */ -export async function prepareParityDemoWeek(page: Page) { +export async function prepareParityDemoWeek( + page: Page, + options: PrepareParityDemoWeekOptions = {}, +) { + const dismissDemoBanner = options.dismissDemoBanner ?? true; await page.goto("/week", { waitUntil: "domcontentloaded" }); await page.locator("#timedColumns").waitFor({ timeout: 20_000 }); await seedParityDemoEvents(page); await page.reload({ waitUntil: "domcontentloaded" }); await expectDemoGrid(page); + if (dismissDemoBanner) { + const okay = page.getByRole("button", { name: "Okay" }); + if (await okay.isVisible().catch(() => false)) { + await okay.click(); + } + } await page.locator("#mainGrid").focus(); await page.keyboard.press("t"); await page - .getByRole("button", { name: /Exercise/i }) + .getByRole("button", { name: /Exercise|Morning standup/i }) + .first() .waitFor({ timeout: 15_000 }); } From ec27b0b29213c2453f2d80a88cf84b2e5f9b1f87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:01:10 +0000 Subject: [PATCH 19/25] fix(macos): align settings, demo banner, up next, and delete account with web Co-authored-by: Tyler Dane --- .../Compass/NativeGridKeyboardRouter.swift | 1 + .../Sources/CompassData/DemoSeedFixture.swift | 2 +- ...tiveCalendarRootModel+AccountActions.swift | 29 +++++++++ .../CompassData/NativeCalendarRootModel.swift | 6 ++ .../CompassData/UITestLaunchPolicy.swift | 5 ++ .../DeleteAccountConfirmationDialogView.swift | 62 +++++++++++++++++++ .../CompassUI/DemoEventsBannerView.swift | 22 +++++-- .../EventForm/EventFormCompactDateField.swift | 1 + .../Sources/CompassUI/RootView.swift | 3 + .../SettingsDataActionsSectionView.swift | 11 ++-- .../Settings/SettingsWindowOverlay.swift | 41 +++++++----- .../CompassUI/Sidebar/UpNextBanner.swift | 16 ++++- 12 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift diff --git a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift index 1164c8961..664fa36fc 100644 --- a/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift +++ b/apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift @@ -119,6 +119,7 @@ final class NativeGridKeyboardRouter { func handleFlagsChanged(_ event: NSEvent) { let commandDown = event.modifierFlags.contains(.command) if model.settingsStore.isPresented || model.authStore.isModalPresented || model.pendingLogoutConfirmation + || model.pendingDeleteAccountConfirmation || model.invitationPrompt != nil || model.pendingRsvpChoice != nil { modHold.handleFlagsChanged(modifierDown: false) formModHold.handleFlagsChanged(modifierDown: false) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/DemoSeedFixture.swift b/apps/calendar-macos/CompassData/Sources/CompassData/DemoSeedFixture.swift index 3edc4fe5a..4265787f8 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/DemoSeedFixture.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/DemoSeedFixture.swift @@ -152,7 +152,7 @@ public struct DemoSeedFixture: Sendable { isActive: true, isPrimary: true, isVisible: true, - name: "Work", + name: "Compass", provider: "local", timeZone: IANATimeZone(rawValue: timeZone) ) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+AccountActions.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+AccountActions.swift index 800aed630..1da0bd9a5 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+AccountActions.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+AccountActions.swift @@ -48,4 +48,33 @@ extension NativeCalendarRootModel { logoutError = "Could not log out. Try again." } } + + public func requestDeleteAccount() { + guard !isDeletingAccount else { return } + deleteAccountError = nil + pendingDeleteAccountConfirmation = true + } + + public func cancelDeleteAccount() { + guard !isDeletingAccount else { return } + pendingDeleteAccountConfirmation = false + } + + public func confirmDeleteAccount() async { + guard pendingDeleteAccountConfirmation, !isDeletingAccount else { return } + deleteAccountError = nil + isDeletingAccount = true + defer { isDeletingAccount = false } + guard isSignedIn else { + deleteAccountError = "Sign in to delete your Compass account." + return + } + do { + try await UserAPI(client: environment.apiClient).deleteAccount() + pendingDeleteAccountConfirmation = false + try await signOut() + } catch { + deleteAccountError = "Could not delete your account. Try again." + } + } } diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index de0be24b7..8e337bdb6 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -367,6 +367,9 @@ public final class NativeCalendarRootModel { await refreshVisibleRange() await refreshSideband() applyParityCapturePresentationIfNeeded() + if UITestLaunchPolicy.showPageJumpHintsForUITest { + setPageJumpHintsVisible(true) + } if isSignedIn { startEventStream() } @@ -412,6 +415,9 @@ public final class NativeCalendarRootModel { public var onExportLocalData: ((Data, String) async throws -> Bool)? public internal(set) var isExportingLocalData = false public internal(set) var pendingLogoutConfirmation = false + public internal(set) var pendingDeleteAccountConfirmation = false + public internal(set) var isDeletingAccount = false + public internal(set) var deleteAccountError: String? public internal(set) var isLoggingOut = false public internal(set) var logoutError: String? diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift index 36a64d4b5..10abd1a66 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift @@ -55,6 +55,11 @@ public enum UITestLaunchPolicy { arguments.contains("-COMPASS_UI_TEST_OPEN_FOCUSED_EVENT_FORM") } + /// Pins page-jump chips visible for mod-hold parity PNGs (no key hold in XCUITest). + public static var showPageJumpHintsForUITest: Bool { + ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_SHOW_PAGE_JUMP_HINTS") + } + /// Desktop ↔ web screenshot capture (`NativeParityCaptureTests`). public static var isParityCapture: Bool { ProcessInfo.processInfo.environment["PARITY_CAPTURE"] == "1" diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift new file mode 100644 index 000000000..34684ee84 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift @@ -0,0 +1,62 @@ +import CompassData +import SwiftUI + +struct DeleteAccountConfirmationDialogView: View { + @Environment(\.nativeWebTheme) private var theme + @Bindable var model: NativeCalendarRootModel + @State private var typedPhrase = "" + + private static let confirmPhrase = "Delete my Compass account" + + var body: some View { + if model.pendingDeleteAccountConfirmation { + VStack(alignment: .leading, spacing: 16) { + ModalDialogTitle("Delete your Compass account?") + Text( + "This deletes your Compass account and data: your calendars, events, and settings. " + + "It can't be undone.\n\n" + + "If you have a Compass trial or subscription, it is canceled immediately and your " + + "saved payment details are removed. Previous payments aren't refunded automatically.\n\n" + + "Your connected calendars are not affected. Nothing there gets deleted, Compass just " + + "loses its access to them." + ) + .font(.rubik(14)) + .foregroundStyle(theme.textColor) + VStack(alignment: .leading, spacing: 8) { + Text("Type \(Self.confirmPhrase) to confirm") + .font(.rubik(13)) + .foregroundStyle(theme.textMutedColor) + TextField("Confirmation phrase", text: $typedPhrase) + .textFieldStyle(.plain) + .font(.rubik(14)) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(theme.surfaceOverlayColor) + .clipShape(RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault)) + .overlay { + RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault) + .stroke(theme.borderColor, lineWidth: 1) + } + .accessibilityIdentifier("compass-delete-account-phrase") + } + if let error = model.deleteAccountError { + Text(error) + .font(.rubik(14)) + .foregroundStyle(theme.textColor) + } + HStack { + Button("Cancel") { model.cancelDeleteAccount() } + .keyboardShortcut(.cancelAction) + Spacer() + Button(model.isDeletingAccount ? "Deleting…" : "Delete account") { + Task { await model.confirmDeleteAccount() } + } + .keyboardShortcut(.defaultAction) + .disabled(typedPhrase != Self.confirmPhrase || model.isDeletingAccount) + } + } + .accessibilityAddTraits(.isModal) + .modalDialogChrome(maxWidth: 480, identifier: "compass-delete-account-dialog") + } + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift index 1861d9838..e52301ff7 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift @@ -10,13 +10,23 @@ public struct DemoEventsBannerView: View { public var body: some View { HStack(spacing: 12) { - Text("Sample events to help you explore. Clear them from the command palette.") - .font(.rubik(12)) - .foregroundStyle(theme.textMutedColor) + HStack(spacing: 6) { + Text("Sample events to help you explore. Clear them from the command palette") + .font(.rubik(12)) + .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: "⌘") + ShortcutHintChip(label: "K") + } Spacer(minLength: 8) - Text("Okay") - .font(.rubik(12)) - .foregroundStyle(theme.textColor) + HStack(spacing: 6) { + Text("Okay") + .font(.rubik(12)) + .foregroundStyle(theme.textColor) + ShortcutHintChip(label: "O") + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .accessibilityLabel("Okay") } .padding(.horizontal, 16) .padding(.vertical, 8) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift index 3127949fe..c1b4c58f0 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift @@ -56,6 +56,7 @@ struct EventFormCompactDateField: View { .datePickerStyle(.graphical) .labelsHidden() .padding(12) + .accessibilityIdentifier("compass-event-form-date-picker") } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index a45c8e4fa..c2ffd657e 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -124,6 +124,9 @@ public struct RootView: View { .overlay { LogoutConfirmationDialogView(model: model) } + .overlay { + DeleteAccountConfirmationDialogView(model: model) + } .overlay { DiscardUnsavedDraftDialogView( isPresented: model.pendingDiscardDraftConfirmation, diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift index c61dd993b..104875698 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift @@ -6,15 +6,17 @@ struct SettingsDataActionsSectionView: View { @Bindable var model: NativeCalendarRootModel var body: some View { - VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 12) { Button(model.isExportingLocalData ? "Exporting…" : "Export data") { Task { await model.exportLocalData() } } .accessibilityIdentifier("settings-export-data") .disabled(model.isExportingLocalData) - Text("Export user-created events stored on this Mac. Synced calendar caches and sample events are excluded.") - .font(.rubik(12)) - .foregroundStyle(theme.textMutedColor) + Button("Delete account") { + model.requestDeleteAccount() + } + .accessibilityIdentifier("settings-delete-account") + .foregroundStyle(theme.errorColor) if model.isSignedIn { Button("Log out") { model.requestLogout() } .accessibilityIdentifier("settings-log-out") @@ -24,4 +26,3 @@ struct SettingsDataActionsSectionView: View { .accessibilityIdentifier("settings-section-data-actions") } } - diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index 8a17d9902..07d75d4d3 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -17,15 +17,30 @@ public struct SettingsWindowOverlay: View { theme.panelBackdropColor .ignoresSafeArea() .onTapGesture { model.closeSettingsIfAllowed() } - HStack(alignment: .top, spacing: 0) { - nav - Divider().overlay(theme.borderColor) - content + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center) { + Text("Settings") + .font(.rubik(18)) + .foregroundStyle(theme.textColor) + Spacer(minLength: 8) + Button("Close") { model.closeSettingsIfAllowed() } + .buttonStyle(.plain) + .font(.rubik(14)) + .foregroundStyle(theme.textMutedColor) + } + .padding(.horizontal, 20) + .padding(.top, 16) + .padding(.bottom, 8) + HStack(alignment: .top, spacing: 0) { + nav + Divider().overlay(theme.borderColor) + content + } } .frame( - width: min(640, max(0, geometry.size.width - 32)), - height: min(620, max(0, geometry.size.height - 32)) - ) + width: min(640, max(0, geometry.size.width - 32)), + height: min(620, max(0, geometry.size.height - 32)) + ) .overlayPanelChrome() } } @@ -40,9 +55,6 @@ public struct SettingsWindowOverlay: View { private var nav: some View { VStack(alignment: .leading, spacing: 8) { - Text("Settings") - .font(.rubik(18)) - .foregroundStyle(theme.textColor) navButton("Accounts", page: .accounts) if model.settingsHasMeetings { navButton("Meetings", page: .booking, attention: model.bookingStore.bookingNeedsAttention) @@ -55,20 +67,17 @@ public struct SettingsWindowOverlay: View { Text("Hold") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) - ShortcutHintChip(label: "Mod") + ShortcutHintChip(label: "^") Text("to see shortcuts.") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) } .padding(.top, 4) } - Spacer() - Button("Close") { model.closeSettingsIfAllowed() } - .buttonStyle(.plain) - .foregroundStyle(theme.textMutedColor) + Spacer(minLength: 0) } .padding(20) - .frame(width: 180, alignment: .leading) + .frame(width: 128, alignment: .leading) } private func navButton(_ title: String, page: SettingsPage, attention: Bool = false) -> some View { diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextBanner.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextBanner.swift index 0b1dd4a00..a8548a200 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextBanner.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/UpNextBanner.swift @@ -38,16 +38,22 @@ struct UpNextBanner: View { } } Spacer(minLength: 8) - Button(state.conferenceURL != nil ? "Join" : "Open") { + Button { if state.conferenceURL != nil { onJoin() } else { onOpen() } + } label: { + HStack(spacing: 6) { + Text(state.conferenceURL != nil ? "Join" : "Open") + ShortcutHintChip( + label: state.conferenceURL != nil ? "V" : "N") + } } .buttonStyle(.borderedProminent) .tint(theme.accentSecondaryColor) - Button("Dismiss") { + Button { withAnimation { isClosing = true } @@ -55,9 +61,13 @@ struct UpNextBanner: View { dismissedEventId = upNext.id isClosing = false } + } label: { + Text("×") + .font(.rubik(16)) + .foregroundStyle(theme.textMutedColor) + .padding(.horizontal, 4) } .buttonStyle(.plain) - .foregroundStyle(theme.textMutedColor) .accessibilityLabel("Dismiss") } .padding(.horizontal, 12) From 48701cb88b992e9a657cb85d2056340ee78ba0fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:18:18 +0000 Subject: [PATCH 20/25] fix(macos): import CompassKit in delete account dialog Co-authored-by: Tyler Dane --- .../Sources/CompassUI/DeleteAccountConfirmationDialogView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift index 34684ee84..c26a3ba4b 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/DeleteAccountConfirmationDialogView.swift @@ -1,4 +1,5 @@ import CompassData +import CompassKit import SwiftUI struct DeleteAccountConfirmationDialogView: View { From e8283307dbbbbad80f7322f9625dfe4556ed54aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:36:19 +0000 Subject: [PATCH 21/25] fix(macos): open date picker for parity capture in UI tests Co-authored-by: Tyler Dane --- .../Sources/CompassData/UITestLaunchPolicy.swift | 5 +++++ .../CompassUI/EventForm/EventFormCompactDateField.swift | 5 +++++ .../CompassUITests/NativeParityCaptureTests.swift | 7 ++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift index 10abd1a66..f67fb24a8 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/UITestLaunchPolicy.swift @@ -60,6 +60,11 @@ public enum UITestLaunchPolicy { ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_SHOW_PAGE_JUMP_HINTS") } + /// Opens the start-date popover when the event form appears (parity PNG capture). + public static var openEventFormDatePickerForUITest: Bool { + ProcessInfo.processInfo.arguments.contains("-COMPASS_UI_TEST_OPEN_EVENT_FORM_DATE_PICKER") + } + /// Desktop ↔ web screenshot capture (`NativeParityCaptureTests`). public static var isParityCapture: Bool { ProcessInfo.processInfo.environment["PARITY_CAPTURE"] == "1" diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift index c1b4c58f0..3ba6603ea 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift @@ -1,3 +1,4 @@ +import CompassData import CompassKit import SwiftUI @@ -13,6 +14,9 @@ struct EventFormCompactDateField: View { _date = date self.accessibilityIdentifier = accessibilityIdentifier _text = State(initialValue: EventFormDateFormat.formatLabel(for: date.wrappedValue)) + let openPicker = UITestLaunchPolicy.openEventFormDatePickerForUITest + && accessibilityIdentifier == "compass-event-form-start-date" + _showsCalendar = State(initialValue: openPicker) } var body: some View { @@ -56,6 +60,7 @@ struct EventFormCompactDateField: View { .datePickerStyle(.graphical) .labelsHidden() .padding(12) + .accessibilityElement(children: .contain) .accessibilityIdentifier("compass-event-form-date-picker") } } diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 64d3d9cee..03bd5cfb1 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -98,14 +98,11 @@ final class NativeParityCaptureTests: XCTestCase { @MainActor private func captureEventFormDatePicker() { - let window = launchDemoWindow() + let window = launchDemoWindow(extraLaunchArgs: ["-COMPASS_UI_TEST_OPEN_EVENT_FORM_DATE_PICKER"]) prepareParityGrid(window, dismissDemoBanner: true) window.typeKey("c", modifierFlags: []) - let startDate = window.descendants(matching: .any)["compass-event-form-start-date"] - XCTAssertTrue(startDate.waitForExistence(timeout: 8)) - startDate.doubleTap() XCTAssertTrue( - window.descendants(matching: .any)["compass-event-form-date-picker"].waitForExistence(timeout: 8)) + window.descendants(matching: .any)["compass-event-form-date-picker"].waitForExistence(timeout: 12)) NativeImageSnapshotSupport.captureWindow(window, named: "event-form-date-picker") } From 5973c73a652f480a12391e73a9fcb71d724a24e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:45:21 +0000 Subject: [PATCH 22/25] fix(macos): polish settings actions and dismiss scroll hint in parity capture Co-authored-by: Tyler Dane --- .../CompassUI/Settings/SettingsDataActionsSectionView.swift | 5 ++++- .../Sources/CompassUI/Settings/SettingsWindowOverlay.swift | 2 ++ .../CompassUITests/NativeParityCaptureTests.swift | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift index 104875698..4ce01f579 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift @@ -10,13 +10,16 @@ struct SettingsDataActionsSectionView: View { Button(model.isExportingLocalData ? "Exporting…" : "Export data") { Task { await model.exportLocalData() } } + .buttonStyle(.bordered) + .tint(theme.borderColor) .accessibilityIdentifier("settings-export-data") .disabled(model.isExportingLocalData) Button("Delete account") { model.requestDeleteAccount() } + .buttonStyle(.borderedProminent) + .tint(theme.errorColor) .accessibilityIdentifier("settings-delete-account") - .foregroundStyle(theme.errorColor) if model.isSignedIn { Button("Log out") { model.requestLogout() } .accessibilityIdentifier("settings-log-out") diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index 07d75d4d3..b8b3a1ee4 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -27,6 +27,8 @@ public struct SettingsWindowOverlay: View { .buttonStyle(.plain) .font(.rubik(14)) .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: "Esc") + .allowsHitTesting(false) } .padding(.horizontal, 20) .padding(.top, 16) diff --git a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift index 03bd5cfb1..d83579907 100644 --- a/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift +++ b/apps/calendar-macos/CompassUITests/NativeParityCaptureTests.swift @@ -196,6 +196,10 @@ final class NativeParityCaptureTests: XCTestCase { XCTAssertTrue( window.buttons["Morning standup"].waitForExistence(timeout: 20), "Expected demo-seed timed event on the week grid") + let dismissScrollHint = window.buttons["Dismiss"] + if dismissScrollHint.waitForExistence(timeout: 2) { + dismissScrollHint.tap() + } sleep(1) } From bb85b29c45b62575073005e1619f1defae926cd5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:49:06 +0000 Subject: [PATCH 23/25] fix(macos): close week grid, sidebar, settings, and form parity gaps Co-authored-by: Tyler Dane --- .../Compass/ParityCaptureWindowPolicy.swift | 9 ++-- .../CompassData/NativeCalendarRootModel.swift | 7 ++++ .../CompassUI/DemoEventsBannerView.swift | 3 +- .../EventForm/EventFormCalendarSection.swift | 5 ++- .../EventForm/EventFormScheduleSection.swift | 2 +- .../CompassUI/EventForm/EventFormView.swift | 29 ++++++++----- .../Sources/CompassUI/RootView.swift | 5 +++ .../SettingsDataActionsSectionView.swift | 1 + .../Settings/SettingsWindowOverlay.swift | 2 +- .../Sidebar/AnonymousCalendarRowView.swift | 13 +++++- .../Sidebar/ShortcutSidebarFooter.swift | 42 ++++++++++++------- .../Sidebar/SidebarMonthPicker.swift | 19 ++++++--- e2e/parity/desktop-web-parity.spec.ts | 4 ++ 13 files changed, 98 insertions(+), 43 deletions(-) diff --git a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift index 4409d5462..0356e1481 100644 --- a/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift +++ b/apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift @@ -12,12 +12,9 @@ enum ParityCaptureWindowPolicy { @MainActor static func applyIfNeeded(to window: NSWindow?) { guard UITestLaunchPolicy.isParityCapture, let window else { return } - let visible = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame - var target = contentSize - if let visible { - target.width = min(target.width, visible.width - 8) - target.height = min(target.height, visible.height - 8) - } + // Always use the canonical parity viewport so five day columns are not + // clipped when the host display is smaller than 1280×820. + let target = contentSize let contentRect = NSRect(origin: .zero, size: target) let frame = window.frameRect(forContentRect: contentRect) if abs(window.frame.width - frame.width) > 2 || abs(window.frame.height - frame.height) > 2 { diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 8e337bdb6..c85e659e7 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -516,6 +516,13 @@ public final class NativeCalendarRootModel { public func updateContentTrackWidth(_ width: CGFloat) { if UITestLaunchPolicy.isParityCapture { applyParityCapturePresentationIfNeeded() + let resolved = max(width, CGFloat(ParityCaptureGeometry.contentTrackWidth)) + if abs(resolved - contentTrackWidth) > 0.5 { + contentTrackWidth = resolved + viewStore.setVisibleDayCount(ParityCaptureGeometry.visibleDayCount) + rebuildPresentation() + scheduleRefreshVisibleRange() + } return } let resolved = max(UITestLaunchPolicy.pinnedWeekGridTrackWidth ?? width, 320) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift index e52301ff7..ddd634f8a 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/DemoEventsBannerView.swift @@ -1,3 +1,4 @@ +import CompassKit import SwiftUI public struct DemoEventsBannerView: View { @@ -14,7 +15,7 @@ public struct DemoEventsBannerView: View { Text("Sample events to help you explore. Clear them from the command palette") .font(.rubik(12)) .foregroundStyle(theme.textMutedColor) - ShortcutHintChip(label: "⌘") + ShortcutHintChip(label: KeycapGlyphs.display(["Mod"])) ShortcutHintChip(label: "K") } Spacer(minLength: 8) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCalendarSection.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCalendarSection.swift index 81f12effc..18766777c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCalendarSection.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCalendarSection.swift @@ -23,7 +23,7 @@ struct EventFormCalendarSection: View { ) ) { ForEach(model.writableEventCalendars, id: \.id) { calendar in - Text(calendar.name).tag(calendar.id) + Text(calendarLabel(calendar)).tag(calendar.id) } } .labelsHidden() @@ -34,4 +34,7 @@ struct EventFormCalendarSection: View { .eventFormSectionChrome(focused: focusField == .calendar) } + private func calendarLabel(_ calendar: CompassCalendar) -> String { + calendar.isPrimary ? "\(calendar.name) (primary)" : calendar.name + } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift index af00a4b0b..b2e4ae8ad 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift @@ -13,7 +13,7 @@ struct EventFormScheduleSection: View { var body: some View { VStack(alignment: .leading, spacing: 6) { - Toggle("All day", isOn: Binding( + Toggle("All day?", isOn: Binding( get: { draft.schedule.kind == .allDay }, set: { model.setDraftAllDay($0) } )) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift index f8b6df48a..37c4d52dd 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift @@ -142,17 +142,10 @@ public struct EventFormView: View { HStack(spacing: 8) { Text("Save") Spacer(minLength: 8) - Text(KeycapGlyphs.display(["Mod", "Enter"])) - .font(.rubik(11, weight: .medium)) - .foregroundStyle(theme.onAccentColor.opacity(0.92)) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(theme.onAccentColor.opacity(0.1)) - .overlay { - RoundedRectangle(cornerRadius: 4) - .stroke(theme.onAccentColor.opacity(0.35), lineWidth: 1) - } - .clipShape(RoundedRectangle(cornerRadius: 4)) + HStack(spacing: 4) { + saveKeycap(KeycapGlyphs.display(["Mod"])) + saveKeycap("Enter") + } } .font(.rubik(14, weight: .medium)) .foregroundStyle(theme.onAccentColor) @@ -188,4 +181,18 @@ public struct EventFormView: View { private func isLogisticsFieldFocused(_ field: EventFormField) -> Bool { field == .location || field == .conference || field == .attendees } + + private func saveKeycap(_ label: String) -> some View { + Text(label) + .font(.rubik(11, weight: .medium)) + .foregroundStyle(theme.onAccentColor.opacity(0.92)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(theme.onAccentColor.opacity(0.1)) + .overlay { + RoundedRectangle(cornerRadius: 4) + .stroke(theme.onAccentColor.opacity(0.35), lineWidth: 1) + } + .clipShape(RoundedRectangle(cornerRadius: 4)) + } } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index c2ffd657e..3be6ce68a 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -27,6 +27,11 @@ public struct RootView: View { } content .padding(.leading, contentLeadingInset) + .frame( + minWidth: UITestLaunchPolicy.isParityCapture + ? CGFloat(ParityCaptureGeometry.contentTrackWidth) + : 0 + ) } // Web: Day/Week keep the panel mounted for event details even // when the sidebar preference is closed. diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift index 4ce01f579..7a87cab6a 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsDataActionsSectionView.swift @@ -19,6 +19,7 @@ struct SettingsDataActionsSectionView: View { } .buttonStyle(.borderedProminent) .tint(theme.errorColor) + .foregroundStyle(theme.onAccentColor) .accessibilityIdentifier("settings-delete-account") if model.isSignedIn { Button("Log out") { model.requestLogout() } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift index b8b3a1ee4..cfa7441be 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Settings/SettingsWindowOverlay.swift @@ -69,7 +69,7 @@ public struct SettingsWindowOverlay: View { Text("Hold") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) - ShortcutHintChip(label: "^") + ShortcutHintChip(label: KeycapGlyphs.display(["Mod"])) Text("to see shortcuts.") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift index f99f85662..770520282 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/AnonymousCalendarRowView.swift @@ -11,7 +11,18 @@ struct AnonymousCalendarRowView: View { Button { showsInfo.toggle() } label: { HStack(spacing: 8) { Circle() - .fill(EventCardColorParser.nsColor(hex: calendar.backgroundColor).map(Color.init(nsColor:)) ?? theme.textColor) + .fill( + EventCardColorParser.nsColor(hex: calendar.backgroundColor).map(Color.init(nsColor:)) + ?? theme.textColor + ) + .overlay { + Circle() + .strokeBorder( + EventCardColorParser.nsColor(hex: calendar.backgroundColor).map(Color.init(nsColor:)) + ?? theme.textColor, + lineWidth: 2 + ) + } .frame(width: 14, height: 14) Text("On this device only") .font(.rubik(12)) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift index 3c53bd964..88314ec37 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift @@ -1,4 +1,5 @@ import CompassData +import CompassKit import SwiftUI struct ShortcutSidebarFooter: View { @@ -26,22 +27,31 @@ struct ShortcutSidebarFooter: View { if isEventFormVisible, !levelsStore.tipsMuted, !suppressTips, !suppressForQuickTime { eventFormSaveHint - } else if !levelsStore.badgeHidden, !suppressTips, !suppressForQuickTime, - let tip = levelsStore.currentTip - { - HStack(spacing: 8) { - Text("Try \(tip.label)") - .font(.rubik(11)) - .foregroundStyle(theme.textMutedColor) - .lineLimit(2) - .accessibilityIdentifier("compass-native-shortcut-tip") - HStack(spacing: 3) { - ForEach(Array(tip.keycaps.enumerated()), id: \.offset) { _, key in - ShortcutHintChip(label: key) - } - } - } + } else if !levelsStore.tipsMuted, !suppressTips, !suppressForQuickTime { + calendarJumpHint + } + } + } + + private var calendarJumpHint: some View { + VStack(alignment: .center, spacing: 2) { + HStack(spacing: 4) { + Text("Hold") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + ShortcutHintChip(label: KeycapGlyphs.display(["Mod"])) + Text("to see jump targets") + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) + } + .frame(maxWidth: .infinity) + .accessibilityIdentifier("compass-native-shortcut-tip") + Button("Hide tips") { + levelsStore.setTipsMuted(true) } + .buttonStyle(.plain) + .font(.rubik(11)) + .foregroundStyle(theme.textMutedColor) } } @@ -50,7 +60,7 @@ struct ShortcutSidebarFooter: View { Text("Enter saves · hold") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) - ShortcutHintChip(label: "Mod") + ShortcutHintChip(label: KeycapGlyphs.display(["Mod"])) Text("to jump fields") .font(.rubik(11)) .foregroundStyle(theme.textMutedColor) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift index fc5d47fe8..aea06cd61 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift @@ -63,11 +63,9 @@ public struct SidebarMonthPicker: View { } .frame(height: 32.333) .background { - if days.contains(where: { isInRange($0, from: selectedDate) }) - || (visibleDayCount > 1 && days.contains(where: { isInRange($0, from: focusedDate ?? selectedDate) })) - { + if rowContainsSelectedWeek(days) { RoundedRectangle(cornerRadius: 4) - .fill(theme.textMutedColor.opacity(0.15)) + .fill(theme.textMutedColor.opacity(0.12)) } } .overlay { @@ -144,8 +142,9 @@ public struct SidebarMonthPicker: View { return formatter.string(from: displayedMonth) } + /// Web `MonthPicker`: Sunday-first columns for every view wider than a single day. private var weekStart: Int { - visibleDayCount == 1 ? 1 : EffectiveTimeZone.calendar.component(.weekday, from: selectedDate) + 1 } private var dayCells: [Date] { @@ -169,6 +168,16 @@ public struct SidebarMonthPicker: View { return date >= start && date < end } + /// Matches web `react-datepicker__week--selected` for the week containing the anchor day. + private func rowContainsSelectedWeek(_ days: [Date]) -> Bool { + let calendar = EffectiveTimeZone.calendar + let anchor = calendar.startOfDay(for: selectedDate) + guard let rowStart = days.first.map({ calendar.startOfDay(for: $0) }), + let rowEnd = days.last.flatMap({ calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: $0)) }) + else { return false } + return anchor >= rowStart && anchor < rowEnd + } + private func shiftMonth(_ offset: Int) { if let next = EffectiveTimeZone.calendar.date(byAdding: .month, value: offset, to: displayedMonth) { displayedMonth = next diff --git a/e2e/parity/desktop-web-parity.spec.ts b/e2e/parity/desktop-web-parity.spec.ts index ac6db2ca9..41333db10 100644 --- a/e2e/parity/desktop-web-parity.spec.ts +++ b/e2e/parity/desktop-web-parity.spec.ts @@ -11,11 +11,15 @@ const outputDir = process.env.PARITY_WEB_OUT ?? join(process.cwd(), "build", "desktop-parity", "web"); +const macChromeUserAgent = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + test.use({ viewport: PARITY_VIEWPORT, storageState: { cookies: [], origins: [] }, timezoneId: "UTC", colorScheme: "light", + userAgent: macChromeUserAgent, }); test.describe("Desktop web parity captures", () => { From 8502095156ee467259b343cc9f96b7f5eb3971d0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 04:51:45 +0000 Subject: [PATCH 24/25] test(parity): use Meta shortcuts with mac chrome user agent Co-authored-by: Tyler Dane --- e2e/parity/desktop-web-parity.spec.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/e2e/parity/desktop-web-parity.spec.ts b/e2e/parity/desktop-web-parity.spec.ts index 41333db10..def508024 100644 --- a/e2e/parity/desktop-web-parity.spec.ts +++ b/e2e/parity/desktop-web-parity.spec.ts @@ -14,6 +14,9 @@ const outputDir = const macChromeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; +/** Parity captures use a Mac Chrome UA so Mod renders as ⌘ like native macOS. */ +const modKey = "Meta"; + test.use({ viewport: PARITY_VIEWPORT, storageState: { cookies: [], origins: [] }, @@ -55,10 +58,10 @@ test.describe("Desktop web parity captures", () => { test("mod-hold-overlay", async ({ page }) => { await prepareParityDemoWeek(page); await page.locator("#mainGrid").focus(); - await page.keyboard.down("Control"); + await page.keyboard.down(modKey); await page.waitForTimeout(600); await page.screenshot({ path: join(outputDir, "mod-hold-overlay.png") }); - await page.keyboard.up("Control"); + await page.keyboard.up(modKey); }); test("up-next-banner", async ({ page }) => { @@ -113,7 +116,7 @@ test.describe("Desktop web parity captures", () => { test("command-palette", async ({ page }) => { await prepareParityDemoWeek(page); - await page.keyboard.press("Control+K"); + await page.keyboard.press(`${modKey}+K`); await expect(page.getByLabel("Command palette search")).toBeVisible({ timeout: 10_000, }); @@ -122,7 +125,7 @@ test.describe("Desktop web parity captures", () => { test("settings", async ({ page }) => { await prepareParityDemoWeek(page); - await page.keyboard.press("Control+,"); + await page.keyboard.press(`${modKey}+,`); await expect(page.getByRole("button", { name: "Accounts" })).toBeVisible({ timeout: 10_000, }); From 703572d7474736f9895a1af3a710f4ee3a0a759e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 05:13:41 +0000 Subject: [PATCH 25/25] fix(macos): pass GridMetrics into quick-time busy period layout Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel+EventJump.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift index e5903c4b7..8f8bea79a 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel+EventJump.swift @@ -128,7 +128,11 @@ extension NativeCalendarRootModel { let slots = QuickTime.buildQuickTimeSlots(busy: busy, targetDay: targetDay) let visibleDates = GridVisibleDate.fromKeys(timeGridState.scenario.visibleDateKeys) let colWidths = timeGridState.resolvedColumnWidths() - let metrics = timeGridState.snapshot(colWidths: colWidths).metrics + let snapshotMetrics = timeGridState.snapshot(colWidths: colWidths).metrics + let gridMetrics = GridMetrics( + colWidths: snapshotMetrics.colWidths, + hourHeight: snapshotMetrics.hourHeight + ) let columnIndex: Int? = { let key = CompassDateParsing.formatCalendarDay(targetDay) return visibleDates.firstIndex(where: { $0.key == key }) @@ -137,7 +141,7 @@ extension NativeCalendarRootModel { let frame = EventPositionCalculator.getBusyPeriodPosition( segmentStart: slot.start, segmentEnd: slot.end, - measurements: metrics, + measurements: gridMetrics, visibleDates: visibleDates, columnIndex: columnIndex, hasSecondaryTimeZone: timeGridState.hasSecondaryTimeZone