Просмотр исходного кода

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 месяц назад
Родитель
Сommit
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 subtitleLabel = NSTextField(labelWithString: "")
     private let priceLabel = NSTextField(labelWithString: "")
     private let priceLabel = NSTextField(labelWithString: "")
     private var badgeView: PaywallBadgeView?
     private var badgeView: PaywallBadgeView?
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
 
 
     var isChosen: Bool = false {
     var isChosen: Bool = false {
         didSet { updateAppearance() }
         didSet { updateAppearance() }
@@ -262,7 +264,12 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
             ])
             ])
         }
         }
 
 
+        applyCardShadow()
         updateAppearance()
         updateAppearance()
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.setHovered(hovering)
+        }
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
@@ -271,6 +278,15 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     func refreshAppearance() {
     func refreshAppearance() {
         updateAppearance()
         updateAppearance()
         subtitleLabel.refreshThemeLabelColor()
         subtitleLabel.refreshThemeLabelColor()
+        if isHovered {
+            applyHoverLift(true)
+        }
+    }
+
+    private func setHovered(_ hovering: Bool) {
+        isHovered = hovering
+        applyHoverLift(hovering)
+        updateAppearance()
     }
     }
 
 
     private func updateAppearance() {
     private func updateAppearance() {
@@ -279,8 +295,19 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
         priceLabel.textColor = titleColor
         priceLabel.textColor = titleColor
 
 
         layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
         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) {
     override func mouseUp(with event: NSEvent) {
@@ -296,6 +323,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
 // MARK: - Footer Link
 // MARK: - Footer Link
 
 
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
+
     init(title: String) {
     init(title: String) {
         super.init(frame: .zero)
         super.init(frame: .zero)
         self.title = title
         self.title = title
@@ -303,13 +333,18 @@ private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
         font = AppTheme.regularFont(size: 11)
         font = AppTheme.regularFont(size: 11)
         translatesAutoresizingMaskIntoConstraints = false
         translatesAutoresizingMaskIntoConstraints = false
         refreshAppearance()
         refreshAppearance()
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.isHovered = hovering
+            self?.refreshAppearance()
+        }
     }
     }
 
 
     @available(*, unavailable)
     @available(*, unavailable)
     required init?(coder: NSCoder) { nil }
     required init?(coder: NSCoder) { nil }
 
 
     func refreshAppearance() {
     func refreshAppearance() {
-        contentTintColor = AppTheme.textSecondary
+        contentTintColor = isHovered ? AppTheme.textPrimary : AppTheme.textSecondary
     }
     }
 
 
     override func resetCursorRects() {
     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
 // MARK: - Main Paywall Card
 
 
 final class PaywallView: NSView, AppearanceRefreshable {
 final class PaywallView: NSView, AppearanceRefreshable {
@@ -401,7 +479,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
 
 
     private var selectedPlan: PaywallPlan = .yearly
     private var selectedPlan: PaywallPlan = .yearly
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
-    private let ctaButton = NSButton()
+    private let ctaButton = PaywallCTAButton()
     private var leftPanelTitle: NSTextField!
     private var leftPanelTitle: NSTextField!
     private var rightTitle: NSTextField!
     private var rightTitle: NSTextField!
     private var rightSubtitle: NSTextField!
     private var rightSubtitle: NSTextField!
@@ -423,8 +501,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
         rightSubtitle?.refreshThemeLabelColor()
         rightSubtitle?.refreshThemeLabelColor()
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
-        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
-        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
+        ctaButton.refreshAppearance()
         subviews.forEach { $0.refreshAppearanceRecursively() }
         subviews.forEach { $0.refreshAppearanceRecursively() }
     }
     }
 
 
@@ -531,10 +608,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
         }
         }
 
 
         ctaButton.title = selectedPlan.ctaTitle
         ctaButton.title = selectedPlan.ctaTitle
-        ctaButton.isBordered = false
-        ctaButton.wantsLayer = true
-        ctaButton.layer?.cornerRadius = 12
-        ctaButton.font = AppTheme.semiboldFont(size: 15)
         ctaButton.target = self
         ctaButton.target = self
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.action = #selector(purchaseTapped)
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
         ctaButton.translatesAutoresizingMaskIntoConstraints = false