Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ public final class NativeCalendarRootModel {
isSidebarOpen.toggle()
case .focusSidebar:
focusSidebar()
case .focusEvent:
focusFirstCalendarEvent()
case .focusNotice:
focusNextNotice()
case .otherSubscribe:
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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) }
}
Loading