浏览代码

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 }
     }
 }