فهرست منبع

Add home-style hover interactions across paywall components.

Apply consistent hover feedback to the CTA button, plan cards, and footer links so the paywall matches the interactive feel of the home page while preserving selected plan emphasis.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 ماه پیش
والد
کامیت
ce69050191
1فایلهای تغییر یافته به همراه52 افزوده شده و 23 حذف شده
  1. 52 23
      gramora/Views/PaywallView.swift

+ 52 - 23
gramora/Views/PaywallView.swift

@@ -3,6 +3,7 @@ import SwiftUI
 struct PaywallView: View {
     let onClose: () -> Void
     @StateObject private var viewModel = PaywallViewModel()
+    @State private var isCTAHovered = false
 
     var body: some View {
         GeometryReader { proxy in
@@ -93,7 +94,7 @@ struct PaywallView: View {
                     Spacer()
                 }
                 .padding(.vertical, 14)
-                .background(Color(red: 0.03, green: 0.62, blue: 0.48))
+                .background(isCTAHovered ? Color(red: 0.02, green: 0.56, blue: 0.43) : Color(red: 0.03, green: 0.62, blue: 0.48))
                 .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
                 .overlay(alignment: .trailing) {
                     Image(systemName: "chevron.right.2")
@@ -102,7 +103,15 @@ struct PaywallView: View {
                         .padding(.trailing, 24)
                 }
             }
+            .scaleEffect(isCTAHovered ? 1.01 : 1.0)
+            .shadow(
+                color: isCTAHovered ? AppTheme.primaryShadow.opacity(0.32) : .clear,
+                radius: isCTAHovered ? 8 : 0,
+                y: isCTAHovered ? 3 : 0
+            )
+            .animation(.easeOut(duration: 0.15), value: isCTAHovered)
             .buttonStyle(.plain)
+            .onHover { isCTAHovered = $0 }
 
             Text("No commitment, cancel anytime.")
                 .font(.system(size: 13, weight: .semibold))
@@ -119,10 +128,10 @@ struct PaywallView: View {
                 .lineSpacing(2)
 
             HStack(spacing: 0) {
-                footerLink("Continue with free plan", action: onClose)
-                footerLink("Privacy Policy")
-                footerLink("Support")
-                footerLink("Terms of Services")
+                PaywallFooterLinkView(title: "Continue with free plan", action: onClose)
+                PaywallFooterLinkView(title: "Privacy Policy")
+                PaywallFooterLinkView(title: "Support")
+                PaywallFooterLinkView(title: "Terms of Services")
             }
             .frame(height: 44)
             .frame(maxWidth: .infinity)
@@ -131,19 +140,6 @@ struct PaywallView: View {
         }
     }
 
-    private func footerLink(_ title: String, action: @escaping () -> Void = {}) -> some View {
-        Button(action: action) {
-            Text(title)
-                .font(.system(size: 14, weight: .semibold))
-                .foregroundStyle(Color(red: 0.59, green: 0.62, blue: 0.68))
-                .lineLimit(1)
-                .minimumScaleFactor(0.75)
-                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
-                .contentShape(Rectangle())
-        }
-        .frame(maxWidth: .infinity, maxHeight: .infinity)
-        .buttonStyle(.plain)
-    }
 }
 
 private struct PaywallFeatureBox: View {
@@ -195,6 +191,7 @@ private struct PaywallPlanCardView: View {
     let plan: PaywallPlan
     let isSelected: Bool
     let onSelect: () -> Void
+    @State private var isHovered = false
 
     var body: some View {
         Button(action: onSelect) {
@@ -267,21 +264,53 @@ private struct PaywallPlanCardView: View {
                 )
             }
             .frame(maxWidth: .infinity, minHeight: 170, alignment: .topLeading)
-            .background(isSelected ? AppTheme.tealLight.opacity(0.28) : AppTheme.cardBackground)
+            .background((isSelected || isHovered) ? AppTheme.tealLight.opacity(isSelected ? 0.28 : 0.16) : AppTheme.cardBackground)
             .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
             .overlay(
                 RoundedRectangle(cornerRadius: 16, style: .continuous)
                     .stroke(
-                        isSelected ? AppTheme.toolbarBorderHover : AppTheme.border,
+                        (isSelected || isHovered) ? AppTheme.toolbarBorderHover : AppTheme.border,
                         lineWidth: isSelected ? 2 : 1
                     )
             )
             .shadow(
-                color: isSelected ? AppTheme.primaryShadow.opacity(0.35) : .clear,
-                radius: isSelected ? 6 : 0,
-                y: isSelected ? 2 : 0
+                color: (isSelected || isHovered) ? AppTheme.primaryShadow.opacity(isSelected ? 0.35 : 0.22) : .clear,
+                radius: (isSelected || isHovered) ? (isSelected ? 6 : 4) : 0,
+                y: (isSelected || isHovered) ? 2 : 0
             )
+            .scaleEffect(isHovered && !isSelected ? 1.008 : 1.0)
+            .animation(.easeOut(duration: 0.15), value: isHovered)
         }
         .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
+    }
+}
+
+private struct PaywallFooterLinkView: View {
+    let title: String
+    let action: () -> Void
+    @State private var isHovered = false
+
+    init(title: String, action: @escaping () -> Void = {}) {
+        self.title = title
+        self.action = action
+    }
+
+    var body: some View {
+        Button(action: action) {
+            Text(title)
+                .font(.system(size: 14, weight: .semibold))
+                .foregroundStyle(isHovered ? Color(red: 0.47, green: 0.51, blue: 0.57) : Color(red: 0.59, green: 0.62, blue: 0.68))
+                .lineLimit(1)
+                .minimumScaleFactor(0.75)
+                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+                .contentShape(Rectangle())
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .background(isHovered ? AppTheme.cardBackground : Color.clear)
+        .scaleEffect(isHovered ? 1.01 : 1.0)
+        .animation(.easeOut(duration: 0.15), value: isHovered)
+        .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
     }
 }