Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9b069ad
fix(macos): keep parity week gutter and Sunday window pinned
cursoragent Oct 20, 2026
c1bf409
fix(macos): align command palette navigation rows and icons with web
cursoragent Oct 20, 2026
6cd18a0
fix(macos): quick-time sidebar status and current-hour gutter accent
cursoragent Oct 20, 2026
80c560f
fix(macos): quick-time on jump day, settings hint, onboarding keycap …
cursoragent Oct 20, 2026
91b2c74
fix(macos): event jump mode and quick-time slot stack like web
cursoragent Oct 20, 2026
b191735
fix(macos): keep parity week gutter and Sunday window pinned
cursoragent Oct 20, 2026
5545c50
fix(macos): keep now-cue on nav today when anchor unchanged
cursoragent Oct 20, 2026
d768812
fix(macos): match web Enter keycap in palette footer
cursoragent Oct 20, 2026
1773277
fix(macos): expose grid timezone header for parity capture
cursoragent Oct 20, 2026
bb943f1
fix(macos): align demo settings and welcome density with web parity
cursoragent Oct 20, 2026
9ba782a
test(macos): avoid internal parity helper in package tests
cursoragent Oct 20, 2026
9dca9d3
fix(macos): clamp week grid horizontal scroll for gutter parity
cursoragent Oct 20, 2026
ab6d7f7
fix(macos): lock parity capture window to 1280×820
cursoragent Oct 20, 2026
4363d18
test(macos): assert parity viewport height constant
cursoragent Oct 20, 2026
2115c55
fix(macos): expose parity layout probe and improve capture window sizing
cursoragent Oct 20, 2026
bbd18fe
merge(macos): stack quick-time on week grid parity branch
cursoragent Oct 20, 2026
47d6cf6
fix(macos): drop flaky parity layout probe assert
cursoragent Oct 20, 2026
34effa1
fix(macos): keep parity week gutter and Sunday window pinned (#4619)
tyler-dane Oct 20, 2026
aec0a87
test(parity): capture allday row, banners, mod-hold, and pickers
cursoragent Oct 20, 2026
ec27b0b
fix(macos): align settings, demo banner, up next, and delete account …
cursoragent Oct 20, 2026
48701cb
fix(macos): import CompassKit in delete account dialog
cursoragent Oct 20, 2026
e828330
fix(macos): open date picker for parity capture in UI tests
cursoragent Oct 20, 2026
5973c73
fix(macos): polish settings actions and dismiss scroll hint in parity…
cursoragent Oct 20, 2026
bb85b29
fix(macos): close week grid, sidebar, settings, and form parity gaps
cursoragent Oct 20, 2026
0ce7b09
merge: quick-time stack parity from #4618
cursoragent Oct 20, 2026
8502095
test(parity): use Meta shortcuts with mac chrome user agent
cursoragent Oct 20, 2026
703572d
fix(macos): pass GridMetrics into quick-time busy period layout
cursoragent Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/scripts/compose-desktop-parity-shots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 14 additions & 1 deletion .github/scripts/record-macos-parity-captures.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 1 addition & 2 deletions apps/calendar-macos/Compass/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 4 additions & 34 deletions apps/calendar-macos/Compass/NativeGridKeyboardRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ShortcutId> = [.navDayView, .navWeekView, .navLifeView]

init(model: NativeCalendarRootModel, registry: ShortcutRegistry) {
Expand All @@ -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<ShortcutId> = [
.editFocusPrev,
.editFocusNext,
Expand Down Expand Up @@ -119,6 +112,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)
Expand Down Expand Up @@ -347,26 +341,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
Expand Down Expand Up @@ -534,6 +512,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
Expand Down Expand Up @@ -798,17 +779,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
Expand Down
4 changes: 4 additions & 0 deletions apps/calendar-macos/Compass/NativeRootController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ final class NativeRootController: NSHostingController<ThemedRootView> {
configureNativeServices()
configureKeyboard()
configureResume()
model.onParityCapturePresentationApplied = { [weak self] in
ParityCaptureWindowPolicy.applyIfNeeded(to: self?.view.window)
}
Task {
await model.start()
deepLinkRouter.markConsumerReady()
Expand All @@ -105,6 +108,7 @@ final class NativeRootController: NSHostingController<ThemedRootView> {

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
Expand Down
39 changes: 39 additions & 0 deletions apps/calendar-macos/Compass/ParityCaptureWindowPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 }
// 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 visible = window.screen?.visibleFrame ?? NSScreen.main?.visibleFrame
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public struct DemoSeedFixture: Sendable {
isActive: true,
isPrimary: true,
isVisible: true,
name: "Work",
name: "Compass",
provider: "local",
timeZone: IANATimeZone(rawValue: timeZone)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -150,6 +151,7 @@ extension NativeCalendarRootModel {
calendarId: defaultTargetCalendarId()
)
draftStore.startGridDraft(activity: .keyboardPlace, draft: draft)
clearEventJumpSelection()
rebuildPresentation()
focusDraftCard()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -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" {
Expand All @@ -36,7 +68,7 @@ extension NativeCalendarRootModel {
if lower == "s" {
eventJumpSelection.setWeekendDisambiguation(announcement: "Weekend")
recordShortcut(.focusWeekDay)
applyFocusPresentation()
refreshEventJumpPresentation()
return
}

Expand All @@ -62,16 +94,63 @@ 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 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 })
}()
return slots.compactMap { slot in
let frame = EventPositionCalculator.getBusyPeriodPosition(
segmentStart: slot.start,
segmentEnd: slot.end,
measurements: gridMetrics,
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):
Expand Down
Loading
Loading