From 1d688b620c3013a428893d2543f52aa2c446d4b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 15:11:52 +0000 Subject: [PATCH 1/5] fix(desktop-ui): align theme tokens, save bar, and focus-event shortcut Co-authored-by: Tyler Dane --- .../CompassData/NativeCalendarRootModel.swift | 11 +++++++ .../NativeCalendarRootModelFocusTests.swift | 12 +++++++ .../CompassUI/EventForm/EventFormView.swift | 20 +++++++++--- .../CompassUI/NativeWebFocusRing.swift | 32 +++++++++++++++++++ .../CompassUI/ThemeTokenColor+SwiftUI.swift | 22 +++++++++++-- 5 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/NativeWebFocusRing.swift 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) } } From 9128d1a02cc539e57baf01f14f580eda9476986a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 16:06:49 +0000 Subject: [PATCH 2/5] fix(desktop-ui): layout tokens, FormCard grouping, and toolbar keycaps Co-authored-by: Tyler Dane --- .../Theme/NativeWebLayoutTokens.swift | 14 +++ .../CompassUI/EventForm/EventFormCard.swift | 16 +++ .../EventForm/EventFormScheduleSection.swift | 1 - .../EventForm/EventFormSectionChrome.swift | 6 +- .../EventForm/EventFormToolbarView.swift | 32 ++++-- .../CompassUI/EventForm/EventFormView.swift | 97 ++++++++++--------- 6 files changed, 110 insertions(+), 56 deletions(-) create mode 100644 apps/calendar-macos/CompassKit/Sources/CompassKit/Theme/NativeWebLayoutTokens.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCard.swift diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Theme/NativeWebLayoutTokens.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Theme/NativeWebLayoutTokens.swift new file mode 100644 index 0000000000..0a2a932a7d --- /dev/null +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Theme/NativeWebLayoutTokens.swift @@ -0,0 +1,14 @@ +import CoreGraphics + +/// Layout tokens mirrored from web `index.css` `@theme` (radius + type scale). +public enum NativeWebLayoutTokens { + public static let radiusDefault: CGFloat = 4 + public static let radiusPanel: CGFloat = 12 + + public static let fontSizeXS: CGFloat = 9 + public static let fontSizeS: CGFloat = 11 + public static let fontSizeM: CGFloat = 13 + public static let fontSizeBody: CGFloat = 14 + public static let fontSizeL: CGFloat = 16 + public static let fontSizeXL: CGFloat = 18 +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCard.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCard.swift new file mode 100644 index 0000000000..e0e0cd3b1b --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCard.swift @@ -0,0 +1,16 @@ +import CompassKit +import SwiftUI + +/// Web `FormCard`: grouped fields on `surface-overlay` with default radius. +struct EventFormCard: View { + @Environment(\.nativeWebTheme) private var theme + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(theme.surfaceOverlayColor) + .clipShape(RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault)) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift index 4d30a78900..ae9a209c16 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift @@ -87,7 +87,6 @@ struct EventFormScheduleSection: View { .accessibilityIdentifier("compass-event-form-schedule-locked-note") } } - .eventFormSectionChrome(focused: focusField == .start || focusField == .end) } private func updateStart(_ date: Date) { diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift index f8065eeaa1..20c81c7640 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift @@ -6,14 +6,14 @@ struct EventFormSectionChrome: ViewModifier { func body(content: Content) -> some View { content - .font(.rubik(13)) + .font(.rubik(NativeWebLayoutTokens.fontSizeM)) .controlSize(.small) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(theme.surfaceOverlayColor) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .clipShape(RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault)) .overlay( - RoundedRectangle(cornerRadius: 4) + RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault) .stroke(focused ? theme.accentColor : .clear, lineWidth: 1) ) } diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormToolbarView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormToolbarView.swift index 70502cf825..d2524adefb 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormToolbarView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormToolbarView.swift @@ -1,4 +1,5 @@ import CompassData +import CompassKit import SwiftUI struct EventFormToolbarView: View { @@ -8,15 +9,19 @@ struct EventFormToolbarView: View { var body: some View { HStack(spacing: 8) { - toolbarButton(label: "Close", systemImage: "xmark") { model.requestCloseEventForm() } + toolbarButton(label: "Close", systemImage: "xmark", keycaps: ["Esc"]) { + model.requestCloseEventForm() + } if draft.kind == .edit { - toolbarButton(label: "Duplicate", systemImage: "plus.square.on.square") { + toolbarButton(label: "Duplicate", systemImage: "plus.square.on.square", keycaps: ["Mod", "D"]) { Task { await model.duplicateFocusedOrFormEvent() } } if model.hasBookedMeetingActions { BookingEventActionsView(model: model) } else if !model.isEventFormReadOnly(for: draft) { - toolbarButton(label: "Delete", systemImage: "trash") { Task { await model.deleteFormEvent() } } + toolbarButton(label: "Delete", systemImage: "trash", keycaps: ["Delete"]) { + Task { await model.deleteFormEvent() } + } } } Spacer() @@ -24,12 +29,23 @@ struct EventFormToolbarView: View { .accessibilityIdentifier("compass-event-form-actions") } - private func toolbarButton(label: String, systemImage: String, action: @escaping () -> Void) -> some View { + private func toolbarButton( + label: String, + systemImage: String, + keycaps: [String], + action: @escaping () -> Void + ) -> some View { Button(action: action) { - Image(systemName: systemImage) - .font(.system(size: 13, weight: .semibold)) - .foregroundStyle(theme.textMutedColor) - .frame(width: 28, height: 28) + VStack(spacing: 2) { + Image(systemName: systemImage) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(theme.textMutedColor) + .frame(width: 28, height: 22) + Text(KeycapGlyphs.display(keycaps)) + .font(.rubik(NativeWebLayoutTokens.fontSizeXS, weight: .medium)) + .foregroundStyle(theme.textSubtleColor) + } + .frame(minWidth: 36) } .buttonStyle(.plain) .accessibilityLabel(label) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift index 1c036ea465..15a36a37b5 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormView.swift @@ -49,39 +49,60 @@ public struct EventFormView: View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 12) { - EventFormScheduleSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) + EventFormCard { + VStack(alignment: .leading, spacing: 10) { + EventFormScheduleSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + EventFormRecurrenceSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + .disabled(model.isScheduleLocked(for: draft)) + } + } .id(EventFormField.start) - EventFormRecurrenceSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .disabled(model.isScheduleLocked(for: draft)) - .id(EventFormField.recurrence) - EventFormCalendarSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .disabled(isReadOnly) + EventFormCard { + VStack(alignment: .leading, spacing: 10) { + EventFormCalendarSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + .disabled(isReadOnly) + EventFormColorSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + .disabled(isReadOnly) + } + } .id(EventFormField.calendar) - EventFormColorSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .disabled(isReadOnly) - .id(EventFormField.color) - EventFormLocationField( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .disabled(isReadOnly) + EventFormCard { + VStack(alignment: .leading, spacing: 10) { + EventFormLocationField( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + .disabled(isReadOnly) + EventFormConferenceSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + EventFormAttendeesSection( + model: model, + draft: draft, + focusField: model.eventFormFocusedField + ) + .id(EventFormField.attendees) + } + } .id(EventFormField.location) DescriptionEditorView( html: Binding( @@ -107,18 +128,6 @@ public struct EventFormView: View { .pageJumpChipAnchor(id: "form-description") .id(EventFormField.description) - EventFormConferenceSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .id(EventFormField.conference) - EventFormAttendeesSection( - model: model, - draft: draft, - focusField: model.eventFormFocusedField - ) - .id(EventFormField.attendees) EventFormRsvpSection( model: model, draft: draft, @@ -160,7 +169,7 @@ public struct EventFormView: View { .padding(.horizontal, 20) .frame(maxWidth: .infinity, minHeight: 36) .background(theme.accentColor) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .clipShape(RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault)) } .buttonStyle(.plain) .disabled(model.hasPendingEventMutations) From 7bec2924a1899ca7a4e83dace2e399019af25d22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 16:07:01 +0000 Subject: [PATCH 3/5] fix(desktop-ui): event form MM.DD.YY dates in FormCard layout Co-authored-by: Tyler Dane --- .../EventForm/EventFormDateFormat.swift | 21 ++++ .../EventFormDateFormatTests.swift | 14 +++ .../EventForm/EventFormCompactDateField.swift | 75 +++++++++++++++ .../EventForm/EventFormScheduleSection.swift | 96 ++++++++++++------- 4 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift create mode 100644 apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift new file mode 100644 index 0000000000..f335873847 --- /dev/null +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Closed-label date format for the sidebar event form. Matches web `formatEventFormDateLabel`. +public enum EventFormDateFormat { + private static let labelFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = EffectiveTimeZone.timeZone + formatter.dateFormat = "MM.dd.yy" + return formatter + }() + + public static func formatLabel(for date: Date) -> String { + labelFormatter.string(from: date) + } + + /// Parses typed form dates using the same rules as go-to-date / web event form. + public static func parseTypedLabel(_ text: String, now: Date) -> Date? { + UserDateParsing.parseUserDate(text, now: now) + } +} diff --git a/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift b/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift new file mode 100644 index 0000000000..b115f4fceb --- /dev/null +++ b/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift @@ -0,0 +1,14 @@ +import CompassKit +import XCTest + +final class EventFormDateFormatTests: XCTestCase { + override func setUp() { + super.setUp() + EffectiveTimeZone.identifier = "UTC" + } + + func testFormatLabelUsesMonthDayYearDots() throws { + let date = try XCTUnwrap(CompassDateParsing.parseInEffectiveTimeZone("2026-10-17T12:00:00")) + XCTAssertEqual(EventFormDateFormat.formatLabel(for: date), "10.17.26") + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift new file mode 100644 index 0000000000..a9564b9a6d --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift @@ -0,0 +1,75 @@ +import CompassKit +import SwiftUI + +struct EventFormCompactDateField: View { + @Environment(\.nativeWebTheme) private var theme + @Binding var date: Date + let accessibilityIdentifier: String + @State private var text: String + @State private var showsCalendar = false + @FocusState private var isFocused: Bool + + init(date: Binding, accessibilityIdentifier: String) { + _date = date + self.accessibilityIdentifier = accessibilityIdentifier + _text = State(initialValue: EventFormDateFormat.formatLabel(for: date.wrappedValue)) + } + + var body: some View { + TextField("Date", text: $text) + .textFieldStyle(.plain) + .font(.rubik(13)) + .foregroundStyle(theme.textColor) + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(theme.surfacePanelColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .overlay { + RoundedRectangle(cornerRadius: 4) + .stroke(isFocused ? theme.accentColor : theme.borderColor, lineWidth: 1) + } + .focused($isFocused) + .accessibilityIdentifier(accessibilityIdentifier) + .onSubmit { commitTypedDate() } + .onChange(of: isFocused) { _, focused in + if !focused { commitTypedDate() } + } + .onChange(of: date) { _, newValue in + guard !isFocused else { return } + text = EventFormDateFormat.formatLabel(for: newValue) + } + .onTapGesture(count: 2) { + showsCalendar = true + } + .popover(isPresented: $showsCalendar, arrowEdge: .bottom) { + DatePicker( + "Pick date", + selection: Binding( + get: { date }, + set: { newDate in + date = newDate + text = EventFormDateFormat.formatLabel(for: newDate) + } + ), + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + .labelsHidden() + .padding(12) + } + } + + private func commitTypedDate() { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + text = EventFormDateFormat.formatLabel(for: date) + return + } + if let parsed = EventFormDateFormat.parseTypedLabel(trimmed, now: date) { + date = parsed + text = EventFormDateFormat.formatLabel(for: parsed) + return + } + text = EventFormDateFormat.formatLabel(for: date) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift index ae9a209c16..731ce59285 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift @@ -23,57 +23,75 @@ struct EventFormScheduleSection: View { .accessibilityIdentifier("compass-event-form-allday") if draft.schedule.kind == .timed { - DatePicker( - "Start", - selection: Binding( - get: { draft.schedule.start }, - set: { updateStart($0) } - ), - displayedComponents: [.date, .hourAndMinute] - ) - .labelsHidden() - .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-start") - .pageJumpChipAnchor(id: "form-start") + HStack(spacing: 8) { + EventFormCompactDateField( + date: Binding( + get: { draft.schedule.start }, + set: { updateStart(mergeCalendarDay(into: draft.schedule.start, day: $0)) } + ), + accessibilityIdentifier: "compass-event-form-start-date" + ) + .disabled(scheduleLocked) + .pageJumpChipAnchor(id: "form-start") - DatePicker( - "End", - selection: Binding( - get: { draft.schedule.end }, - set: { updateEnd($0) } - ), - displayedComponents: [.date, .hourAndMinute] - ) - .labelsHidden() - .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-end") - .pageJumpChipAnchor(id: "form-end") + DatePicker( + "Start time", + selection: Binding( + get: { draft.schedule.start }, + set: { updateStart($0) } + ), + displayedComponents: [.hourAndMinute] + ) + .labelsHidden() + .disabled(scheduleLocked) + .accessibilityIdentifier("compass-event-form-start") + } + + HStack(spacing: 8) { + EventFormCompactDateField( + date: Binding( + get: { draft.schedule.end }, + set: { updateEnd(mergeCalendarDay(into: draft.schedule.end, day: $0)) } + ), + accessibilityIdentifier: "compass-event-form-end-date" + ) + .disabled(scheduleLocked) + .pageJumpChipAnchor(id: "form-end") + + DatePicker( + "End time", + selection: Binding( + get: { draft.schedule.end }, + set: { updateEnd($0) } + ), + displayedComponents: [.hourAndMinute] + ) + .labelsHidden() + .disabled(scheduleLocked) + .accessibilityIdentifier("compass-event-form-end") + } } else { - DatePicker( - "Start day", - selection: Binding( + EventFormCompactDateField( + date: Binding( get: { draft.schedule.start }, set: { model.updateAllDayDraftStart($0) } ), - displayedComponents: [.date] + accessibilityIdentifier: "compass-event-form-start" ) .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-start") .pageJumpChipAnchor(id: "form-start") - DatePicker( - "End day", - selection: Binding( + EventFormCompactDateField( + date: Binding( get: { EffectiveTimeZone.calendar.date(byAdding: .day, value: -1, to: draft.schedule.end) ?? draft.schedule.start }, set: { model.updateAllDayDraftEnd($0) } ), - displayedComponents: [.date] + accessibilityIdentifier: "compass-event-form-end" ) .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-end") .pageJumpChipAnchor(id: "form-end") } @@ -89,6 +107,16 @@ struct EventFormScheduleSection: View { } } + private func mergeCalendarDay(into scheduleDate: Date, day: Date) -> Date { + var calendar = EffectiveTimeZone.calendar + let time = calendar.dateComponents([.hour, .minute, .second], from: scheduleDate) + var dayComponents = calendar.dateComponents([.year, .month, .day], from: day) + dayComponents.hour = time.hour + dayComponents.minute = time.minute + dayComponents.second = time.second ?? 0 + return calendar.date(from: dayComponents) ?? day + } + private func updateStart(_ date: Date) { var schedule = draft.schedule schedule.start = date From abce0bc8b5c23ed55180c931f5fbf724adb55e96 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 18:15:36 +0000 Subject: [PATCH 4/5] fix(desktop-ui): import CompassKit in EventFormSectionChrome Co-authored-by: Tyler Dane --- .../Sources/CompassUI/EventForm/EventFormSectionChrome.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift index 20c81c7640..141d6fa81c 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormSectionChrome.swift @@ -1,3 +1,4 @@ +import CompassKit import SwiftUI struct EventFormSectionChrome: ViewModifier { From ef2869a4f4c52896de5c79edae2d317f4a932e79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 20:27:41 +0000 Subject: [PATCH 5/5] test(desktop-ui): scroll event form color grid in layout test Co-authored-by: Tyler Dane --- .../CompassUITests/EventFormLayoutTests.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/calendar-macos/CompassUITests/EventFormLayoutTests.swift b/apps/calendar-macos/CompassUITests/EventFormLayoutTests.swift index a7dc04d3ed..d8d6e05d57 100644 --- a/apps/calendar-macos/CompassUITests/EventFormLayoutTests.swift +++ b/apps/calendar-macos/CompassUITests/EventFormLayoutTests.swift @@ -15,14 +15,19 @@ final class EventFormLayoutTests: XCTestCase { let form = window.descendants(matching: .any)["compass-event-form"] XCTAssertTrue(form.waitForExistence(timeout: 8)) window.typeKey("6", modifierFlags: [.command]) + let colorGrid = form.descendants(matching: .any)["compass-event-form-color"] + XCTAssertTrue(colorGrid.waitForExistence(timeout: 8)) + for _ in 0 ..< 3 where !colorGrid.buttons["red"].exists { + form.swipeUp() + } for name in ["Default", "blue", "coral", "gold", "green", "indigo", "lavender", "mint", "orange", "plum", "red", "slate"] { - let swatch = window.buttons[name] - XCTAssertTrue(swatch.exists, "Missing color \(name)") + let swatch = colorGrid.buttons[name] + XCTAssertTrue(swatch.waitForExistence(timeout: 2), "Missing color \(name)") XCTAssertGreaterThanOrEqual(swatch.frame.minX, form.frame.minX) XCTAssertLessThanOrEqual(swatch.frame.maxX, form.frame.maxX) } - let slate = window.buttons["slate"] + let slate = colorGrid.buttons["slate"] slate.click() XCTAssertEqual(slate.value as? String, "Selected") }