diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index 87db945b25..c4670c180b 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -587,6 +587,8 @@ public final class NativeCalendarRootModel { isSidebarOpen.toggle() case .focusSidebar: focusSidebar() + case .focusEvent: + focusFirstCalendarEvent() case .focusNotice: focusNextNotice() case .otherSubscribe: @@ -784,6 +786,15 @@ public final class NativeCalendarRootModel { applyFocusPresentation() } + /// Web `onFocusCalendar` / `u`: seed grid focus on the nearest event when nothing is focused. + public func focusFirstCalendarEvent() { + guard viewStore.view != .life else { return } + guard focusStore.focusedEventId == nil else { return } + if let seeded = seedFocusEventId() { + focusEvent(eventId: seeded) + } + } + private func seedFocusEventId() -> String? { let reference = referenceNow if !loadedEvents.isEmpty { diff --git a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelFocusTests.swift b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelFocusTests.swift index 322aadca27..fbd9f3b5e7 100644 --- a/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelFocusTests.swift +++ b/apps/calendar-macos/CompassData/Tests/CompassDataTests/NativeCalendarRootModelFocusTests.swift @@ -55,6 +55,18 @@ final class NativeCalendarRootModelFocusTests: XCTestCase { XCTAssertEqual(model.draftStore.gridDraft?.title, "Morning standup") } + func testFocusEventShortcutSeedsNearestEvent() async throws { + let fixture = try DemoSeedFixture.load() + let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) + let model = NativeCalendarRootModel(environment: environment, demoPresentation: fixture) + await model.start() + + XCTAssertNil(model.timeGridState.focusedEventId) + model.handleShortcut(.focusEvent) + XCTAssertNotNil(model.timeGridState.focusedEventId) + XCTAssertNotNil(model.gridFocusAccessibilityLabel) + } + func testShowPointerHintForEventCardKeepsFocusedGridEventLabel() async throws { let fixture = try DemoSeedFixture.load() let environment = try NativeCalendarEnvironment(inMemoryDatabase: true) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift index 92abfb62e4..1c036ea465 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift @@ -142,12 +142,24 @@ public struct EventFormView: View { } label: { HStack(spacing: 8) { Text("Save") - ShortcutHintChip(label: "Enter") + Spacer(minLength: 8) + Text(KeycapGlyphs.display(["Mod", "Enter"])) + .font(.rubik(11, weight: .medium)) + .foregroundStyle(theme.onAccentColor.opacity(0.92)) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(theme.onAccentColor.opacity(0.1)) + .overlay { + RoundedRectangle(cornerRadius: 4) + .stroke(theme.onAccentColor.opacity(0.35), lineWidth: 1) + } + .clipShape(RoundedRectangle(cornerRadius: 4)) } .font(.rubik(14, weight: .medium)) - .foregroundStyle(theme.backgroundColor) - .frame(maxWidth: .infinity, minHeight: 40) - .background(theme.textColor) + .foregroundStyle(theme.onAccentColor) + .padding(.horizontal, 20) + .frame(maxWidth: .infinity, minHeight: 36) + .background(theme.accentColor) .clipShape(RoundedRectangle(cornerRadius: 4)) } .buttonStyle(.plain) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/NativeWebFocusRing.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/NativeWebFocusRing.swift new file mode 100644 index 0000000000..dece0dbe07 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/NativeWebFocusRing.swift @@ -0,0 +1,32 @@ +import SwiftUI + +extension View { + /// Web `c-focus-ring`: 1px accent stroke with a 1px surface-panel offset. + func nativeWebFocusRing(_ isFocused: Bool, cornerRadius: CGFloat = 4) -> some View { + modifier(NativeWebFocusRing(isFocused: isFocused, cornerRadius: cornerRadius)) + } +} + +private struct NativeWebFocusRing: ViewModifier { + @Environment(\.nativeWebTheme) private var theme + let isFocused: Bool + let cornerRadius: CGFloat + + func body(content: Content) -> some View { + content + .padding(isFocused ? 1 : 0) + .background { + if isFocused { + RoundedRectangle(cornerRadius: cornerRadius + 1) + .fill(theme.surfacePanelColor) + } + } + .overlay { + if isFocused { + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(theme.accentColor, lineWidth: 1) + .padding(1) + } + } + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift index c74fa3d6dd..bd70b4bf95 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/ThemeTokenColor+SwiftUI.swift @@ -11,16 +11,25 @@ private protocol NativeThemePalette { var background: ThemeTokenColor { get } var surface: ThemeTokenColor { get } var surfacePanel: ThemeTokenColor { get } + var surfaceRaised: ThemeTokenColor { get } var surfaceOverlay: ThemeTokenColor { get } + var border: ThemeTokenColor { get } + var borderStrong: ThemeTokenColor { get } var text: ThemeTokenColor { get } var textMuted: ThemeTokenColor { get } - var border: ThemeTokenColor { get } + var textSubtle: ThemeTokenColor { get } var accent: ThemeTokenColor { get } + var accentHover: ThemeTokenColor { get } + var accentStrong: ThemeTokenColor { get } var accentSecondary: ThemeTokenColor { get } - var error: ThemeTokenColor { get } + var accentSecondaryHover: ThemeTokenColor { get } + var onAccent: ThemeTokenColor { get } var success: ThemeTokenColor { get } var warning: ThemeTokenColor { get } + var error: ThemeTokenColor { get } + var info: ThemeTokenColor { get } var overlayBackdrop: ThemeTokenColor { get } + var shadowDefault: ThemeTokenColor { get } } extension LightBeachPalette: NativeThemePalette {} @@ -60,16 +69,25 @@ public enum NativeWebTheme: String, Sendable, CaseIterable { public var backgroundColor: Color { palette.background.swiftUIColor } public var surfaceColor: Color { palette.surface.swiftUIColor } public var surfacePanelColor: Color { palette.surfacePanel.swiftUIColor } + public var surfaceRaisedColor: Color { palette.surfaceRaised.swiftUIColor } public var surfaceOverlayColor: Color { palette.surfaceOverlay.swiftUIColor } public var textColor: Color { palette.text.swiftUIColor } public var textMutedColor: Color { palette.textMuted.swiftUIColor } + public var textSubtleColor: Color { palette.textSubtle.swiftUIColor } public var borderColor: Color { palette.border.swiftUIColor } + public var borderStrongColor: Color { palette.borderStrong.swiftUIColor } public var accentColor: Color { palette.accent.swiftUIColor } + public var accentHoverColor: Color { palette.accentHover.swiftUIColor } + public var accentStrongColor: Color { palette.accentStrong.swiftUIColor } public var accentSecondaryColor: Color { palette.accentSecondary.swiftUIColor } + public var accentSecondaryHoverColor: Color { palette.accentSecondaryHover.swiftUIColor } + public var onAccentColor: Color { palette.onAccent.swiftUIColor } public var errorColor: Color { palette.error.swiftUIColor } public var successColor: Color { palette.success.swiftUIColor } public var warningColor: Color { palette.warning.swiftUIColor } + public var infoColor: Color { palette.info.swiftUIColor } public var overlayBackdropColor: Color { palette.overlayBackdrop.swiftUIColor } + public var shadowDefaultColor: Color { palette.shadowDefault.swiftUIColor } /// Web `OverlayPanel` backdrop (`bg-background/85`), used by modal panels. public var panelBackdropColor: Color { backgroundColor.opacity(0.85) } }