Procházet zdrojové kódy

Polish Premium Plans window layout and pricing cards

- Add hover border/shadow on pricing cards via HoverPricingCardView
- Keep cards equal height; use flexible spacer before the action button
- Position trial pill top-right; build card stack conditionally for billing lines
- Add top inset below the divider so feature lists breathe (20pt)
- Minor tweaks: yearly pill copy, button height, card bottom inset

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 2 měsíci
rodič
revize
1814456f74

+ 104 - 26
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -30,6 +30,68 @@ final class PremiumPlansWindowController: NSWindowController {
 }
 
 private final class PremiumPlansViewController: NSViewController {
+    private final class HoverPricingCardView: NSView {
+        private let baseBorderColor: NSColor
+        private let hoverBorderColor: NSColor
+        private var trackingAreaRef: NSTrackingArea?
+
+        init(baseBorderColor: NSColor, hoverBorderColor: NSColor) {
+            self.baseBorderColor = baseBorderColor
+            self.hoverBorderColor = hoverBorderColor
+            super.init(frame: .zero)
+            wantsLayer = true
+            layer?.cornerRadius = 16
+            applyHoverStyle(isHovered: false, animated: false)
+        }
+
+        @available(*, unavailable)
+        required init?(coder: NSCoder) {
+            nil
+        }
+
+        override func updateTrackingAreas() {
+            super.updateTrackingAreas()
+            if let trackingAreaRef {
+                removeTrackingArea(trackingAreaRef)
+            }
+            let options: NSTrackingArea.Options = [.activeInActiveApp, .mouseEnteredAndExited, .inVisibleRect]
+            let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
+            addTrackingArea(area)
+            trackingAreaRef = area
+        }
+
+        override func mouseEntered(with event: NSEvent) {
+            super.mouseEntered(with: event)
+            applyHoverStyle(isHovered: true, animated: true)
+        }
+
+        override func mouseExited(with event: NSEvent) {
+            super.mouseExited(with: event)
+            applyHoverStyle(isHovered: false, animated: true)
+        }
+
+        private func applyHoverStyle(isHovered: Bool, animated: Bool) {
+            guard let layer else { return }
+            let updates = {
+                layer.borderWidth = isHovered ? 2 : 1
+                layer.borderColor = (isHovered ? self.hoverBorderColor : self.baseBorderColor).cgColor
+                layer.shadowColor = self.hoverBorderColor.withAlphaComponent(0.35).cgColor
+                layer.shadowOpacity = isHovered ? 0.22 : 0
+                layer.shadowRadius = isHovered ? 14 : 0
+                layer.shadowOffset = .init(width: 0, height: -2)
+            }
+
+            if animated {
+                NSAnimationContext.runAnimationGroup { context in
+                    context.duration = 0.16
+                    updates()
+                }
+            } else {
+                updates()
+            }
+        }
+    }
+
     private struct Plan {
         let id: String
         let title: String
@@ -107,7 +169,7 @@ private final class PremiumPlansViewController: NSViewController {
             subtitle: "Best value for long-term users",
             price: "$39.99",
             period: "/ year",
-            billedPill: "3-day free trial",
+            billedPill: "3 days free trial",
             billedLine: "",
             crossedPrice: nil,
             savingsText: nil,
@@ -176,6 +238,9 @@ private final class PremiumPlansViewController: NSViewController {
         cardsRow.alignment = .top
         cardsRow.distribution = .fillEqually
         cardsRow.translatesAutoresizingMaskIntoConstraints = false
+        for card in cardsRow.arrangedSubviews {
+            card.heightAnchor.constraint(equalTo: cardsRow.heightAnchor).isActive = true
+        }
 
         let trustRow = makeTrustRow()
         let footerRow = makeFooterRow()
@@ -206,13 +271,9 @@ private final class PremiumPlansViewController: NSViewController {
     }
 
     private func makePricingCard(_ plan: Plan) -> NSView {
-        let card = NSView()
+        let card = HoverPricingCardView(baseBorderColor: Theme.cardBorder, hoverBorderColor: Theme.accent)
         card.translatesAutoresizingMaskIntoConstraints = false
-        card.wantsLayer = true
         card.layer?.backgroundColor = Theme.cardBackground.cgColor
-        card.layer?.cornerRadius = 16
-        card.layer?.borderWidth = plan.highlight ? 2 : 1
-        card.layer?.borderColor = (plan.highlight ? Theme.accent : Theme.cardBorder).cgColor
 
         let iconWell = NSView()
         iconWell.translatesAutoresizingMaskIntoConstraints = false
@@ -243,8 +304,10 @@ private final class PremiumPlansViewController: NSViewController {
         subtitleLabel.textColor = Theme.secondaryText
         subtitleLabel.alignment = .center
 
-        let billingPill = pillLabel(text: plan.billedPill, tint: Theme.bottomStrip, textColor: Theme.iconTint)
-        billingPill.isHidden = plan.billedPill.isEmpty
+        let topRightTag = pillLabel(text: plan.billedPill, tint: Theme.bottomStrip, textColor: Theme.iconTint)
+        topRightTag.isHidden = plan.billedPill.isEmpty
+        topRightTag.font = .systemFont(ofSize: 10, weight: .bold)
+        topRightTag.heightAnchor.constraint(equalToConstant: 20).isActive = true
 
         let priceLabel = NSTextField(labelWithString: plan.price)
         priceLabel.font = .systemFont(ofSize: 18, weight: .semibold)
@@ -262,10 +325,8 @@ private final class PremiumPlansViewController: NSViewController {
         let billingLabel = NSTextField(labelWithString: plan.billedLine)
         billingLabel.font = .systemFont(ofSize: 13, weight: .medium)
         billingLabel.textColor = Theme.secondaryText
-        billingLabel.isHidden = plan.billedLine.isEmpty
 
         let inlinePriceInfo = inlinePriceInfoLabel(oldPrice: plan.crossedPrice, newPrice: plan.savingsText)
-        inlinePriceInfo.isHidden = (plan.crossedPrice == nil || plan.savingsText == nil)
 
         let divider = NSBox()
         divider.boxType = .separator
@@ -276,6 +337,7 @@ private final class PremiumPlansViewController: NSViewController {
         featuresStack.orientation = .vertical
         featuresStack.spacing = 9
         featuresStack.alignment = .leading
+        featuresStack.edgeInsets = NSEdgeInsets(top: 20, left: 22, bottom: 0, right: 0)
 
         let selectButton = NSButton(title: "Get \(plan.title)", target: self, action: #selector(didTapSelectPlan))
         selectButton.identifier = NSUserInterfaceItemIdentifier(plan.id)
@@ -291,25 +353,41 @@ private final class PremiumPlansViewController: NSViewController {
             ? NSColor(srgbRed: 189 / 255, green: 52 / 255, blue: 255 / 255, alpha: 1)
             : Theme.mutedButtonFill).cgColor
         selectButton.translatesAutoresizingMaskIntoConstraints = false
-        selectButton.heightAnchor.constraint(equalToConstant: 58).isActive = true
-
-        let spacer = NSView()
-        spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
+        selectButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
 
-        let content = NSStackView(views: [iconWell, titleLabel, subtitleLabel, billingPill, priceRow, billingLabel, inlinePriceInfo, divider, featuresStack, spacer, selectButton])
-        content.orientation = .vertical
-        content.spacing = 10
-        content.alignment = .centerX
-        content.translatesAutoresizingMaskIntoConstraints = false
-        card.addSubview(content)
+        var contentViews: [NSView] = [iconWell, titleLabel, subtitleLabel, priceRow]
+        if !plan.billedLine.isEmpty {
+            contentViews.append(billingLabel)
+        }
+        if plan.crossedPrice != nil, plan.savingsText != nil {
+            contentViews.append(inlinePriceInfo)
+        }
+        contentViews.append(contentsOf: [divider, featuresStack])
+
+        let verticalFlex = NSView()
+        verticalFlex.translatesAutoresizingMaskIntoConstraints = false
+        verticalFlex.setContentHuggingPriority(.defaultLow, for: .vertical)
+        verticalFlex.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+        let column = NSStackView(views: contentViews + [verticalFlex, selectButton])
+        column.orientation = .vertical
+        column.spacing = 10
+        column.alignment = .centerX
+        column.distribution = .fill
+        column.translatesAutoresizingMaskIntoConstraints = false
+        card.addSubview(column)
+        card.addSubview(topRightTag)
 
         NSLayoutConstraint.activate([
-            divider.widthAnchor.constraint(equalTo: content.widthAnchor),
-            content.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
-            content.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
-            content.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
-            content.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
-            selectButton.widthAnchor.constraint(equalTo: content.widthAnchor)
+            divider.widthAnchor.constraint(equalTo: column.widthAnchor),
+            featuresStack.widthAnchor.constraint(equalTo: column.widthAnchor),
+            column.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
+            column.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
+            column.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            column.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -12),
+            selectButton.widthAnchor.constraint(equalTo: column.widthAnchor),
+            topRightTag.topAnchor.constraint(equalTo: card.topAnchor, constant: 12),
+            topRightTag.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -12)
         ])
 
         return card