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+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/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..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/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/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/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/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/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) 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,