Просмотр исходного кода

Add consistent hover feedback across all buttons to improve macOS affordance.

Extend the Clear button interaction pattern to primary, toolbar, icon, sidebar, and premium buttons with shared theme tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 месяц назад
Родитель
Сommit
7cd1958891

+ 22 - 0
gramora/Utilities/AppTheme.swift

@@ -42,6 +42,20 @@ enum AppTheme {
     static let clearBorder = Color(red: 0.94, green: 0.80, blue: 0.80)
     static let clearBorder = Color(red: 0.94, green: 0.80, blue: 0.80)
     static let clearShadow = Color(red: 0.82, green: 0.32, blue: 0.36).opacity(0.18)
     static let clearShadow = Color(red: 0.82, green: 0.32, blue: 0.36).opacity(0.18)
 
 
+    static let toolbarBackgroundHover = Color(red: 0.96, green: 0.99, blue: 0.98)
+    static let toolbarForegroundHover = teal
+    static let toolbarBorderHover = teal.opacity(0.35)
+    static let toolbarShadow = Color.black.opacity(0.03)
+    static let toolbarShadowHover = Color.black.opacity(0.08)
+
+    static let gradientStartHover = Color(red: 0.0, green: 0.68, blue: 0.52)
+    static let gradientEndHover = Color(red: 0.25, green: 0.84, blue: 0.66)
+    static let primaryShadow = teal.opacity(0.30)
+    static let primaryShadowHover = teal.opacity(0.42)
+
+    static let navBackgroundHover = Color(red: 0.88, green: 0.96, blue: 0.93)
+    static let navForegroundHover = teal
+
     static var primaryGradient: LinearGradient {
     static var primaryGradient: LinearGradient {
         LinearGradient(
         LinearGradient(
             colors: [gradientStart, gradientEnd],
             colors: [gradientStart, gradientEnd],
@@ -49,4 +63,12 @@ enum AppTheme {
             endPoint: .trailing
             endPoint: .trailing
         )
         )
     }
     }
+
+    static var primaryGradientHover: LinearGradient {
+        LinearGradient(
+            colors: [gradientStartHover, gradientEndHover],
+            startPoint: .leading,
+            endPoint: .trailing
+        )
+    }
 }
 }

+ 37 - 10
gramora/Views/Components/GradientButton.swift

@@ -4,6 +4,8 @@ struct GradientButton: View {
     let title: String
     let title: String
     let action: () -> Void
     let action: () -> Void
 
 
+    @State private var isHovered = false
+
     var body: some View {
     var body: some View {
         Button(action: action) {
         Button(action: action) {
             HStack(spacing: 10) {
             HStack(spacing: 10) {
@@ -21,11 +23,18 @@ struct GradientButton: View {
             .padding(.horizontal, 28)
             .padding(.horizontal, 28)
             .padding(.vertical, 15)
             .padding(.vertical, 15)
             .frame(maxWidth: .infinity)
             .frame(maxWidth: .infinity)
-            .background(AppTheme.primaryGradient)
+            .background(isHovered ? AppTheme.primaryGradientHover : AppTheme.primaryGradient)
             .clipShape(RoundedRectangle(cornerRadius: AppTheme.buttonCornerRadius))
             .clipShape(RoundedRectangle(cornerRadius: AppTheme.buttonCornerRadius))
-            .shadow(color: AppTheme.teal.opacity(0.30), radius: 10, y: 4)
+            .shadow(
+                color: isHovered ? AppTheme.primaryShadowHover : AppTheme.primaryShadow,
+                radius: isHovered ? 14 : 10,
+                y: isHovered ? 6 : 4
+            )
+            .scaleEffect(isHovered ? 1.02 : 1.0)
+            .animation(.easeOut(duration: 0.15), value: isHovered)
         }
         }
         .buttonStyle(.plain)
         .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
     }
     }
 }
 }
 
 
@@ -34,6 +43,8 @@ struct ToolbarButton: View {
     let iconName: String
     let iconName: String
     let action: () -> Void
     let action: () -> Void
 
 
+    @State private var isHovered = false
+
     var body: some View {
     var body: some View {
         Button(action: action) {
         Button(action: action) {
             HStack(spacing: 6) {
             HStack(spacing: 6) {
@@ -43,18 +54,25 @@ struct ToolbarButton: View {
                 Text(title)
                 Text(title)
                     .font(.system(size: 12, weight: .medium))
                     .font(.system(size: 12, weight: .medium))
             }
             }
-            .foregroundStyle(AppTheme.textSecondary)
+            .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
             .padding(.horizontal, 12)
             .padding(.horizontal, 12)
             .padding(.vertical, 7)
             .padding(.vertical, 7)
-            .background(Color.white)
+            .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
             .clipShape(RoundedRectangle(cornerRadius: 8))
             .clipShape(RoundedRectangle(cornerRadius: 8))
             .overlay(
             .overlay(
                 RoundedRectangle(cornerRadius: 8)
                 RoundedRectangle(cornerRadius: 8)
-                    .stroke(AppTheme.border, lineWidth: 1)
+                    .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
             )
             )
-            .shadow(color: Color.black.opacity(0.03), radius: 2, y: 1)
+            .shadow(
+                color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
+                radius: isHovered ? 4 : 2,
+                y: isHovered ? 2 : 1
+            )
+            .scaleEffect(isHovered ? 1.02 : 1.0)
+            .animation(.easeOut(duration: 0.15), value: isHovered)
         }
         }
         .buttonStyle(.plain)
         .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
     }
     }
 }
 }
 
 
@@ -62,21 +80,30 @@ struct IconButton: View {
     let iconName: String
     let iconName: String
     let action: () -> Void
     let action: () -> Void
 
 
+    @State private var isHovered = false
+
     var body: some View {
     var body: some View {
         Button(action: action) {
         Button(action: action) {
             Image(systemName: iconName)
             Image(systemName: iconName)
                 .font(.system(size: 14))
                 .font(.system(size: 14))
-                .foregroundStyle(AppTheme.textSecondary)
+                .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
                 .frame(width: 36, height: 36)
                 .frame(width: 36, height: 36)
-                .background(Color.white)
+                .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                 .overlay(
                     RoundedRectangle(cornerRadius: 8)
                     RoundedRectangle(cornerRadius: 8)
-                        .stroke(AppTheme.border, lineWidth: 1)
+                        .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
                 )
                 )
-                .shadow(color: Color.black.opacity(0.04), radius: 3, y: 1)
+                .shadow(
+                    color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
+                    radius: isHovered ? 5 : 3,
+                    y: isHovered ? 2 : 1
+                )
+                .scaleEffect(isHovered ? 1.02 : 1.0)
+                .animation(.easeOut(duration: 0.15), value: isHovered)
         }
         }
         .buttonStyle(.plain)
         .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
     }
     }
 }
 }
 
 

+ 13 - 3
gramora/Views/Components/PremiumCardView.swift

@@ -1,6 +1,8 @@
 import SwiftUI
 import SwiftUI
 
 
 struct PremiumCardView: View {
 struct PremiumCardView: View {
+    @State private var isUpgradeHovered = false
+
     var body: some View {
     var body: some View {
         VStack(alignment: .leading, spacing: 10) {
         VStack(alignment: .leading, spacing: 10) {
             HStack(spacing: 8) {
             HStack(spacing: 8) {
@@ -27,17 +29,25 @@ struct PremiumCardView: View {
                     Image(systemName: "arrow.up.right")
                     Image(systemName: "arrow.up.right")
                         .font(.system(size: 10, weight: .semibold))
                         .font(.system(size: 10, weight: .semibold))
                 }
                 }
-                .foregroundStyle(AppTheme.textPrimary)
+                .foregroundStyle(isUpgradeHovered ? AppTheme.toolbarForegroundHover : AppTheme.textPrimary)
                 .frame(maxWidth: .infinity)
                 .frame(maxWidth: .infinity)
                 .padding(.vertical, 9)
                 .padding(.vertical, 9)
-                .background(Color.white)
+                .background(isUpgradeHovered ? AppTheme.toolbarBackgroundHover : Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                 .overlay(
                     RoundedRectangle(cornerRadius: 8)
                     RoundedRectangle(cornerRadius: 8)
-                        .stroke(AppTheme.border, lineWidth: 1)
+                        .stroke(isUpgradeHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
+                )
+                .shadow(
+                    color: isUpgradeHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
+                    radius: isUpgradeHovered ? 4 : 2,
+                    y: isUpgradeHovered ? 2 : 1
                 )
                 )
+                .scaleEffect(isUpgradeHovered ? 1.02 : 1.0)
+                .animation(.easeOut(duration: 0.15), value: isUpgradeHovered)
             }
             }
             .buttonStyle(.plain)
             .buttonStyle(.plain)
+            .onHover { isUpgradeHovered = $0 }
         }
         }
         .padding(14)
         .padding(14)
         .background(AppTheme.premiumBackground)
         .background(AppTheme.premiumBackground)

+ 21 - 2
gramora/Views/SidebarView.swift

@@ -61,6 +61,8 @@ private struct SidebarNavItem: View {
     let isSelected: Bool
     let isSelected: Bool
     let action: () -> Void
     let action: () -> Void
 
 
+    @State private var isHovered = false
+
     var body: some View {
     var body: some View {
         Button(action: action) {
         Button(action: action) {
             HStack(spacing: 10) {
             HStack(spacing: 10) {
@@ -74,14 +76,31 @@ private struct SidebarNavItem: View {
 
 
                 Spacer()
                 Spacer()
             }
             }
-            .foregroundStyle(isSelected ? AppTheme.teal : AppTheme.textSecondary)
+            .foregroundStyle(foregroundColor)
             .padding(.horizontal, 12)
             .padding(.horizontal, 12)
             .padding(.vertical, 9)
             .padding(.vertical, 9)
             .background(
             .background(
                 RoundedRectangle(cornerRadius: AppTheme.navItemCornerRadius)
                 RoundedRectangle(cornerRadius: AppTheme.navItemCornerRadius)
-                    .fill(isSelected ? AppTheme.tealLight : Color.clear)
+                    .fill(backgroundColor)
             )
             )
+            .scaleEffect(isHovered ? 1.02 : 1.0)
+            .animation(.easeOut(duration: 0.15), value: isHovered)
         }
         }
         .buttonStyle(.plain)
         .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
+    }
+
+    private var foregroundColor: Color {
+        if isSelected {
+            return AppTheme.teal
+        }
+        return isHovered ? AppTheme.navForegroundHover : AppTheme.textSecondary
+    }
+
+    private var backgroundColor: Color {
+        if isSelected {
+            return isHovered ? AppTheme.navBackgroundHover : AppTheme.tealLight
+        }
+        return isHovered ? AppTheme.tealLight : Color.clear
     }
     }
 }
 }