import AppKit import SwiftUI // MARK: - Theme Tokens extension AppTheme { static let hoverAnimation: Animation = .easeOut(duration: 0.15) static let hoverPressScale: CGFloat = 0.98 static let hoverLiftScale: CGFloat = 1.012 } // MARK: - Button Styles struct AppPlainButtonStyle: ButtonStyle { @State private var isHovered = false func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect( configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1) ) .brightness(configuration.isPressed ? -0.04 : (isHovered ? 0.05 : 0)) .animation(AppTheme.hoverAnimation, value: isHovered) .animation(AppTheme.hoverAnimation, value: configuration.isPressed) .onHover { isHovered = $0 } .hoverCursor() } } struct AppSecondaryButtonStyle: ButtonStyle { @State private var isHovered = false func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(AppTheme.textSecondary) .padding(.horizontal, 12) .padding(.vertical, 8) .background(secondaryBackground(isPressed: configuration.isPressed)) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke( isHovered || configuration.isPressed ? AppTheme.hoverBorder : AppTheme.cardBorder, lineWidth: 1 ) ) .scaleEffect(configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1)) .animation(AppTheme.hoverAnimation, value: isHovered) .animation(AppTheme.hoverAnimation, value: configuration.isPressed) .onHover { isHovered = $0 } .hoverCursor() } private func secondaryBackground(isPressed: Bool) -> Color { if isPressed { return AppTheme.cardBackground.opacity(0.55) } if isHovered { return AppTheme.cardBackground.opacity(0.85) } return AppTheme.cardBackground } } struct AppPrimaryButtonStyle: ButtonStyle { @State private var isHovered = false func makeBody(configuration: Configuration) -> some View { configuration.label .brightness(configuration.isPressed ? -0.08 : (isHovered ? 0.08 : 0)) .scaleEffect(configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1)) .animation(AppTheme.hoverAnimation, value: isHovered) .animation(AppTheme.hoverAnimation, value: configuration.isPressed) .onHover { isHovered = $0 } .hoverCursor() } } struct AppTextLinkButtonStyle: ButtonStyle { @State private var isHovered = false func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(isHovered ? AppTheme.textPrimary : AppTheme.textSecondary) .underline(isHovered || configuration.isPressed) .animation(AppTheme.hoverAnimation, value: isHovered) .animation(AppTheme.hoverAnimation, value: configuration.isPressed) .onHover { isHovered = $0 } .hoverCursor() } } // MARK: - View Modifiers private struct HoverCursorModifier: ViewModifier { @State private var isHovered = false func body(content: Content) -> some View { content .onHover { hovering in isHovered = hovering if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } } } } private struct HoverOverlayModifier: ViewModifier { let cornerRadius: CGFloat var isEnabled: Bool = true @State private var isHovered = false func body(content: Content) -> some View { content .overlay { if isEnabled && isHovered { RoundedRectangle(cornerRadius: cornerRadius) .fill(AppTheme.hoverFill) .allowsHitTesting(false) } } .animation(AppTheme.hoverAnimation, value: isHovered) .onHover { isHovered = $0 } } } private struct HoverCardModifier: ViewModifier { let cornerRadius: CGFloat var isSelected: Bool = false var isEnabled: Bool = true @State private var isHovered = false func body(content: Content) -> some View { content .overlay { if isEnabled && isHovered { RoundedRectangle(cornerRadius: cornerRadius) .fill(isSelected ? AppTheme.selectedRowFill : AppTheme.hoverFill) .allowsHitTesting(false) } } .overlay { if isEnabled && isHovered { RoundedRectangle(cornerRadius: cornerRadius) .stroke(AppTheme.hoverBorder, lineWidth: 1) .allowsHitTesting(false) } } .scaleEffect(isEnabled && isHovered ? AppTheme.hoverLiftScale : 1) .animation(AppTheme.hoverAnimation, value: isHovered) .onHover { isHovered = $0 } } } private struct SidebarRowHoverModifier: ViewModifier { let isSelected: Bool let showsCardBackground: Bool let selectedBorderColor: Color @State private var isHovered = false func body(content: Content) -> some View { content .background(sidebarRowBackground) .animation(AppTheme.hoverAnimation, value: isHovered) .onHover { isHovered = $0 } } @ViewBuilder private var sidebarRowBackground: some View { if showsCardBackground && isSelected { RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.quickAccessBackground) .overlay( RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .stroke(selectedBorderColor, lineWidth: 1) ) } else if isSelected { RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.selectedRowFill) } else if isHovered { RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall) .fill(AppTheme.hoverFill) } } } // MARK: - View Extensions extension View { func hoverCursor(_ enabled: Bool = true) -> some View { modifier(HoverCursorEnabledModifier(enabled: enabled)) } func hoverOverlay(cornerRadius: CGFloat, isEnabled: Bool = true) -> some View { modifier(HoverOverlayModifier(cornerRadius: cornerRadius, isEnabled: isEnabled)) } func hoverCard(cornerRadius: CGFloat, isSelected: Bool = false, isEnabled: Bool = true) -> some View { modifier(HoverCardModifier(cornerRadius: cornerRadius, isSelected: isSelected, isEnabled: isEnabled)) } func sidebarRowHover( isSelected: Bool, showsCardBackground: Bool = false, selectedBorderColor: Color = .clear ) -> some View { modifier( SidebarRowHoverModifier( isSelected: isSelected, showsCardBackground: showsCardBackground, selectedBorderColor: selectedBorderColor ) ) } } private struct HoverCursorEnabledModifier: ViewModifier { let enabled: Bool func body(content: Content) -> some View { if enabled { content.modifier(HoverCursorModifier()) } else { content } } }