瀏覽代碼

Add hover states for paywall controls

Add hover feedback for the paywall close button, Continue CTA, and subscription plan cards (while preserving selected styling). Introduce HoverButton to support reliable hover tracking on NSButton.

Made-with: Cursor
huzaifahayat12 1 周之前
父節點
當前提交
01530aa0ec
共有 1 個文件被更改,包括 115 次插入16 次删除
  1. 115 16
      meetings_app/ViewController.swift

+ 115 - 16
meetings_app/ViewController.swift

@@ -53,7 +53,7 @@ final class ViewController: NSViewController {
53
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
53
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
54
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
54
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
55
     private weak var centeredTitleLabel: NSTextField?
55
     private weak var centeredTitleLabel: NSTextField?
56
-    private weak var paywallWindow: NSWindow?
56
+    private var paywallWindow: NSWindow?
57
     private let paywallContentWidth: CGFloat = 520
57
     private let paywallContentWidth: CGFloat = 520
58
     private var selectedPremiumPlan: PremiumPlan = .monthly
58
     private var selectedPremiumPlan: PremiumPlan = .monthly
59
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
59
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
@@ -445,8 +445,11 @@ private extension ViewController {
445
         panel.title = "Get Premium"
445
         panel.title = "Get Premium"
446
         panel.titleVisibility = .hidden
446
         panel.titleVisibility = .hidden
447
         panel.titlebarAppearsTransparent = true
447
         panel.titlebarAppearsTransparent = true
448
-        panel.isFloatingPanel = true
449
-        panel.hidesOnDeactivate = false
448
+        panel.isFloatingPanel = false
449
+        panel.level = .normal
450
+        panel.hidesOnDeactivate = true
451
+        panel.isReleasedWhenClosed = false
452
+        panel.delegate = self
450
         panel.standardWindowButton(.closeButton)?.isHidden = true
453
         panel.standardWindowButton(.closeButton)?.isHidden = true
451
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
454
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
452
         panel.standardWindowButton(.zoomButton)?.isHidden = true
455
         panel.standardWindowButton(.zoomButton)?.isHidden = true
@@ -457,9 +460,19 @@ private extension ViewController {
457
         paywallWindow = panel
460
         paywallWindow = panel
458
     }
461
     }
459
 
462
 
460
-    @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
461
-        paywallWindow?.close()
462
-        paywallWindow = nil
463
+    @objc private func closePaywallClicked(_ sender: Any?) {
464
+        if let win = paywallWindow {
465
+            win.performClose(nil)
466
+            return
467
+        }
468
+        if let gesture = sender as? NSGestureRecognizer, let win = gesture.view?.window {
469
+            win.performClose(nil)
470
+            return
471
+        }
472
+        if let view = sender as? NSView, let win = view.window {
473
+            win.performClose(nil)
474
+            return
475
+        }
463
     }
476
     }
464
 
477
 
465
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
478
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
@@ -504,19 +517,22 @@ private extension ViewController {
504
         }
517
         }
505
     }
518
     }
506
 
519
 
507
-    private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
520
+    private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool, hovering: Bool = false) {
508
         let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
521
         let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
509
         let idleBorder = palette.inputBorder
522
         let idleBorder = palette.inputBorder
523
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
524
+        let hoverIdleBackground =
525
+            palette.sectionCard.blended(withFraction: 0.10, of: hoverBlend) ?? palette.sectionCard
510
         let selectedBackground = darkModeEnabled
526
         let selectedBackground = darkModeEnabled
511
             ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
527
             ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
512
             : NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1)
528
             : NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1)
513
-        card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
514
-        card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
529
+        card.layer?.backgroundColor = (isSelected ? selectedBackground : (hovering ? hoverIdleBackground : palette.sectionCard)).cgColor
530
+        card.layer?.borderColor = (isSelected ? selectedBorder : (hovering ? selectedBorder.withAlphaComponent(0.55) : idleBorder)).cgColor
515
         card.layer?.borderWidth = isSelected ? 2 : 1
531
         card.layer?.borderWidth = isSelected ? 2 : 1
516
         card.layer?.shadowColor = NSColor.black.cgColor
532
         card.layer?.shadowColor = NSColor.black.cgColor
517
-        card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : 0.12
533
+        card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : (hovering ? 0.18 : 0.12)
518
         card.layer?.shadowOffset = CGSize(width: 0, height: -1)
534
         card.layer?.shadowOffset = CGSize(width: 0, height: -1)
519
-        card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : 5
535
+        card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : (hovering ? 7 : 5)
520
     }
536
     }
521
 
537
 
522
     private func viewForPage(_ page: SidebarPage) -> NSView {
538
     private func viewForPage(_ page: SidebarPage) -> NSView {
@@ -1052,10 +1068,28 @@ private extension ViewController {
1052
         let topSpacer = NSView()
1068
         let topSpacer = NSView()
1053
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
1069
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
1054
         topRow.addArrangedSubview(topSpacer)
1070
         topRow.addArrangedSubview(topSpacer)
1055
-        let closeButton = iconRoundButton("✕", size: 28)
1071
+        let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
1072
+        closeButton.translatesAutoresizingMaskIntoConstraints = false
1073
+        closeButton.isBordered = false
1074
+        closeButton.bezelStyle = .regularSquare
1075
+        closeButton.wantsLayer = true
1076
+        closeButton.layer?.cornerRadius = 14
1077
+        closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
1078
+        closeButton.layer?.borderColor = palette.inputBorder.cgColor
1079
+        closeButton.layer?.borderWidth = 1
1080
+        closeButton.font = typography.iconButton
1081
+        closeButton.contentTintColor = palette.textSecondary
1082
+        closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
1083
+        closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
1084
+        closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
1085
+            guard let closeButton, let self else { return }
1086
+            let base = self.palette.inputBackground
1087
+            let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
1088
+            let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
1089
+            closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
1090
+            closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
1091
+        }
1056
         topRow.addArrangedSubview(closeButton)
1092
         topRow.addArrangedSubview(closeButton)
1057
-        let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
1058
-        closeButton.addGestureRecognizer(closeClick)
1059
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1093
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1060
         contentStack.addArrangedSubview(topRow)
1094
         contentStack.addArrangedSubview(topRow)
1061
 
1095
 
@@ -1125,8 +1159,11 @@ private extension ViewController {
1125
         contentStack.addArrangedSubview(offerWrap)
1159
         contentStack.addArrangedSubview(offerWrap)
1126
         contentStack.setCustomSpacing(18, after: offerWrap)
1160
         contentStack.setCustomSpacing(18, after: offerWrap)
1127
 
1161
 
1128
-        let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
1162
+        let continueButton = HoverTrackingView()
1129
         continueButton.translatesAutoresizingMaskIntoConstraints = false
1163
         continueButton.translatesAutoresizingMaskIntoConstraints = false
1164
+        continueButton.wantsLayer = true
1165
+        continueButton.layer?.cornerRadius = 14
1166
+        continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
1130
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
1167
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
1131
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1168
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1132
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
1169
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
@@ -1136,6 +1173,13 @@ private extension ViewController {
1136
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
1173
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
1137
             continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
1174
             continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
1138
         ])
1175
         ])
1176
+        let baseBlue = palette.primaryBlue
1177
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
1178
+        let hoverBlue = baseBlue.blended(withFraction: 0.10, of: hoverBlend) ?? baseBlue
1179
+        continueButton.onHoverChanged = { hovering in
1180
+            continueButton.layer?.backgroundColor = (hovering ? hoverBlue : baseBlue).cgColor
1181
+        }
1182
+        continueButton.onHoverChanged?(false)
1139
         contentStack.addArrangedSubview(continueButton)
1183
         contentStack.addArrangedSubview(continueButton)
1140
         contentStack.setCustomSpacing(16, after: continueButton)
1184
         contentStack.setCustomSpacing(16, after: continueButton)
1141
 
1185
 
@@ -1175,7 +1219,7 @@ private extension ViewController {
1175
         plan: PremiumPlan,
1219
         plan: PremiumPlan,
1176
         strikePrice: String?
1220
         strikePrice: String?
1177
     ) -> NSView {
1221
     ) -> NSView {
1178
-        let wrapper = NSView()
1222
+        let wrapper = HoverTrackingView()
1179
         wrapper.translatesAutoresizingMaskIntoConstraints = false
1223
         wrapper.translatesAutoresizingMaskIntoConstraints = false
1180
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1224
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1181
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
1225
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
@@ -1249,6 +1293,11 @@ private extension ViewController {
1249
         wrapper.addGestureRecognizer(click)
1293
         wrapper.addGestureRecognizer(click)
1250
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
1294
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
1251
         paywallPlanViews[plan] = card
1295
         paywallPlanViews[plan] = card
1296
+        wrapper.onHoverChanged = { [weak self, weak card] hovering in
1297
+            guard let self, let card else { return }
1298
+            self.applyPaywallPlanStyle(card, isSelected: plan == self.selectedPremiumPlan, hovering: hovering)
1299
+        }
1300
+        wrapper.onHoverChanged?(false)
1252
 
1301
 
1253
         return wrapper
1302
         return wrapper
1254
     }
1303
     }
@@ -1761,6 +1810,15 @@ extension ViewController: NSTextFieldDelegate {
1761
     }
1810
     }
1762
 }
1811
 }
1763
 
1812
 
1813
+extension ViewController: NSWindowDelegate {
1814
+    func windowWillClose(_ notification: Notification) {
1815
+        guard let closingWindow = notification.object as? NSWindow else { return }
1816
+        if closingWindow === paywallWindow {
1817
+            paywallWindow = nil
1818
+        }
1819
+    }
1820
+}
1821
+
1764
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
1822
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
1765
 private class RowHitTestView: NSView {
1823
 private class RowHitTestView: NSView {
1766
     override func hitTest(_ point: NSPoint) -> NSView? {
1824
     override func hitTest(_ point: NSPoint) -> NSView? {
@@ -1850,6 +1908,47 @@ private final class HoverSurfaceView: NSView {
1850
     }
1908
     }
1851
 }
1909
 }
1852
 
1910
 
1911
+private final class HoverButton: NSButton {
1912
+    var onHoverChanged: ((Bool) -> Void)?
1913
+    var showsHandCursor = true
1914
+
1915
+    private var trackingAreaRef: NSTrackingArea?
1916
+    private var isHovering = false {
1917
+        didSet {
1918
+            guard isHovering != oldValue else { return }
1919
+            onHoverChanged?(isHovering)
1920
+        }
1921
+    }
1922
+
1923
+    override func updateTrackingAreas() {
1924
+        super.updateTrackingAreas()
1925
+        if let trackingAreaRef {
1926
+            removeTrackingArea(trackingAreaRef)
1927
+        }
1928
+        let options: NSTrackingArea.Options = [
1929
+            .activeInKeyWindow,
1930
+            .inVisibleRect,
1931
+            .mouseEnteredAndExited
1932
+        ]
1933
+        let tracking = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
1934
+        addTrackingArea(tracking)
1935
+        trackingAreaRef = tracking
1936
+    }
1937
+
1938
+    override func mouseEntered(with event: NSEvent) {
1939
+        super.mouseEntered(with: event)
1940
+        if showsHandCursor {
1941
+            NSCursor.pointingHand.set()
1942
+        }
1943
+        isHovering = true
1944
+    }
1945
+
1946
+    override func mouseExited(with event: NSEvent) {
1947
+        super.mouseExited(with: event)
1948
+        isHovering = false
1949
+    }
1950
+}
1951
+
1853
 private final class SettingsMenuViewController: NSViewController {
1952
 private final class SettingsMenuViewController: NSViewController {
1854
     private let palette: Palette
1953
     private let palette: Palette
1855
     private let typography: Typography
1954
     private let typography: Typography