Bläddra i källkod

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 6 timmar sedan
förälder
incheckning
1b5da34bb3
1 ändrade filer med 83 tillägg och 10 borttagningar
  1. 83 10
      smart_printer/PaywallView.swift

+ 83 - 10
smart_printer/PaywallView.swift

@@ -188,6 +188,8 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
188 188
     private let subtitleLabel = NSTextField(labelWithString: "")
189 189
     private let priceLabel = NSTextField(labelWithString: "")
190 190
     private var badgeView: PaywallBadgeView?
191
+    private var hoverTracker: HoverTracker?
192
+    private var isHovered = false
191 193
 
192 194
     var isChosen: Bool = false {
193 195
         didSet { updateAppearance() }
@@ -262,7 +264,12 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
262 264
             ])
263 265
         }
264 266
 
267
+        applyCardShadow()
265 268
         updateAppearance()
269
+
270
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
271
+            self?.setHovered(hovering)
272
+        }
266 273
     }
267 274
 
268 275
     @available(*, unavailable)
@@ -271,6 +278,15 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
271 278
     func refreshAppearance() {
272 279
         updateAppearance()
273 280
         subtitleLabel.refreshThemeLabelColor()
281
+        if isHovered {
282
+            applyHoverLift(true)
283
+        }
284
+    }
285
+
286
+    private func setHovered(_ hovering: Bool) {
287
+        isHovered = hovering
288
+        applyHoverLift(hovering)
289
+        updateAppearance()
274 290
     }
275 291
 
276 292
     private func updateAppearance() {
@@ -279,8 +295,19 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
279 295
         priceLabel.textColor = titleColor
280 296
 
281 297
         layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
282
-        layer?.borderWidth = isChosen ? 2 : 1
283
-        layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
298
+
299
+        if isChosen {
300
+            layer?.borderWidth = 2
301
+            layer?.borderColor = AppTheme.green.cgColor
302
+        } else if isHovered {
303
+            layer?.borderWidth = 1.5
304
+            let hoverBorder = AppTheme.paywallBorder.blended(withFraction: 0.35, of: AppTheme.paywallAccent)
305
+                ?? AppTheme.paywallBorder
306
+            layer?.borderColor = hoverBorder.cgColor
307
+        } else {
308
+            layer?.borderWidth = 1
309
+            layer?.borderColor = AppTheme.paywallBorder.cgColor
310
+        }
284 311
     }
285 312
 
286 313
     override func mouseUp(with event: NSEvent) {
@@ -296,6 +323,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
296 323
 // MARK: - Footer Link
297 324
 
298 325
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
326
+    private var hoverTracker: HoverTracker?
327
+    private var isHovered = false
328
+
299 329
     init(title: String) {
300 330
         super.init(frame: .zero)
301 331
         self.title = title
@@ -303,13 +333,18 @@ private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
303 333
         font = AppTheme.regularFont(size: 11)
304 334
         translatesAutoresizingMaskIntoConstraints = false
305 335
         refreshAppearance()
336
+
337
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
338
+            self?.isHovered = hovering
339
+            self?.refreshAppearance()
340
+        }
306 341
     }
307 342
 
308 343
     @available(*, unavailable)
309 344
     required init?(coder: NSCoder) { nil }
310 345
 
311 346
     func refreshAppearance() {
312
-        contentTintColor = AppTheme.textSecondary
347
+        contentTintColor = isHovered ? AppTheme.textPrimary : AppTheme.textSecondary
313 348
     }
314 349
 
315 350
     override func resetCursorRects() {
@@ -392,6 +427,49 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
392 427
     }
393 428
 }
394 429
 
430
+// MARK: - CTA Button
431
+
432
+private final class PaywallCTAButton: NSButton, AppearanceRefreshable {
433
+    private var hoverTracker: HoverTracker?
434
+
435
+    init() {
436
+        super.init(frame: .zero)
437
+        isBordered = false
438
+        wantsLayer = true
439
+        layer?.cornerRadius = 12
440
+        font = AppTheme.semiboldFont(size: 15)
441
+        translatesAutoresizingMaskIntoConstraints = false
442
+        refreshAppearance()
443
+
444
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
445
+            self?.setHovered(hovering)
446
+        }
447
+    }
448
+
449
+    @available(*, unavailable)
450
+    required init?(coder: NSCoder) { nil }
451
+
452
+    func refreshAppearance() {
453
+        layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
454
+        contentTintColor = AppTheme.paywallCTAForeground
455
+    }
456
+
457
+    private func setHovered(_ hovering: Bool) {
458
+        let base = AppTheme.paywallCTABackground
459
+        let color = hovering ? base.blended(withFraction: 0.12, of: .black) ?? base : base
460
+        animateHover {
461
+            layer?.backgroundColor = color.cgColor
462
+            layer?.transform = hovering
463
+                ? CATransform3DMakeScale(1.02, 1.02, 1)
464
+                : CATransform3DIdentity
465
+        }
466
+    }
467
+
468
+    override func resetCursorRects() {
469
+        addCursorRect(bounds, cursor: .pointingHand)
470
+    }
471
+}
472
+
395 473
 // MARK: - Main Paywall Card
396 474
 
397 475
 final class PaywallView: NSView, AppearanceRefreshable {
@@ -401,7 +479,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
401 479
 
402 480
     private var selectedPlan: PaywallPlan = .yearly
403 481
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
404
-    private let ctaButton = NSButton()
482
+    private let ctaButton = PaywallCTAButton()
405 483
     private var leftPanelTitle: NSTextField!
406 484
     private var rightTitle: NSTextField!
407 485
     private var rightSubtitle: NSTextField!
@@ -423,8 +501,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
423 501
         rightSubtitle?.refreshThemeLabelColor()
424 502
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
425 503
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
426
-        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
427
-        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
504
+        ctaButton.refreshAppearance()
428 505
         subviews.forEach { $0.refreshAppearanceRecursively() }
429 506
     }
430 507
 
@@ -531,10 +608,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
531 608
         }
532 609
 
533 610
         ctaButton.title = selectedPlan.ctaTitle
534
-        ctaButton.isBordered = false
535
-        ctaButton.wantsLayer = true
536
-        ctaButton.layer?.cornerRadius = 12
537
-        ctaButton.font = AppTheme.semiboldFont(size: 15)
538 611
         ctaButton.target = self
539 612
         ctaButton.action = #selector(purchaseTapped)
540 613
         ctaButton.translatesAutoresizingMaskIntoConstraints = false