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

CV Maker: fix duplicate-id selection; remove preview overlay

Track gallery cards by per-card UUID so only one card highlights when
templates share the same catalog id. Remove the hover Preview overlay
and palette overlay tint; card click selects the template only.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 3
Родитель
Сommit
a624e0e698

+ 36 - 103
App for Indeed/Views/CVMakerPageView.swift

@@ -4,7 +4,7 @@
4 4
 //
5 5
 //  Template gallery for the CV Maker sidebar destination. Light-theme rendering
6 6
 //  inspired by a dark reference UI: page header, category toggle, style chips,
7
-//  4-column thumbnail grid with hover Preview overlay, and a sticky bottom CTA.
7
+//  4-column thumbnail grid and a sticky bottom CTA.
8 8
 //
9 9
 
10 10
 import Cocoa
@@ -512,7 +512,6 @@ final class CVMakerPageView: NSView {
512 512
         static let ctaBackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
513 513
         static let ctaHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
514 514
         static let ctaText = NSColor.white
515
-        static let overlayTint = NSColor.black.withAlphaComponent(0.45)
516 515
         static let selectionGlow = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
517 516
         static let gradientTop = NSColor(srgbRed: 250 / 255, green: 252 / 255, blue: 1, alpha: 1)
518 517
         static let gradientBottom = NSColor(srgbRed: 236 / 255, green: 244 / 255, blue: 1, alpha: 1)
@@ -534,11 +533,14 @@ final class CVMakerPageView: NSView {
534 533
     private var selectedGroup: CVCategoryGroup = .designBased
535 534
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
536 535
     private var selectedTemplateID: String? = "paper-white"
536
+    /// Exactly one gallery card — avoids multiple highlighted cards when catalog entries share the same `template.id`.
537
+    private var selectedTemplateCardToken: UUID?
537 538
     /// Shown immediately; replaced when `CVTemplateFetchService` returns AI-generated entries.
538 539
     private var activeCatalog: [CVTemplate] = CVTemplateCatalog.all
539 540
     private var groupTabButtons: [CVCategoryGroup: CVChipButton] = [:]
540 541
     private var familyChipButtons: [CVDesignFamily?: CVChipButton] = [:]
541
-    private var templateCardsByID: [String: CVTemplateCard] = [:]
542
+    /// Every visible gallery card (not keyed by id — duplicate AI ids would collapse in a dictionary and break single-selection visuals).
543
+    private var templateCardsInGrid: [CVTemplateCard] = []
542 544
 
543 545
     private var appliedGridColumnCount: Int = 0
544 546
 
@@ -764,7 +766,7 @@ final class CVMakerPageView: NSView {
764 766
             gridStack.removeArrangedSubview($0)
765 767
             $0.removeFromSuperview()
766 768
         }
767
-        templateCardsByID.removeAll()
769
+        templateCardsInGrid.removeAll()
768 770
 
769 771
         let templates = visibleTemplates
770 772
         if templates.isEmpty {
@@ -772,6 +774,7 @@ final class CVMakerPageView: NSView {
772 774
             empty.font = .systemFont(ofSize: 13)
773 775
             empty.textColor = Palette.secondaryText
774 776
             gridStack.addArrangedSubview(empty)
777
+            selectedTemplateCardToken = nil
775 778
             return
776 779
         }
777 780
 
@@ -792,10 +795,12 @@ final class CVMakerPageView: NSView {
792 795
                     let template = templates[position]
793 796
                     let card = CVTemplateCard(template: template, palette: palette())
794 797
                     card.translatesAutoresizingMaskIntoConstraints = false
795
-                    card.onSelect = { [weak self] in self?.didSelectTemplate(template.id) }
796
-                    card.onPreview = { [weak self] in self?.didPreviewTemplate(template.id) }
798
+                    card.onSelect = { [weak self] in
799
+                        guard let self else { return }
800
+                        self.didSelectCard(card)
801
+                    }
797 802
                     row.addArrangedSubview(card)
798
-                    templateCardsByID[template.id] = card
803
+                    templateCardsInGrid.append(card)
799 804
                 } else {
800 805
                     let filler = NSView()
801 806
                     filler.translatesAutoresizingMaskIntoConstraints = false
@@ -807,8 +812,15 @@ final class CVMakerPageView: NSView {
807 812
             index += columns
808 813
         }
809 814
 
810
-        if selectedTemplateID == nil || templateCardsByID[selectedTemplateID ?? ""] == nil {
811
-            selectedTemplateID = templates.first?.id
815
+        if let sid = selectedTemplateID,
816
+           let match = templateCardsInGrid.first(where: { $0.templateID == sid }) {
817
+            selectedTemplateCardToken = match.selectionToken
818
+        } else if let first = templateCardsInGrid.first {
819
+            selectedTemplateCardToken = first.selectionToken
820
+            selectedTemplateID = first.templateID
821
+        } else {
822
+            selectedTemplateCardToken = nil
823
+            selectedTemplateID = nil
812 824
         }
813 825
         applySelectionToCards()
814 826
     }
@@ -835,8 +847,9 @@ final class CVMakerPageView: NSView {
835 847
     }
836 848
 
837 849
     private func applySelectionToCards() {
838
-        for (id, card) in templateCardsByID {
839
-            card.isSelected = (id == selectedTemplateID)
850
+        let token = selectedTemplateCardToken
851
+        for card in templateCardsInGrid {
852
+            card.isSelected = (card.selectionToken == token)
840 853
         }
841 854
     }
842 855
 
@@ -855,8 +868,7 @@ final class CVMakerPageView: NSView {
855 868
             previewAccentRed: Palette.previewAccentRed,
856 869
             previewAccentBlue: Palette.previewAccentBlue,
857 870
             primaryText: Palette.primaryText,
858
-            secondaryText: Palette.secondaryText,
859
-            overlayTint: Palette.overlayTint
871
+            secondaryText: Palette.secondaryText
860 872
         )
861 873
     }
862 874
 
@@ -878,21 +890,12 @@ final class CVMakerPageView: NSView {
878 890
         updateSelectedChipStates()
879 891
     }
880 892
 
881
-    private func didSelectTemplate(_ id: String) {
882
-        selectedTemplateID = id
893
+    private func didSelectCard(_ card: CVTemplateCard) {
894
+        selectedTemplateCardToken = card.selectionToken
895
+        selectedTemplateID = card.templateID
883 896
         applySelectionToCards()
884 897
     }
885 898
 
886
-    private func didPreviewTemplate(_ id: String) {
887
-        selectedTemplateID = id
888
-        applySelectionToCards()
889
-        guard let template = activeCatalog.first(where: { $0.id == id }) else { return }
890
-        presentPlaceholderAlert(
891
-            title: "Preview \"\(template.name)\"",
892
-            message: "Full-page previews and PDF export are coming soon."
893
-        )
894
-    }
895
-
896 899
     @objc private func didTapUseTemplate() {
897 900
         guard let id = selectedTemplateID,
898 901
               let template = activeCatalog.first(where: { $0.id == id }) else {
@@ -1173,22 +1176,20 @@ private final class CVChipButton: NSView {
1173 1176
 
1174 1177
 // MARK: - Template card
1175 1178
 
1176
-/// Premium gallery card: live résumé thumbnail, glass-style preview overlay, soft
1177
-/// shadow, and an animated brand border when selected.
1179
+/// Premium gallery card: live résumé thumbnail, soft shadow, and an animated
1180
+/// brand border when selected.
1178 1181
 private final class CVTemplateCard: NSView {
1179 1182
     var onSelect: (() -> Void)?
1180
-    var onPreview: (() -> Void)?
1181 1183
     var isSelected: Bool = false { didSet { applyChrome() } }
1184
+    /// Distinguishes this card from others that may share the same catalog `template.id`.
1185
+    let selectionToken = UUID()
1186
+    var templateID: String { template.id }
1182 1187
     private let template: CVTemplate
1183 1188
     private let palette: CVTemplateCardPalette
1184 1189
     private let previewSurface = NSView()
1185 1190
     private let preview: CVTemplatePreviewView
1186 1191
     private let nameLabel = NSTextField(labelWithString: "")
1187 1192
     private let categoryLabel = NSTextField(labelWithString: "")
1188
-    private let overlay = NSView()
1189
-    private let overlayBadge = NSView()
1190
-    private let overlayBadgeLabel = NSTextField(labelWithString: "Preview")
1191
-    private let overlayBadgeIcon = NSImageView()
1192 1193
     private var trackingArea: NSTrackingArea?
1193 1194
     private var isHovering: Bool = false
1194 1195
     private var didPushCursor: Bool = false
@@ -1248,8 +1249,6 @@ private final class CVTemplateCard: NSView {
1248 1249
 
1249 1250
         addSubview(previewSurface)
1250 1251
         addSubview(footer)
1251
-        addSubview(overlay)
1252
-        configureOverlay()
1253 1252
 
1254 1253
         NSLayoutConstraint.activate([
1255 1254
             previewSurface.topAnchor.constraint(equalTo: topAnchor),
@@ -1265,12 +1264,7 @@ private final class CVTemplateCard: NSView {
1265 1264
             footer.topAnchor.constraint(equalTo: previewSurface.bottomAnchor),
1266 1265
             footer.leadingAnchor.constraint(equalTo: leadingAnchor),
1267 1266
             footer.trailingAnchor.constraint(equalTo: trailingAnchor),
1268
-            footer.bottomAnchor.constraint(equalTo: bottomAnchor),
1269
-
1270
-            overlay.topAnchor.constraint(equalTo: previewSurface.topAnchor),
1271
-            overlay.leadingAnchor.constraint(equalTo: previewSurface.leadingAnchor),
1272
-            overlay.trailingAnchor.constraint(equalTo: previewSurface.trailingAnchor),
1273
-            overlay.bottomAnchor.constraint(equalTo: previewSurface.bottomAnchor)
1267
+            footer.bottomAnchor.constraint(equalTo: bottomAnchor)
1274 1268
         ])
1275 1269
         applyChrome()
1276 1270
     }
@@ -1285,50 +1279,6 @@ private final class CVTemplateCard: NSView {
1285 1279
         layer?.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 24, cornerHeight: 24, transform: nil)
1286 1280
     }
1287 1281
 
1288
-    private func configureOverlay() {
1289
-        overlay.translatesAutoresizingMaskIntoConstraints = false
1290
-        overlay.wantsLayer = true
1291
-        overlay.layer?.backgroundColor = palette.overlayTint.cgColor
1292
-        overlay.alphaValue = 0
1293
-        overlay.isHidden = false
1294
-
1295
-        overlayBadge.translatesAutoresizingMaskIntoConstraints = false
1296
-        overlayBadge.wantsLayer = true
1297
-        overlayBadge.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.22).cgColor
1298
-        overlayBadge.layer?.cornerRadius = 20
1299
-        overlayBadge.layer?.borderWidth = 1
1300
-        overlayBadge.layer?.borderColor = NSColor.white.withAlphaComponent(0.45).cgColor
1301
-
1302
-        overlayBadgeIcon.translatesAutoresizingMaskIntoConstraints = false
1303
-        overlayBadgeIcon.image = NSImage(systemSymbolName: "eye", accessibilityDescription: nil)
1304
-        overlayBadgeIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
1305
-        overlayBadgeIcon.contentTintColor = .white
1306
-
1307
-        overlayBadgeLabel.font = .systemFont(ofSize: 12.5, weight: .semibold)
1308
-        overlayBadgeLabel.textColor = .white
1309
-        overlayBadgeLabel.isBordered = false
1310
-        overlayBadgeLabel.drawsBackground = false
1311
-        overlayBadgeLabel.isEditable = false
1312
-        overlayBadgeLabel.isSelectable = false
1313
-
1314
-        let badgeStack = NSStackView(views: [overlayBadgeIcon, overlayBadgeLabel])
1315
-        badgeStack.orientation = .horizontal
1316
-        badgeStack.spacing = 7
1317
-        badgeStack.alignment = .centerY
1318
-        badgeStack.translatesAutoresizingMaskIntoConstraints = false
1319
-
1320
-        overlayBadge.addSubview(badgeStack)
1321
-        overlay.addSubview(overlayBadge)
1322
-        NSLayoutConstraint.activate([
1323
-            badgeStack.leadingAnchor.constraint(equalTo: overlayBadge.leadingAnchor, constant: 16),
1324
-            badgeStack.trailingAnchor.constraint(equalTo: overlayBadge.trailingAnchor, constant: -16),
1325
-            badgeStack.topAnchor.constraint(equalTo: overlayBadge.topAnchor, constant: 9),
1326
-            badgeStack.bottomAnchor.constraint(equalTo: overlayBadge.bottomAnchor, constant: -9),
1327
-            overlayBadge.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
1328
-            overlayBadge.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
1329
-        ])
1330
-    }
1331
-
1332 1282
     override func hitTest(_ point: NSPoint) -> NSView? {
1333 1283
         guard let superview else { return super.hitTest(point) }
1334 1284
         let local = convert(point, from: superview)
@@ -1336,13 +1286,8 @@ private final class CVTemplateCard: NSView {
1336 1286
     }
1337 1287
 
1338 1288
     override func mouseDown(with event: NSEvent) {
1339
-        let local = convert(event.locationInWindow, from: nil)
1340
-        if overlay.frame.contains(local) {
1341
-            onPreview?()
1342
-        } else {
1343
-            playTapPulse()
1344
-            onSelect?()
1345
-        }
1289
+        playTapPulse()
1290
+        onSelect?()
1346 1291
     }
1347 1292
 
1348 1293
     private func playTapPulse() {
@@ -1372,7 +1317,6 @@ private final class CVTemplateCard: NSView {
1372 1317
     override func mouseEntered(with event: NSEvent) {
1373 1318
         super.mouseEntered(with: event)
1374 1319
         isHovering = true
1375
-        animateOverlay(visible: true)
1376 1320
         applyChrome()
1377 1321
         if !didPushCursor {
1378 1322
             NSCursor.pointingHand.push()
@@ -1383,7 +1327,6 @@ private final class CVTemplateCard: NSView {
1383 1327
     override func mouseExited(with event: NSEvent) {
1384 1328
         super.mouseExited(with: event)
1385 1329
         isHovering = false
1386
-        animateOverlay(visible: false)
1387 1330
         applyChrome()
1388 1331
         if didPushCursor {
1389 1332
             NSCursor.pop()
@@ -1400,16 +1343,6 @@ private final class CVTemplateCard: NSView {
1400 1343
         }
1401 1344
     }
1402 1345
 
1403
-    private func animateOverlay(visible: Bool) {
1404
-        let target: CGFloat = visible ? 1 : 0
1405
-        NSAnimationContext.runAnimationGroup { context in
1406
-            context.duration = 0.18
1407
-            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
1408
-            context.allowsImplicitAnimation = true
1409
-            overlay.animator().alphaValue = target
1410
-        }
1411
-    }
1412
-
1413 1346
     private func applyChrome() {
1414 1347
         if isSelected {
1415 1348
             layer?.borderColor = palette.borderSelected.cgColor

+ 0 - 1
App for Indeed/Views/CVTemplateMiniPreview.swift

@@ -25,7 +25,6 @@ struct CVTemplateCardPalette {
25 25
     let previewAccentBlue: NSColor
26 26
     let primaryText: NSColor
27 27
     let secondaryText: NSColor
28
-    let overlayTint: NSColor
29 28
 }
30 29
 
31 30
 // MARK: - Demo résumé content