Преглед на файлове

Add interactive premium paywall popup and refine card UI.

Replace the temporary premium alert with a dedicated paywall window, add selectable pricing plans with improved styling and badge placement, and tighten spacing/typography for a cleaner premium purchase experience.

Made-with: Cursor
huzaifahayat12 преди 2 седмици
родител
ревизия
861b95a21e
променени са 1 файла, в които са добавени 421 реда и са изтрити 1 реда
  1. 421 1
      meetings_app/ViewController.swift

+ 421 - 1
meetings_app/ViewController.swift

@@ -30,6 +30,13 @@ private enum SettingsAction: Int {
30 30
     case shareApp = 4
31 31
 }
32 32
 
33
+private enum PremiumPlan: Int {
34
+    case weekly = 0
35
+    case monthly = 1
36
+    case yearly = 2
37
+    case lifetime = 3
38
+}
39
+
33 40
 final class ViewController: NSViewController {
34 41
     private let palette = Palette()
35 42
     private let typography = Typography()
@@ -46,6 +53,11 @@ final class ViewController: NSViewController {
46 53
     private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]()
47 54
     private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
48 55
     private weak var centeredTitleLabel: NSTextField?
56
+    private weak var paywallWindow: NSWindow?
57
+    private let paywallContentWidth: CGFloat = 520
58
+    private var selectedPremiumPlan: PremiumPlan = .monthly
59
+    private var paywallPlanViews: [PremiumPlan: NSView] = [:]
60
+    private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
49 61
 
50 62
     private let darkModeDefaultsKey = "settings.darkModeEnabled"
51 63
     private var darkModeEnabled: Bool {
@@ -158,7 +170,7 @@ private extension ViewController {
158 170
     }
159 171
 
160 172
     @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
161
-        showSimpleAlert(title: "Get Premium", message: "Premium features coming soon.")
173
+        showPaywall()
162 174
     }
163 175
 
164 176
     private func showSidebarPage(_ page: SidebarPage) {
@@ -226,6 +238,75 @@ private extension ViewController {
226 238
         alert.runModal()
227 239
     }
228 240
 
241
+    private func showPaywall() {
242
+        if let existing = paywallWindow {
243
+            existing.makeKeyAndOrderFront(nil)
244
+            NSApp.activate(ignoringOtherApps: true)
245
+            return
246
+        }
247
+
248
+        let content = makePaywallContent()
249
+        let controller = NSViewController()
250
+        controller.view = content
251
+
252
+        let panel = NSPanel(
253
+            contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
254
+            styleMask: [.titled, .closable, .fullSizeContentView],
255
+            backing: .buffered,
256
+            defer: false
257
+        )
258
+        panel.title = "Get Premium"
259
+        panel.titleVisibility = .hidden
260
+        panel.titlebarAppearsTransparent = true
261
+        panel.isFloatingPanel = true
262
+        panel.hidesOnDeactivate = false
263
+        panel.standardWindowButton(.closeButton)?.isHidden = true
264
+        panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
265
+        panel.standardWindowButton(.zoomButton)?.isHidden = true
266
+        panel.center()
267
+        panel.contentViewController = controller
268
+        panel.makeKeyAndOrderFront(nil)
269
+        NSApp.activate(ignoringOtherApps: true)
270
+        paywallWindow = panel
271
+    }
272
+
273
+    @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
274
+        paywallWindow?.close()
275
+        paywallWindow = nil
276
+    }
277
+
278
+    @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
279
+        guard let view = sender.view else { return }
280
+        let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
281
+        showSimpleAlert(title: text, message: "\(text) tapped.")
282
+    }
283
+
284
+    @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
285
+        guard let view = sender.view,
286
+              let plan = premiumPlanByView[ObjectIdentifier(view)] else { return }
287
+        selectedPremiumPlan = plan
288
+        updatePaywallPlanSelection()
289
+    }
290
+
291
+    private func updatePaywallPlanSelection() {
292
+        for (plan, view) in paywallPlanViews {
293
+            applyPaywallPlanStyle(view, isSelected: plan == selectedPremiumPlan)
294
+        }
295
+    }
296
+
297
+    private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
298
+        let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
299
+        let idleBorder = palette.inputBorder
300
+        let selectedBackground = NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
301
+        card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
302
+        card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
303
+        card.layer?.borderWidth = isSelected ? 2 : 1
304
+        card.layer?.shadowColor = NSColor.black.cgColor
305
+        card.layer?.shadowOpacity = isSelected ? 0.26 : 0.12
306
+        card.layer?.shadowOffset = CGSize(width: 0, height: -1)
307
+        card.layer?.shadowRadius = isSelected ? 10 : 5
308
+    }
309
+
229 310
     private func viewForPage(_ page: SidebarPage) -> NSView {
230 311
         if let cached = pageCache[page] { return cached }
231 312
         let built: NSView
@@ -491,6 +572,345 @@ private extension ViewController {
491 572
         return panel
492 573
     }
493 574
 
575
+    func makePaywallContent() -> NSView {
576
+        paywallPlanViews.removeAll()
577
+        premiumPlanByView.removeAll()
578
+
579
+        let panel = NSView()
580
+        panel.translatesAutoresizingMaskIntoConstraints = false
581
+        panel.wantsLayer = true
582
+        panel.layer?.backgroundColor = palette.pageBackground.cgColor
583
+
584
+        let contentStack = NSStackView()
585
+        contentStack.translatesAutoresizingMaskIntoConstraints = false
586
+        contentStack.orientation = .vertical
587
+        contentStack.spacing = 12
588
+        contentStack.alignment = .leading
589
+        panel.addSubview(contentStack)
590
+
591
+        let topRow = NSStackView()
592
+        topRow.translatesAutoresizingMaskIntoConstraints = false
593
+        topRow.orientation = .horizontal
594
+        topRow.alignment = .centerY
595
+        topRow.distribution = .fill
596
+        topRow.spacing = 10
597
+        topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
598
+        let topSpacer = NSView()
599
+        topSpacer.translatesAutoresizingMaskIntoConstraints = false
600
+        topRow.addArrangedSubview(topSpacer)
601
+        let closeButton = iconRoundButton("✕", size: 28)
602
+        topRow.addArrangedSubview(closeButton)
603
+        let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
604
+        closeButton.addGestureRecognizer(closeClick)
605
+        topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
606
+        contentStack.addArrangedSubview(topRow)
607
+
608
+        contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
609
+        let benefits = paywallBenefitsSection()
610
+        contentStack.addArrangedSubview(benefits)
611
+        contentStack.setCustomSpacing(18, after: benefits)
612
+
613
+        let weeklyCard = paywallPlanCard(
614
+            title: "Weekly",
615
+            price: "Rs 1,100.00",
616
+            badge: "Basic Deal",
617
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
618
+            subtitle: nil,
619
+            plan: .weekly,
620
+            strikePrice: nil
621
+        )
622
+        contentStack.addArrangedSubview(weeklyCard)
623
+
624
+        let monthlyCard = paywallPlanCard(
625
+            title: "Monthly",
626
+            price: "Rs 2,500.00",
627
+            badge: "Free Trial",
628
+            badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
629
+            subtitle: "625.00/week",
630
+            plan: .monthly,
631
+            strikePrice: nil
632
+        )
633
+        contentStack.addArrangedSubview(monthlyCard)
634
+
635
+        let yearlyCard = paywallPlanCard(
636
+            title: "Yearly",
637
+            price: "Rs 9,900.00",
638
+            badge: "Best Deal",
639
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
640
+            subtitle: "190.38/week",
641
+            plan: .yearly,
642
+            strikePrice: nil
643
+        )
644
+        contentStack.addArrangedSubview(yearlyCard)
645
+
646
+        let lifetimeCard = paywallPlanCard(
647
+            title: "Lifetime",
648
+            price: "Rs 14,900.00",
649
+            badge: "Save 50%",
650
+            badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
651
+            subtitle: nil,
652
+            plan: .lifetime,
653
+            strikePrice: "Rs 29,800.00"
654
+        )
655
+        contentStack.addArrangedSubview(lifetimeCard)
656
+        updatePaywallPlanSelection()
657
+        contentStack.setCustomSpacing(20, after: lifetimeCard)
658
+
659
+        let offer = textLabel("Free for 3 Days then Rs 2,500.00/month", font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
660
+        offer.alignment = .center
661
+        let offerWrap = NSView()
662
+        offerWrap.translatesAutoresizingMaskIntoConstraints = false
663
+        offerWrap.addSubview(offer)
664
+        NSLayoutConstraint.activate([
665
+            offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
666
+            offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
667
+            offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
668
+            offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
669
+        ])
670
+        contentStack.addArrangedSubview(offerWrap)
671
+        contentStack.setCustomSpacing(18, after: offerWrap)
672
+
673
+        let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
674
+        continueButton.translatesAutoresizingMaskIntoConstraints = false
675
+        continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
676
+        continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
677
+        styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
678
+        let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
679
+        continueButton.addSubview(continueLabel)
680
+        NSLayoutConstraint.activate([
681
+            continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
682
+            continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
683
+        ])
684
+        contentStack.addArrangedSubview(continueButton)
685
+        contentStack.setCustomSpacing(16, after: continueButton)
686
+
687
+        let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
688
+        secure.alignment = .center
689
+        let secureWrap = NSView()
690
+        secureWrap.translatesAutoresizingMaskIntoConstraints = false
691
+        secureWrap.addSubview(secure)
692
+        NSLayoutConstraint.activate([
693
+            secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
694
+            secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
695
+            secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
696
+            secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
697
+        ])
698
+        contentStack.addArrangedSubview(secureWrap)
699
+        contentStack.setCustomSpacing(16, after: secureWrap)
700
+
701
+        let footer = paywallFooterLinks()
702
+        contentStack.addArrangedSubview(footer)
703
+
704
+        NSLayoutConstraint.activate([
705
+            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
706
+            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
707
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
708
+            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
709
+        ])
710
+
711
+        return panel
712
+    }
713
+
714
+    func paywallPlanCard(
715
+        title: String,
716
+        price: String,
717
+        badge: String,
718
+        badgeColor: NSColor,
719
+        subtitle: String?,
720
+        plan: PremiumPlan,
721
+        strikePrice: String?
722
+    ) -> NSView {
723
+        let wrapper = NSView()
724
+        wrapper.translatesAutoresizingMaskIntoConstraints = false
725
+        wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
726
+        wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
727
+
728
+        let card = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
729
+        card.translatesAutoresizingMaskIntoConstraints = false
730
+        card.heightAnchor.constraint(equalToConstant: 82).isActive = true
731
+        wrapper.addSubview(card)
732
+        NSLayoutConstraint.activate([
733
+            card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
734
+            card.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
735
+            card.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 12),
736
+            card.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
737
+        ])
738
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
739
+
740
+        let badgeLabel = textLabel(badge, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: .white)
741
+        let badgeWrap = roundedContainer(cornerRadius: 10, color: badgeColor)
742
+        badgeWrap.translatesAutoresizingMaskIntoConstraints = false
743
+        badgeWrap.wantsLayer = true
744
+        badgeWrap.layer?.borderColor = NSColor(calibratedWhite: 1, alpha: 0.22).cgColor
745
+        badgeWrap.layer?.borderWidth = 1
746
+        badgeWrap.layer?.shadowColor = NSColor.black.cgColor
747
+        badgeWrap.layer?.shadowOpacity = 0.20
748
+        badgeWrap.layer?.shadowOffset = CGSize(width: 0, height: -1)
749
+        badgeWrap.layer?.shadowRadius = 3
750
+        badgeWrap.addSubview(badgeLabel)
751
+        NSLayoutConstraint.activate([
752
+            badgeLabel.leadingAnchor.constraint(equalTo: badgeWrap.leadingAnchor, constant: 8),
753
+            badgeLabel.trailingAnchor.constraint(equalTo: badgeWrap.trailingAnchor, constant: -8),
754
+            badgeLabel.topAnchor.constraint(equalTo: badgeWrap.topAnchor, constant: 2),
755
+            badgeLabel.bottomAnchor.constraint(equalTo: badgeWrap.bottomAnchor, constant: -2)
756
+        ])
757
+        wrapper.addSubview(badgeWrap)
758
+
759
+        let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: palette.primaryBlue)
760
+        card.addSubview(titleLabel)
761
+        let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 12, weight: .bold), color: palette.textPrimary)
762
+        card.addSubview(priceLabel)
763
+
764
+        NSLayoutConstraint.activate([
765
+            badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
766
+            badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
767
+
768
+            titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
769
+            titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
770
+
771
+            priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
772
+            priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
773
+        ])
774
+
775
+        if let subtitle {
776
+            let sub = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .semibold), color: palette.textSecondary)
777
+            card.addSubview(sub)
778
+            NSLayoutConstraint.activate([
779
+                sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
780
+                sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
781
+            ])
782
+        }
783
+
784
+        if let strikePrice {
785
+            let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
786
+            card.addSubview(strike)
787
+            NSLayoutConstraint.activate([
788
+                strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
789
+                strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
790
+            ])
791
+        }
792
+
793
+        let click = NSClickGestureRecognizer(target: self, action: #selector(paywallPlanClicked(_:)))
794
+        wrapper.addGestureRecognizer(click)
795
+        premiumPlanByView[ObjectIdentifier(wrapper)] = plan
796
+        paywallPlanViews[plan] = card
797
+
798
+        return wrapper
799
+    }
800
+
801
+    func paywallFooterLinks() -> NSView {
802
+        let wrap = NSView()
803
+        wrap.translatesAutoresizingMaskIntoConstraints = false
804
+        wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
805
+        wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
806
+
807
+        let row = NSStackView()
808
+        row.translatesAutoresizingMaskIntoConstraints = false
809
+        row.orientation = .horizontal
810
+        row.distribution = .fillEqually
811
+        row.alignment = .centerY
812
+        row.spacing = 0
813
+        wrap.addSubview(row)
814
+
815
+        row.addArrangedSubview(footerLink("Privacy Policy"))
816
+        row.addArrangedSubview(footerLink("Support"))
817
+        row.addArrangedSubview(footerLink("Terms of Services"))
818
+
819
+        NSLayoutConstraint.activate([
820
+            row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
821
+            row.trailingAnchor.constraint(equalTo: wrap.trailingAnchor),
822
+            row.topAnchor.constraint(equalTo: wrap.topAnchor),
823
+            row.bottomAnchor.constraint(equalTo: wrap.bottomAnchor)
824
+        ])
825
+
826
+        return wrap
827
+    }
828
+
829
+    func footerLink(_ title: String) -> NSView {
830
+        let container = HoverTrackingView()
831
+        container.translatesAutoresizingMaskIntoConstraints = false
832
+        let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
833
+        label.alignment = .center
834
+        container.addSubview(label)
835
+
836
+        NSLayoutConstraint.activate([
837
+            label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
838
+            label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
839
+        ])
840
+
841
+        let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
842
+        container.addGestureRecognizer(click)
843
+        container.onHoverChanged = { hovering in
844
+            label.textColor = hovering ? .white : self.palette.textSecondary
845
+        }
846
+        container.onHoverChanged?(false)
847
+        return container
848
+    }
849
+
850
+    func paywallBenefitsSection() -> NSView {
851
+        let stack = NSStackView()
852
+        stack.translatesAutoresizingMaskIntoConstraints = false
853
+        stack.orientation = .vertical
854
+        stack.spacing = 8
855
+        stack.alignment = .leading
856
+        stack.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
857
+
858
+        let rowOne = NSStackView()
859
+        rowOne.translatesAutoresizingMaskIntoConstraints = false
860
+        rowOne.orientation = .horizontal
861
+        rowOne.spacing = 10
862
+        rowOne.distribution = .fillEqually
863
+        rowOne.alignment = .centerY
864
+        rowOne.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
865
+        rowOne.addArrangedSubview(paywallBenefitItem(icon: "🖼️", text: "Virtual backgrounds"))
866
+
867
+        let rowTwo = NSStackView()
868
+        rowTwo.translatesAutoresizingMaskIntoConstraints = false
869
+        rowTwo.orientation = .horizontal
870
+        rowTwo.spacing = 10
871
+        rowTwo.distribution = .fillEqually
872
+        rowTwo.alignment = .centerY
873
+        rowTwo.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Tools for productivity"))
874
+        rowTwo.addArrangedSubview(paywallBenefitItem(icon: "🛟", text: "24/7 support"))
875
+
876
+        stack.addArrangedSubview(rowOne)
877
+        stack.addArrangedSubview(rowTwo)
878
+        return stack
879
+    }
880
+
881
+    func paywallBenefitItem(icon: String, text: String) -> NSView {
882
+        let card = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
883
+        card.translatesAutoresizingMaskIntoConstraints = false
884
+        card.heightAnchor.constraint(equalToConstant: 36).isActive = true
885
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
886
+
887
+        let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
888
+        iconWrap.translatesAutoresizingMaskIntoConstraints = false
889
+        iconWrap.widthAnchor.constraint(equalToConstant: 24).isActive = true
890
+        iconWrap.heightAnchor.constraint(equalToConstant: 24).isActive = true
891
+        styleSurface(iconWrap, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
892
+
893
+        let iconLabel = textLabel(icon, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.primaryBlue)
894
+        iconWrap.addSubview(iconLabel)
895
+        NSLayoutConstraint.activate([
896
+            iconLabel.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor),
897
+            iconLabel.centerYAnchor.constraint(equalTo: iconWrap.centerYAnchor)
898
+        ])
899
+
900
+        let title = textLabel(text, font: NSFont.systemFont(ofSize: 11, weight: .medium), color: palette.textPrimary)
901
+
902
+        card.addSubview(iconWrap)
903
+        card.addSubview(title)
904
+        NSLayoutConstraint.activate([
905
+            iconWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 8),
906
+            iconWrap.centerYAnchor.constraint(equalTo: card.centerYAnchor),
907
+            title.leadingAnchor.constraint(equalTo: iconWrap.trailingAnchor, constant: 10),
908
+            title.centerYAnchor.constraint(equalTo: card.centerYAnchor),
909
+            title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -8)
910
+        ])
911
+        return card
912
+    }
913
+
494 914
     func joinWithURLHeading() -> NSView {
495 915
         let container = NSView()
496 916
         container.translatesAutoresizingMaskIntoConstraints = false