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

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 месяцев назад
Родитель
Сommit
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 {
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
     private weak var centeredTitleLabel: NSTextField?
     private weak var centeredTitleLabel: NSTextField?
-    private weak var paywallWindow: NSWindow?
+    private var paywallWindow: NSWindow?
     private let paywallContentWidth: CGFloat = 520
     private let paywallContentWidth: CGFloat = 520
     private var selectedPremiumPlan: PremiumPlan = .monthly
     private var selectedPremiumPlan: PremiumPlan = .monthly
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
@@ -445,8 +445,11 @@ private extension ViewController {
         panel.title = "Get Premium"
         panel.title = "Get Premium"
         panel.titleVisibility = .hidden
         panel.titleVisibility = .hidden
         panel.titlebarAppearsTransparent = true
         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(.closeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
         panel.standardWindowButton(.zoomButton)?.isHidden = true
         panel.standardWindowButton(.zoomButton)?.isHidden = true
@@ -457,9 +460,19 @@ private extension ViewController {
         paywallWindow = panel
         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) {
     @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 selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
         let idleBorder = palette.inputBorder
         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
         let selectedBackground = darkModeEnabled
             ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
             ? 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)
             : 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?.borderWidth = isSelected ? 2 : 1
         card.layer?.shadowColor = NSColor.black.cgColor
         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?.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 {
     private func viewForPage(_ page: SidebarPage) -> NSView {
@@ -1052,10 +1068,28 @@ private extension ViewController {
         let topSpacer = NSView()
         let topSpacer = NSView()
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
         topRow.addArrangedSubview(topSpacer)
         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)
         topRow.addArrangedSubview(closeButton)
-        let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
-        closeButton.addGestureRecognizer(closeClick)
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         contentStack.addArrangedSubview(topRow)
         contentStack.addArrangedSubview(topRow)
 
 
@@ -1125,8 +1159,11 @@ private extension ViewController {
         contentStack.addArrangedSubview(offerWrap)
         contentStack.addArrangedSubview(offerWrap)
         contentStack.setCustomSpacing(18, after: offerWrap)
         contentStack.setCustomSpacing(18, after: offerWrap)
 
 
-        let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
+        let continueButton = HoverTrackingView()
         continueButton.translatesAutoresizingMaskIntoConstraints = false
         continueButton.translatesAutoresizingMaskIntoConstraints = false
+        continueButton.wantsLayer = true
+        continueButton.layer?.cornerRadius = 14
+        continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
@@ -1136,6 +1173,13 @@ private extension ViewController {
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
             continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
             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.addArrangedSubview(continueButton)
         contentStack.setCustomSpacing(16, after: continueButton)
         contentStack.setCustomSpacing(16, after: continueButton)
 
 
@@ -1175,7 +1219,7 @@ private extension ViewController {
         plan: PremiumPlan,
         plan: PremiumPlan,
         strikePrice: String?
         strikePrice: String?
     ) -> NSView {
     ) -> NSView {
-        let wrapper = NSView()
+        let wrapper = HoverTrackingView()
         wrapper.translatesAutoresizingMaskIntoConstraints = false
         wrapper.translatesAutoresizingMaskIntoConstraints = false
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
@@ -1249,6 +1293,11 @@ private extension ViewController {
         wrapper.addGestureRecognizer(click)
         wrapper.addGestureRecognizer(click)
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
         paywallPlanViews[plan] = card
         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
         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.
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
 private class RowHitTestView: NSView {
 private class RowHitTestView: NSView {
     override func hitTest(_ point: NSPoint) -> 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 final class SettingsMenuViewController: NSViewController {
     private let palette: Palette
     private let palette: Palette
     private let typography: Typography
     private let typography: Typography