| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import AppKit
- import SwiftUI
- // MARK: - Theme Tokens
- extension AppTheme {
- static let hoverAnimation: Animation = .easeOut(duration: 0.15)
- static let hoverFillOpacity: Double = 0.06
- static let hoverBorderOpacity: Double = 0.14
- static let hoverPressScale: CGFloat = 0.98
- static let hoverLiftScale: CGFloat = 1.012
- static var hoverFill: Color { Color.white.opacity(hoverFillOpacity) }
- static var hoverBorder: Color { Color.white.opacity(hoverBorderOpacity) }
- }
- // 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 ? Color.white.opacity(0.04) : 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 {
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .fill(AppTheme.quickAccessBackground)
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .stroke(
- isSelected
- ? selectedBorderColor
- : (isHovered ? AppTheme.hoverBorder : AppTheme.cardBorder),
- lineWidth: 1
- )
- )
- .overlay {
- if isHovered && !isSelected {
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .fill(AppTheme.hoverFill)
- }
- }
- } else if isSelected {
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .fill(Color.white.opacity(0.05))
- } 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
- }
- }
- }
|