diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift index e609ca474..90affa787 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/CommandPalette/CommandPaletteCatalog.swift @@ -10,7 +10,10 @@ public enum CommandPaletteCatalog { isSignedIn: Bool, isTrialing: Bool, eventHits: [CommandPaletteEventHit], - recentIds: [String] + recentIds: [String], + tipsMuted: Bool, + levelHidden: Bool, + theme: CompassThemeName ) -> [CommandPaletteSection] { var built: [CommandPaletteSection] = [] @@ -18,7 +21,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)) } @@ -43,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", @@ -89,14 +95,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 +113,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 +122,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 +131,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 +189,8 @@ public enum CommandPaletteCatalog { id: "recent-" + id, label: item.label, shortcut: item.shortcut, - keywords: item.keywords) + keywords: item.keywords, + systemImage: item.systemImage) } } @@ -167,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", @@ -181,25 +231,21 @@ public enum CommandPaletteCatalog { 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", - ]), + 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", 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..4c35c0f2c 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarOverlays.swift @@ -30,7 +30,10 @@ extension NativeCalendarRootModel { isSignedIn: isSignedIn, isTrialing: billingStore.status?.subscriptionStatus == .trialing, eventHits: mergedHits, - recentIds: commandPaletteStore.recentCommandIds) + recentIds: commandPaletteStore.recentCommandIds, + tipsMuted: levelsStore.tipsMuted, + levelHidden: levelsStore.badgeHidden, + theme: settingsStore.theme) } public func filteredPaletteSections() -> [CommandPaletteSection] { @@ -114,12 +117,22 @@ 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": 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/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index b86eebf5d..ddbb68a21 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,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(CalendarWindowMath.sundayWeekViewStart(containing: today)) monthPickerMonth = today pendingScroll = .scrollToNowOrPulse rebuildPresentation() @@ -922,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/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/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/CommandPaletteActionsTests.swift index a92377fc3..2fee754a7 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/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift index 5bd3f31f2..8b8ee28aa 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/Timezone/TimeZoneFormatting.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift index dd8a19ed2..fd9986d53 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 df862fda0..c1420df43 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 36f67e497..f346efec5 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -7,4 +7,5 @@ final class ParityCaptureGeometryTests: XCTestCase { XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1) XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } + } diff --git a/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift b/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift new file mode 100644 index 000000000..19880cc48 --- /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") + } +} 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 { 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 b52a8c04a..345ace199 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,