From d01905b65c2e9825f6367bb1d07b3f757040180a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:12:06 +0000 Subject: [PATCH 1/5] fix(macos): align parity week anchor with web Sunday start Co-authored-by: Tyler Dane --- .../Sources/CompassData/NativeCalendarRootModel.swift | 4 +--- .../CompassKit/Layout/ParityCaptureGeometry.swift | 11 +++++++++++ .../CompassKitTests/ParityCaptureGeometryTests.swift | 8 ++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index b86eebf5de..7ff96df611 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -372,9 +372,7 @@ public final class NativeCalendarRootModel { contentTrackWidth = CGFloat(ParityCaptureGeometry.contentTrackWidth) viewStore.setVisibleDayCount(ParityCaptureGeometry.visibleDayCount) let today = referenceNow - let calendar = EffectiveTimeZone.calendar - let weekStart = calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today - viewStore.setAnchorDate(weekStart) + viewStore.setAnchorDate(ParityCaptureGeometry.weekViewStart(containing: today)) monthPickerMonth = today pendingScroll = .scrollToNowOrPulse rebuildPresentation() diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift index 1b8253ae75..8c02bd3c04 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift @@ -21,4 +21,15 @@ public enum ParityCaptureGeometry { marginLeft: gridMarginLeft ) } + + /// Web week view uses Day.js `startOf("week")` (Sunday) in the demo timezone. + public static func weekViewStart(containing date: Date) -> Date { + var calendar = EffectiveTimeZone.calendar + calendar.firstWeekday = 1 + calendar.minimumDaysInFirstWeek = 1 + let startOfDay = calendar.startOfDay(for: date) + let weekday = calendar.component(.weekday, from: startOfDay) + let daysFromSunday = (weekday - calendar.firstWeekday + 7) % 7 + return calendar.date(byAdding: .day, value: -daysFromSunday, to: startOfDay) ?? startOfDay + } } diff --git a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift index 36f67e497f..e73343f37d 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -7,4 +7,12 @@ final class ParityCaptureGeometryTests: XCTestCase { XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1) XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } + + func testWeekViewStartMatchesWebSundayWeek() throws { + EffectiveTimeZone.identifier = "UTC" + let reference = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-06-10T15:00:00.000Z")) + let weekStart = ParityCaptureGeometry.weekViewStart(containing: reference) + XCTAssertEqual(CompassDateParsing.formatCalendarDay(weekStart), "2026-06-07") + } } From d96e42fc7388bd288f6b66a85e1adeabce9aeaef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:17:13 +0000 Subject: [PATCH 2/5] fix(macos): use Sunday week start and UTC labels like web Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 7 ++- .../Calendar/CalendarWindowMath.swift | 11 +++++ .../Layout/ParityCaptureGeometry.swift | 11 ----- .../Timezone/TimeZoneFormatting.swift | 45 +++++++++++++++++-- .../CalendarWindowMathTests.swift | 7 +++ .../ParityCaptureGeometryTests.swift | 7 --- .../TimeZoneFormattingTests.swift | 18 ++++++++ 7 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 7ff96df611..ddbb68a21c 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -174,7 +174,7 @@ public final class NativeCalendarRootModel { let anchor: Date = { let today = demoPresentation?.referenceNow ?? now() - return EffectiveTimeZone.calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today + return CalendarWindowMath.sundayWeekViewStart(containing: today) }() viewStore = ViewStore( view: .week, @@ -372,7 +372,7 @@ public final class NativeCalendarRootModel { contentTrackWidth = CGFloat(ParityCaptureGeometry.contentTrackWidth) viewStore.setVisibleDayCount(ParityCaptureGeometry.visibleDayCount) let today = referenceNow - viewStore.setAnchorDate(ParityCaptureGeometry.weekViewStart(containing: today)) + viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) monthPickerMonth = today pendingScroll = .scrollToNowOrPulse rebuildPresentation() @@ -920,8 +920,7 @@ public final class NativeCalendarRootModel { if viewStore.visibleDayCount == 1 { viewStore.setAnchorDate(today) } else { - let weekStart = calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today - viewStore.setAnchorDate(weekStart) + viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) } monthPickerMonth = today rebuildPresentation() diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift index 5bd3f31f21..8b8ee28aa9 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift @@ -3,6 +3,17 @@ import Foundation public enum CalendarWindowMath { public static let weekDayCount = 7 + /// Web Day.js `startOf("week")`: Sunday 00:00 in the effective timezone. + public static func sundayWeekViewStart(containing date: Date) -> Date { + var calendar = EffectiveTimeZone.calendar + calendar.firstWeekday = 1 + calendar.minimumDaysInFirstWeek = 1 + let startOfDay = calendar.startOfDay(for: date) + let weekday = calendar.component(.weekday, from: startOfDay) + let daysFromSunday = (weekday - calendar.firstWeekday + 7) % 7 + return calendar.date(byAdding: .day, value: -daysFromSunday, to: startOfDay) ?? startOfDay + } + /// Seven local days from the anchor's start of day; `end` is exclusive next midnight. public static func weekEventsQueryWindow(anchor: Date) -> DateRange { let calendar = EffectiveTimeZone.calendar diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift index 8c02bd3c04..1b8253ae75 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Layout/ParityCaptureGeometry.swift @@ -21,15 +21,4 @@ public enum ParityCaptureGeometry { marginLeft: gridMarginLeft ) } - - /// Web week view uses Day.js `startOf("week")` (Sunday) in the demo timezone. - public static func weekViewStart(containing date: Date) -> Date { - var calendar = EffectiveTimeZone.calendar - calendar.firstWeekday = 1 - calendar.minimumDaysInFirstWeek = 1 - let startOfDay = calendar.startOfDay(for: date) - let weekday = calendar.component(.weekday, from: startOfDay) - let daysFromSunday = (weekday - calendar.firstWeekday + 7) % 7 - return calendar.date(byAdding: .day, value: -daysFromSunday, to: startOfDay) ?? startOfDay - } } diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift index dd8a19ed20..fd9986d537 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift @@ -5,12 +5,22 @@ public enum TimeZoneFormatting { TimeZone(identifier: identifier) != nil } + /// Matches web `formatTimeZoneAbbreviation` (Intl short, offset fallback). public static func abbreviation(for timeZoneId: String, at date: Date = Date()) -> String { guard let zone = TimeZone(identifier: timeZoneId) else { return timeZoneId } - let formatter = DateFormatter() - formatter.timeZone = zone - formatter.dateFormat = "z" - return formatter.string(from: date) + if timeZoneId == "UTC" { + return "UTC" + } + + let short = timeZoneNamePart(timeZone: zone, at: date, style: .short) + if let short, !looksLikeIanaId(short) { + return short + } + + if let offset = timeZoneNamePart(timeZone: zone, at: date, style: .shortOffset) { + return offset + } + return short ?? timeZoneId } public static func cityName(for timeZoneId: String) -> String { @@ -18,4 +28,31 @@ public enum TimeZoneFormatting { guard let city = parts.last else { return timeZoneId } return city.replacingOccurrences(of: "_", with: " ") } + + private enum NameStyle { + case short + case shortOffset + } + + private static func timeZoneNamePart( + timeZone: TimeZone, + at date: Date, + style: NameStyle + ) -> String? { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US") + formatter.timeZone = timeZone + switch style { + case .short: + formatter.dateFormat = "zzz" + case .shortOffset: + formatter.dateFormat = "ZZZZZ" + } + let value = formatter.string(from: date) + return value.isEmpty ? nil : value + } + + private static func looksLikeIanaId(_ value: String) -> Bool { + value.contains("/") + } } diff --git a/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift b/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift index df862fda0f..c1420df432 100644 --- a/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift +++ b/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift @@ -25,6 +25,13 @@ final class CalendarWindowMathTests: XCTestCase { ) } + func testSundayWeekViewStartMatchesWeb() throws { + let reference = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-06-10T15:00:00.000Z")) + let weekStart = CalendarWindowMath.sundayWeekViewStart(containing: reference) + XCTAssertEqual(CompassDateParsing.formatCalendarDay(weekStart), "2026-06-07") + } + func testDayEventQueryRangeUsesExclusiveEnd() throws { let anchor = try XCTUnwrap( CompassDateParsing.calendarDateInEffectiveTimeZone("2026-06-28") diff --git a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift index e73343f37d..f346efec5e 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -8,11 +8,4 @@ final class ParityCaptureGeometryTests: XCTestCase { XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } - func testWeekViewStartMatchesWebSundayWeek() throws { - EffectiveTimeZone.identifier = "UTC" - let reference = try XCTUnwrap( - CompassDateParsing.parseInEffectiveTimeZone("2026-06-10T15:00:00.000Z")) - let weekStart = ParityCaptureGeometry.weekViewStart(containing: reference) - XCTAssertEqual(CompassDateParsing.formatCalendarDay(weekStart), "2026-06-07") - } } diff --git a/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift b/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift new file mode 100644 index 0000000000..19880cc48c --- /dev/null +++ b/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift @@ -0,0 +1,18 @@ +import CompassKit +import XCTest + +final class TimeZoneFormattingTests: XCTestCase { + func testUTCLabelMatchesWeb() throws { + let at = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-07-15T12:00:00.000Z")) + XCTAssertEqual(TimeZoneFormatting.abbreviation(for: "UTC", at: at), "UTC") + } + + func testNamedZoneUsesShortAbbreviation() throws { + let summer = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-07-15T18:00:00.000Z")) + XCTAssertEqual( + TimeZoneFormatting.abbreviation(for: "America/Denver", at: summer), + "MDT") + } +} From ad83115c7dba5ecf087ce23562003140da53ad35 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:23:35 +0000 Subject: [PATCH 3/5] 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 e609ca4746..d183029766 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 3a4fbdda6a..e5131c8e66 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 5972a530e6..b046f36f7c 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 d89c7b4a21..b1509980aa 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 1f1e7ac450..0672d1042d 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 14c9b7eba1..520870b3c3 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 cf9224bceb551fd66dcdd3a148c5d3e7302560b9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:30:05 +0000 Subject: [PATCH 4/5] fix(macos): align sidebar tips, Up Next hints, and theme palette row with web Co-authored-by: Tyler Dane --- .../CommandPaletteCatalog.swift | 14 +++++++++++--- .../CompassData/NativeCalendarOverlays.swift | 7 ++++++- .../CommandPaletteActionsTests.swift | 10 ++++++++++ .../Sources/CompassUI/RootView.swift | 5 ++++- .../Sidebar/ShortcutSidebarFooter.swift | 19 ++++++++++++++++++- .../CompassUI/Sidebar/UpNextCard.swift | 8 ++++++++ 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift index d183029766..90affa7872 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 b046f36f7c..4c35c0f2c0 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/Tests/CompassDataTests/CommandPaletteActionsTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift index a92377fc3c..2fee754a75 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift @@ -107,6 +107,16 @@ 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) + model.runPaletteCommand(id: "toggle-theme") + XCTAssertEqual(model.settingsStore.theme, initial) } func testAboutInvokesNativePresentation() async throws { diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index 6ae7fa5b87..f1c9b271c0 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -193,7 +193,10 @@ public struct RootView: View { SidebarSyncStatusLine(model: model) .padding(.horizontal, 16) .padding(.bottom, 4) - ShortcutSidebarFooter(levelsStore: model.levelsStore) + ShortcutSidebarFooter( + levelsStore: model.levelsStore, + isEventFormVisible: model.isEventFormVisible, + suppressTips: model.eventJumpStatusText != nil) .padding(.horizontal, 16) .padding(.bottom, 8) sidebarActions diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift index 44d1f72a37..803dd19063 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift @@ -4,6 +4,8 @@ import SwiftUI struct ShortcutSidebarFooter: View { @Environment(\.nativeWebTheme) private var theme @Bindable var levelsStore: LevelsStore + var isEventFormVisible: Bool = false + var suppressTips: Bool = false var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -21,7 +23,9 @@ struct ShortcutSidebarFooter: View { } } - if !levelsStore.badgeHidden, let tip = levelsStore.currentTip { + if isEventFormVisible, !levelsStore.tipsMuted, !suppressTips { + eventFormSaveHint + } else if !levelsStore.badgeHidden, !suppressTips, let tip = levelsStore.currentTip { HStack(spacing: 8) { Text("Try \(tip.label)") .font(.rubik(11)) @@ -37,4 +41,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 ff7b824784..bbde87956d 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") From 3d1b22f6ce95af1e1da27544a707088af5901a5e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 00:33:33 +0000 Subject: [PATCH 5/5] fix(macos): quick-time sidebar status and current-hour gutter accent Co-authored-by: Tyler Dane --- .../Sources/CompassUI/RootView.swift | 6 +++- .../Sidebar/QuickTimeStatusLine.swift | 35 +++++++++++++++++++ .../Sidebar/ShortcutSidebarFooter.swift | 7 ++-- .../CompassUI/TimeGrid/TimeGridView.swift | 13 ++++--- 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/QuickTimeStatusLine.swift diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift index f1c9b271c0..a45c8e4fa3 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/RootView.swift @@ -187,6 +187,9 @@ public struct RootView: View { } else { sidebarBody } + QuickTimeStatusLine(model: model) + .padding(.horizontal, 16) + .padding(.bottom, 2) EventJumpStatusLine(model: model) .padding(.horizontal, 16) .padding(.bottom, 2) @@ -196,7 +199,8 @@ public struct RootView: View { ShortcutSidebarFooter( levelsStore: model.levelsStore, isEventFormVisible: model.isEventFormVisible, - suppressTips: model.eventJumpStatusText != nil) + 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 0000000000..9d6499b615 --- /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 803dd19063..3c53bd964f 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/ShortcutSidebarFooter.swift @@ -6,6 +6,7 @@ struct ShortcutSidebarFooter: View { @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) { @@ -23,9 +24,11 @@ struct ShortcutSidebarFooter: View { } } - if isEventFormVisible, !levelsStore.tipsMuted, !suppressTips { + if isEventFormVisible, !levelsStore.tipsMuted, !suppressTips, !suppressForQuickTime { eventFormSaveHint - } else if !levelsStore.badgeHidden, !suppressTips, let tip = levelsStore.currentTip { + } else if !levelsStore.badgeHidden, !suppressTips, !suppressForQuickTime, + let tip = levelsStore.currentTip + { HStack(spacing: 8) { Text("Try \(tip.label)") .font(.rubik(11)) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift index b52a8c04aa..345ace199d 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/TimeGrid/TimeGridView.swift @@ -318,6 +318,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( @@ -329,7 +330,8 @@ public final class TimeGridView: NSView { xOffset: 0, width: columnWidth, hourHeight: hourHeight, - palette: palette) + palette: palette, + currentHour: currentHour) } let primaryLabels = MappedHourLabels.labels( @@ -342,7 +344,8 @@ public final class TimeGridView: NSView { xOffset: primaryX, width: columnWidth, hourHeight: hourHeight, - palette: palette) + palette: palette, + currentHour: currentHour) } private func renderHourColumn( @@ -350,13 +353,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,