소스 검색

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 개월 전
부모
커밋
7cd1958891
4개의 변경된 파일93개의 추가작업 그리고 15개의 파일을 삭제
  1. 22 0
      gramora/Utilities/AppTheme.swift
  2. 37 10
      gramora/Views/Components/GradientButton.swift
  3. 13 3
      gramora/Views/Components/PremiumCardView.swift
  4. 21 2
      gramora/Views/SidebarView.swift

+ 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 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 {
         LinearGradient(
             colors: [gradientStart, gradientEnd],
@@ -49,4 +63,12 @@ enum AppTheme {
             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 action: () -> Void
 
+    @State private var isHovered = false
+
     var body: some View {
         Button(action: action) {
             HStack(spacing: 10) {
@@ -21,11 +23,18 @@ struct GradientButton: View {
             .padding(.horizontal, 28)
             .padding(.vertical, 15)
             .frame(maxWidth: .infinity)
-            .background(AppTheme.primaryGradient)
+            .background(isHovered ? AppTheme.primaryGradientHover : AppTheme.primaryGradient)
             .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)
+        .onHover { isHovered = $0 }
     }
 }
 
@@ -34,6 +43,8 @@ struct ToolbarButton: View {
     let iconName: String
     let action: () -> Void
 
+    @State private var isHovered = false
+
     var body: some View {
         Button(action: action) {
             HStack(spacing: 6) {
@@ -43,18 +54,25 @@ struct ToolbarButton: View {
                 Text(title)
                     .font(.system(size: 12, weight: .medium))
             }
-            .foregroundStyle(AppTheme.textSecondary)
+            .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
             .padding(.horizontal, 12)
             .padding(.vertical, 7)
-            .background(Color.white)
+            .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
             .clipShape(RoundedRectangle(cornerRadius: 8))
             .overlay(
                 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)
+        .onHover { isHovered = $0 }
     }
 }
 
@@ -62,21 +80,30 @@ struct IconButton: View {
     let iconName: String
     let action: () -> Void
 
+    @State private var isHovered = false
+
     var body: some View {
         Button(action: action) {
             Image(systemName: iconName)
                 .font(.system(size: 14))
-                .foregroundStyle(AppTheme.textSecondary)
+                .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
                 .frame(width: 36, height: 36)
-                .background(Color.white)
+                .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                     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)
+        .onHover { isHovered = $0 }
     }
 }
 

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

@@ -1,6 +1,8 @@
 import SwiftUI
 
 struct PremiumCardView: View {
+    @State private var isUpgradeHovered = false
+
     var body: some View {
         VStack(alignment: .leading, spacing: 10) {
             HStack(spacing: 8) {
@@ -27,17 +29,25 @@ struct PremiumCardView: View {
                     Image(systemName: "arrow.up.right")
                         .font(.system(size: 10, weight: .semibold))
                 }
-                .foregroundStyle(AppTheme.textPrimary)
+                .foregroundStyle(isUpgradeHovered ? AppTheme.toolbarForegroundHover : AppTheme.textPrimary)
                 .frame(maxWidth: .infinity)
                 .padding(.vertical, 9)
-                .background(Color.white)
+                .background(isUpgradeHovered ? AppTheme.toolbarBackgroundHover : Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                     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)
+            .onHover { isUpgradeHovered = $0 }
         }
         .padding(14)
         .background(AppTheme.premiumBackground)

+ 21 - 2
gramora/Views/SidebarView.swift

@@ -61,6 +61,8 @@ private struct SidebarNavItem: View {
     let isSelected: Bool
     let action: () -> Void
 
+    @State private var isHovered = false
+
     var body: some View {
         Button(action: action) {
             HStack(spacing: 10) {
@@ -74,14 +76,31 @@ private struct SidebarNavItem: View {
 
                 Spacer()
             }
-            .foregroundStyle(isSelected ? AppTheme.teal : AppTheme.textSecondary)
+            .foregroundStyle(foregroundColor)
             .padding(.horizontal, 12)
             .padding(.vertical, 9)
             .background(
                 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)
+        .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
     }
 }