Selaa lähdekoodia

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 3 kuukautta sitten
vanhempi
sitoutus
01530aa0ec
1 muutettua tiedostoa jossa 115 lisäystä ja 16 poistoa
  1. 115 16
      meetings_app/ViewController.swift

+ 115 - 16
meetings_app/ViewController.swift

@@ -53,7 +53,7 @@ final class ViewController: NSViewController {
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
     private weak var centeredTitleLabel: NSTextField?
-    private weak var paywallWindow: NSWindow?
+    private var paywallWindow: NSWindow?
     private let paywallContentWidth: CGFloat = 520
     private var selectedPremiumPlan: PremiumPlan = .monthly
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
@@ -445,8 +445,11 @@ private extension ViewController {
         panel.title = "Get Premium"
         panel.titleVisibility = .hidden
         panel.titlebarAppearsTransparent = true
-        panel.isFloatingPanel = true
-        panel.hidesOnDeactivate = false
+        panel.isFloatingPanel = false
+        panel.level = .normal
+        panel.hidesOnDeactivate = true
+        panel.isReleasedWhenClosed = false
+        panel.delegate = self
         panel.standardWindowButton(.closeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.zoomButton)?.isHidden = true
@@ -457,9 +460,19 @@ private extension ViewController {
         paywallWindow = panel
     }
 
-    @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
-        paywallWindow?.close()
-        paywallWindow = nil
+    @objc private func closePaywallClicked(_ sender: Any?) {
+        if let win = paywallWindow {
+            win.performClose(nil)
+            return
+        }
+        if let gesture = sender as? NSGestureRecognizer, let win = gesture.view?.window {
+            win.performClose(nil)
+            return
+        }
+        if let view = sender as? NSView, let win = view.window {
+            win.performClose(nil)
+            return
+        }
     }
 
     @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
@@ -504,19 +517,22 @@ private extension ViewController {
         }
     }
 
-    private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
+    private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool, hovering: Bool = false) {
         let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
         let idleBorder = palette.inputBorder
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
+        let hoverIdleBackground =
+            palette.sectionCard.blended(withFraction: 0.10, of: hoverBlend) ?? palette.sectionCard
         let selectedBackground = darkModeEnabled
             ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
             : NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1)
-        card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
-        card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
+        card.layer?.backgroundColor = (isSelected ? selectedBackground : (hovering ? hoverIdleBackground : palette.sectionCard)).cgColor
+        card.layer?.borderColor = (isSelected ? selectedBorder : (hovering ? selectedBorder.withAlphaComponent(0.55) : idleBorder)).cgColor
         card.layer?.borderWidth = isSelected ? 2 : 1
         card.layer?.shadowColor = NSColor.black.cgColor
-        card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : 0.12
+        card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : (hovering ? 0.18 : 0.12)
         card.layer?.shadowOffset = CGSize(width: 0, height: -1)
-        card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : 5
+        card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : (hovering ? 7 : 5)
     }
 
     private func viewForPage(_ page: SidebarPage) -> NSView {
@@ -1052,10 +1068,28 @@ private extension ViewController {
         let topSpacer = NSView()
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
         topRow.addArrangedSubview(topSpacer)
-        let closeButton = iconRoundButton("✕", size: 28)
+        let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
+        closeButton.translatesAutoresizingMaskIntoConstraints = false
+        closeButton.isBordered = false
+        closeButton.bezelStyle = .regularSquare
+        closeButton.wantsLayer = true
+        closeButton.layer?.cornerRadius = 14
+        closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
+        closeButton.layer?.borderColor = palette.inputBorder.cgColor
+        closeButton.layer?.borderWidth = 1
+        closeButton.font = typography.iconButton
+        closeButton.contentTintColor = palette.textSecondary
+        closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
+        closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
+        closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
+            guard let closeButton, let self else { return }
+            let base = self.palette.inputBackground
+            let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
+            let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
+            closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
+            closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
+        }
         topRow.addArrangedSubview(closeButton)
-        let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
-        closeButton.addGestureRecognizer(closeClick)
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         contentStack.addArrangedSubview(topRow)
 
@@ -1125,8 +1159,11 @@ private extension ViewController {
         contentStack.addArrangedSubview(offerWrap)
         contentStack.setCustomSpacing(18, after: offerWrap)
 
-        let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
+        let continueButton = HoverTrackingView()
         continueButton.translatesAutoresizingMaskIntoConstraints = false
+        continueButton.wantsLayer = true
+        continueButton.layer?.cornerRadius = 14
+        continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
@@ -1136,6 +1173,13 @@ private extension ViewController {
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
             continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
         ])
+        let baseBlue = palette.primaryBlue
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
+        let hoverBlue = baseBlue.blended(withFraction: 0.10, of: hoverBlend) ?? baseBlue
+        continueButton.onHoverChanged = { hovering in
+            continueButton.layer?.backgroundColor = (hovering ? hoverBlue : baseBlue).cgColor
+        }
+        continueButton.onHoverChanged?(false)
         contentStack.addArrangedSubview(continueButton)
         contentStack.setCustomSpacing(16, after: continueButton)
 
@@ -1175,7 +1219,7 @@ private extension ViewController {
         plan: PremiumPlan,
         strikePrice: String?
     ) -> NSView {
-        let wrapper = NSView()
+        let wrapper = HoverTrackingView()
         wrapper.translatesAutoresizingMaskIntoConstraints = false
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
@@ -1249,6 +1293,11 @@ private extension ViewController {
         wrapper.addGestureRecognizer(click)
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
         paywallPlanViews[plan] = card
+        wrapper.onHoverChanged = { [weak self, weak card] hovering in
+            guard let self, let card else { return }
+            self.applyPaywallPlanStyle(card, isSelected: plan == self.selectedPremiumPlan, hovering: hovering)
+        }
+        wrapper.onHoverChanged?(false)
 
         return wrapper
     }
@@ -1761,6 +1810,15 @@ extension ViewController: NSTextFieldDelegate {
     }
 }
 
+extension ViewController: NSWindowDelegate {
+    func windowWillClose(_ notification: Notification) {
+        guard let closingWindow = notification.object as? NSWindow else { return }
+        if closingWindow === paywallWindow {
+            paywallWindow = nil
+        }
+    }
+}
+
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
 private class RowHitTestView: NSView {
     override func hitTest(_ point: NSPoint) -> NSView? {
@@ -1850,6 +1908,47 @@ private final class HoverSurfaceView: NSView {
     }
 }
 
+private final class HoverButton: NSButton {
+    var onHoverChanged: ((Bool) -> Void)?
+    var showsHandCursor = true
+
+    private var trackingAreaRef: NSTrackingArea?
+    private var isHovering = false {
+        didSet {
+            guard isHovering != oldValue else { return }
+            onHoverChanged?(isHovering)
+        }
+    }
+
+    override func updateTrackingAreas() {
+        super.updateTrackingAreas()
+        if let trackingAreaRef {
+            removeTrackingArea(trackingAreaRef)
+        }
+        let options: NSTrackingArea.Options = [
+            .activeInKeyWindow,
+            .inVisibleRect,
+            .mouseEnteredAndExited
+        ]
+        let tracking = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
+        addTrackingArea(tracking)
+        trackingAreaRef = tracking
+    }
+
+    override func mouseEntered(with event: NSEvent) {
+        super.mouseEntered(with: event)
+        if showsHandCursor {
+            NSCursor.pointingHand.set()
+        }
+        isHovering = true
+    }
+
+    override func mouseExited(with event: NSEvent) {
+        super.mouseExited(with: event)
+        isHovering = false
+    }
+}
+
 private final class SettingsMenuViewController: NSViewController {
     private let palette: Palette
     private let typography: Typography