Procházet zdrojové kódy

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 před 1 měsícem
rodič
revize
ce69050191
1 změnil soubory, kde provedl 52 přidání a 23 odebrání
  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 }
     }
 }