Преглед на файлове

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 часа
родител
ревизия
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 {
188
     private let subtitleLabel = NSTextField(labelWithString: "")
188
     private let subtitleLabel = NSTextField(labelWithString: "")
189
     private let priceLabel = NSTextField(labelWithString: "")
189
     private let priceLabel = NSTextField(labelWithString: "")
190
     private var badgeView: PaywallBadgeView?
190
     private var badgeView: PaywallBadgeView?
191
+    private var hoverTracker: HoverTracker?
192
+    private var isHovered = false
191
 
193
 
192
     var isChosen: Bool = false {
194
     var isChosen: Bool = false {
193
         didSet { updateAppearance() }
195
         didSet { updateAppearance() }
@@ -262,7 +264,12 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
262
             ])
264
             ])
263
         }
265
         }
264
 
266
 
267
+        applyCardShadow()
265
         updateAppearance()
268
         updateAppearance()
269
+
270
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
271
+            self?.setHovered(hovering)
272
+        }
266
     }
273
     }
267
 
274
 
268
     @available(*, unavailable)
275
     @available(*, unavailable)
@@ -271,6 +278,15 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
271
     func refreshAppearance() {
278
     func refreshAppearance() {
272
         updateAppearance()
279
         updateAppearance()
273
         subtitleLabel.refreshThemeLabelColor()
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
     private func updateAppearance() {
292
     private func updateAppearance() {
@@ -279,8 +295,19 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
279
         priceLabel.textColor = titleColor
295
         priceLabel.textColor = titleColor
280
 
296
 
281
         layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
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
     override func mouseUp(with event: NSEvent) {
313
     override func mouseUp(with event: NSEvent) {
@@ -296,6 +323,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
296
 // MARK: - Footer Link
323
 // MARK: - Footer Link
297
 
324
 
298
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
325
 private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
326
+    private var hoverTracker: HoverTracker?
327
+    private var isHovered = false
328
+
299
     init(title: String) {
329
     init(title: String) {
300
         super.init(frame: .zero)
330
         super.init(frame: .zero)
301
         self.title = title
331
         self.title = title
@@ -303,13 +333,18 @@ private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
303
         font = AppTheme.regularFont(size: 11)
333
         font = AppTheme.regularFont(size: 11)
304
         translatesAutoresizingMaskIntoConstraints = false
334
         translatesAutoresizingMaskIntoConstraints = false
305
         refreshAppearance()
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
     @available(*, unavailable)
343
     @available(*, unavailable)
309
     required init?(coder: NSCoder) { nil }
344
     required init?(coder: NSCoder) { nil }
310
 
345
 
311
     func refreshAppearance() {
346
     func refreshAppearance() {
312
-        contentTintColor = AppTheme.textSecondary
347
+        contentTintColor = isHovered ? AppTheme.textPrimary : AppTheme.textSecondary
313
     }
348
     }
314
 
349
 
315
     override func resetCursorRects() {
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
 // MARK: - Main Paywall Card
473
 // MARK: - Main Paywall Card
396
 
474
 
397
 final class PaywallView: NSView, AppearanceRefreshable {
475
 final class PaywallView: NSView, AppearanceRefreshable {
@@ -401,7 +479,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
401
 
479
 
402
     private var selectedPlan: PaywallPlan = .yearly
480
     private var selectedPlan: PaywallPlan = .yearly
403
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
481
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
404
-    private let ctaButton = NSButton()
482
+    private let ctaButton = PaywallCTAButton()
405
     private var leftPanelTitle: NSTextField!
483
     private var leftPanelTitle: NSTextField!
406
     private var rightTitle: NSTextField!
484
     private var rightTitle: NSTextField!
407
     private var rightSubtitle: NSTextField!
485
     private var rightSubtitle: NSTextField!
@@ -423,8 +501,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
423
         rightSubtitle?.refreshThemeLabelColor()
501
         rightSubtitle?.refreshThemeLabelColor()
424
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
502
         trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
425
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
503
         trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
426
-        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
427
-        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
504
+        ctaButton.refreshAppearance()
428
         subviews.forEach { $0.refreshAppearanceRecursively() }
505
         subviews.forEach { $0.refreshAppearanceRecursively() }
429
     }
506
     }
430
 
507
 
@@ -531,10 +608,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
531
         }
608
         }
532
 
609
 
533
         ctaButton.title = selectedPlan.ctaTitle
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
         ctaButton.target = self
611
         ctaButton.target = self
539
         ctaButton.action = #selector(purchaseTapped)
612
         ctaButton.action = #selector(purchaseTapped)
540
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
613
         ctaButton.translatesAutoresizingMaskIntoConstraints = false