Преглед изворни кода

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 53
     private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
54 54
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
55 55
     private weak var centeredTitleLabel: NSTextField?
56
-    private weak var paywallWindow: NSWindow?
56
+    private var paywallWindow: NSWindow?
57 57
     private let paywallContentWidth: CGFloat = 520
58 58
     private var selectedPremiumPlan: PremiumPlan = .monthly
59 59
     private var paywallPlanViews: [PremiumPlan: NSView] = [:]
@@ -445,8 +445,11 @@ private extension ViewController {
445 445
         panel.title = "Get Premium"
446 446
         panel.titleVisibility = .hidden
447 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 453
         panel.standardWindowButton(.closeButton)?.isHidden = true
451 454
         panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
452 455
         panel.standardWindowButton(.zoomButton)?.isHidden = true
@@ -457,9 +460,19 @@ private extension ViewController {
457 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 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 521
         let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
509 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 526
         let selectedBackground = darkModeEnabled
511 527
             ? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
512 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 531
         card.layer?.borderWidth = isSelected ? 2 : 1
516 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 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 538
     private func viewForPage(_ page: SidebarPage) -> NSView {
@@ -1052,10 +1068,28 @@ private extension ViewController {
1052 1068
         let topSpacer = NSView()
1053 1069
         topSpacer.translatesAutoresizingMaskIntoConstraints = false
1054 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 1092
         topRow.addArrangedSubview(closeButton)
1057
-        let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
1058
-        closeButton.addGestureRecognizer(closeClick)
1059 1093
         topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1060 1094
         contentStack.addArrangedSubview(topRow)
1061 1095
 
@@ -1125,8 +1159,11 @@ private extension ViewController {
1125 1159
         contentStack.addArrangedSubview(offerWrap)
1126 1160
         contentStack.setCustomSpacing(18, after: offerWrap)
1127 1161
 
1128
-        let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
1162
+        let continueButton = HoverTrackingView()
1129 1163
         continueButton.translatesAutoresizingMaskIntoConstraints = false
1164
+        continueButton.wantsLayer = true
1165
+        continueButton.layer?.cornerRadius = 14
1166
+        continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
1130 1167
         continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
1131 1168
         continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1132 1169
         styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
@@ -1136,6 +1173,13 @@ private extension ViewController {
1136 1173
             continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
1137 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 1183
         contentStack.addArrangedSubview(continueButton)
1140 1184
         contentStack.setCustomSpacing(16, after: continueButton)
1141 1185
 
@@ -1175,7 +1219,7 @@ private extension ViewController {
1175 1219
         plan: PremiumPlan,
1176 1220
         strikePrice: String?
1177 1221
     ) -> NSView {
1178
-        let wrapper = NSView()
1222
+        let wrapper = HoverTrackingView()
1179 1223
         wrapper.translatesAutoresizingMaskIntoConstraints = false
1180 1224
         wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
1181 1225
         wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
@@ -1249,6 +1293,11 @@ private extension ViewController {
1249 1293
         wrapper.addGestureRecognizer(click)
1250 1294
         premiumPlanByView[ObjectIdentifier(wrapper)] = plan
1251 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 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 1822
 /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
1765 1823
 private class RowHitTestView: NSView {
1766 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 1952
 private final class SettingsMenuViewController: NSViewController {
1854 1953
     private let palette: Palette
1855 1954
     private let typography: Typography