Преглед изворни кода

Add hover effects to premium paywall interactive elements.

Plan cards, the CTA button, and footer links now respond to hover with the same animated feedback used elsewhere in the app.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 пре 1 месец
родитељ
комит
1b5da34bb3
1 измењених фајлова са 83 додато и 10 уклоњено
  1. 83 10
      smart_printer/PaywallView.swift

+ 83 - 10
smart_printer/PaywallView.swift

@@ -188,6 +188,8 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     private let subtitleLabel = NSTextField(labelWithString: "")
     private let priceLabel = NSTextField(labelWithString: "")
     private var badgeView: PaywallBadgeView?
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
 
     var isChosen: Bool = false {
         didSet { updateAppearance() }
@@ -262,7 +264,12 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
             ])
         }
 
+        applyCardShadow()
         updateAppearance()
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.setHovered(hovering)
+        }
     }
 
     @available(*, unavailable)
@@ -271,6 +278,15 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     func refreshAppearance() {
         updateAppearance()
         subtitleLabel.refreshThemeLabelColor()
+        if isHovered {
+            applyHoverLift(true)
+        }
+    }
+
+    private func setHovered(_ hovering: Bool) {
+        isHovered = hovering
+        applyHoverLift(hovering)
+        updateAppearance()
     }
 
     private func updateAppearance() {
@@ -279,8 +295,19 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
         priceLabel.textColor = titleColor
 
         layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
-        layer?.borderWidth = isChosen ? 2 : 1
-        layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
+
+        if isChosen {
+            layer?.borderWidth = 2
+            layer?.borderColor = AppTheme.green.cgColor
+        } else if isHovered {
+            layer?.borderWidth = 1.5
+            let hoverBorder = AppTheme.paywallBorder.blended(withFraction: 0.35, of: AppTheme.paywallAccent)
+                ?? AppTheme.paywallBorder
+            layer?.borderColor = hoverBorder.cgColor
+        } else {
+            layer?.borderWidth = 1
+            layer?.borderColor = AppTheme.paywallBorder.cgColor
+        }
     }
 
     override func mouseUp(with event: NSEvent) {
@@ -296,6 +323,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
 // MARK: - Footer Link
 
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
+
     init(title: String) {
         super.init(frame: .zero)
         self.title = title
@@ -303,13 +333,18 @@ private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
         font = AppTheme.regularFont(size: 11)
         translatesAutoresizingMaskIntoConstraints = false
         refreshAppearance()
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.isHovered = hovering
+            self?.refreshAppearance()
+        }
     }
 
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
 
     func refreshAppearance() {
-        contentTintColor = AppTheme.textSecondary
+        contentTintColor = isHovered ? AppTheme.textPrimary : AppTheme.textSecondary
     }
 
     override func resetCursorRects() {
@@ -392,6 +427,49 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
     }
 }
 
+// MARK: - CTA Button
+
+private final class PaywallCTAButton: NSButton, AppearanceRefreshable {
+    private var hoverTracker: HoverTracker?
+
+    init() {
+        super.init(frame: .zero)
+        isBordered = false
+        wantsLayer = true
+        layer?.cornerRadius = 12
+        font = AppTheme.semiboldFont(size: 15)
+        translatesAutoresizingMaskIntoConstraints = false
+        refreshAppearance()
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.setHovered(hovering)
+        }
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
+        contentTintColor = AppTheme.paywallCTAForeground
+    }
+
+    private func setHovered(_ hovering: Bool) {
+        let base = AppTheme.paywallCTABackground
+        let color = hovering ? base.blended(withFraction: 0.12, of: .black) ?? base : base
+        animateHover {
+            layer?.backgroundColor = color.cgColor
+            layer?.transform = hovering
+                ? CATransform3DMakeScale(1.02, 1.02, 1)
+                : CATransform3DIdentity
+        }
+    }
+
+    override func resetCursorRects() {
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}
+
 // MARK: - Main Paywall Card
 
 final class PaywallView: NSView, AppearanceRefreshable {
@@ -401,7 +479,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
 
     private var selectedPlan: PaywallPlan = .yearly
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
-    private let ctaButton = NSButton()
+    private let ctaButton = PaywallCTAButton()
     private var leftPanelTitle: NSTextField!
     private var rightTitle: NSTextField!
     private var rightSubtitle: NSTextField!
@@ -423,8 +501,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
         rightSubtitle?.refreshThemeLabelColor()
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
-        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
-        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
+        ctaButton.refreshAppearance()
         subviews.forEach { $0.refreshAppearanceRecursively() }
     }
 
@@ -531,10 +608,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
         }
 
         ctaButton.title = selectedPlan.ctaTitle
-        ctaButton.isBordered = false
-        ctaButton.wantsLayer = true
-        ctaButton.layer?.cornerRadius = 12
-        ctaButton.font = AppTheme.semiboldFont(size: 15)
         ctaButton.target = self
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.translatesAutoresizingMaskIntoConstraints = false