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

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
2 измененных файлов с 36 добавлено и 104 удалено
  1. 36 103
      App for Indeed/Views/CVMakerPageView.swift
  2. 0 1
      App for Indeed/Views/CVTemplateMiniPreview.swift

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

@@ -4,7 +4,7 @@
4
 //
4
 //
5
 //  Template gallery for the CV Maker sidebar destination. Light-theme rendering
5
 //  Template gallery for the CV Maker sidebar destination. Light-theme rendering
6
 //  inspired by a dark reference UI: page header, category toggle, style chips,
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
 import Cocoa
10
 import Cocoa
@@ -512,7 +512,6 @@ final class CVMakerPageView: NSView {
512
         static let ctaBackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
512
         static let ctaBackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
513
         static let ctaHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
513
         static let ctaHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
514
         static let ctaText = NSColor.white
514
         static let ctaText = NSColor.white
515
-        static let overlayTint = NSColor.black.withAlphaComponent(0.45)
516
         static let selectionGlow = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
515
         static let selectionGlow = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
517
         static let gradientTop = NSColor(srgbRed: 250 / 255, green: 252 / 255, blue: 1, alpha: 1)
516
         static let gradientTop = NSColor(srgbRed: 250 / 255, green: 252 / 255, blue: 1, alpha: 1)
518
         static let gradientBottom = NSColor(srgbRed: 236 / 255, green: 244 / 255, blue: 1, alpha: 1)
517
         static let gradientBottom = NSColor(srgbRed: 236 / 255, green: 244 / 255, blue: 1, alpha: 1)
@@ -534,11 +533,14 @@ final class CVMakerPageView: NSView {
534
     private var selectedGroup: CVCategoryGroup = .designBased
533
     private var selectedGroup: CVCategoryGroup = .designBased
535
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
534
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
536
     private var selectedTemplateID: String? = "paper-white"
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
     /// Shown immediately; replaced when `CVTemplateFetchService` returns AI-generated entries.
538
     /// Shown immediately; replaced when `CVTemplateFetchService` returns AI-generated entries.
538
     private var activeCatalog: [CVTemplate] = CVTemplateCatalog.all
539
     private var activeCatalog: [CVTemplate] = CVTemplateCatalog.all
539
     private var groupTabButtons: [CVCategoryGroup: CVChipButton] = [:]
540
     private var groupTabButtons: [CVCategoryGroup: CVChipButton] = [:]
540
     private var familyChipButtons: [CVDesignFamily?: CVChipButton] = [:]
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
     private var appliedGridColumnCount: Int = 0
545
     private var appliedGridColumnCount: Int = 0
544
 
546
 
@@ -764,7 +766,7 @@ final class CVMakerPageView: NSView {
764
             gridStack.removeArrangedSubview($0)
766
             gridStack.removeArrangedSubview($0)
765
             $0.removeFromSuperview()
767
             $0.removeFromSuperview()
766
         }
768
         }
767
-        templateCardsByID.removeAll()
769
+        templateCardsInGrid.removeAll()
768
 
770
 
769
         let templates = visibleTemplates
771
         let templates = visibleTemplates
770
         if templates.isEmpty {
772
         if templates.isEmpty {
@@ -772,6 +774,7 @@ final class CVMakerPageView: NSView {
772
             empty.font = .systemFont(ofSize: 13)
774
             empty.font = .systemFont(ofSize: 13)
773
             empty.textColor = Palette.secondaryText
775
             empty.textColor = Palette.secondaryText
774
             gridStack.addArrangedSubview(empty)
776
             gridStack.addArrangedSubview(empty)
777
+            selectedTemplateCardToken = nil
775
             return
778
             return
776
         }
779
         }
777
 
780
 
@@ -792,10 +795,12 @@ final class CVMakerPageView: NSView {
792
                     let template = templates[position]
795
                     let template = templates[position]
793
                     let card = CVTemplateCard(template: template, palette: palette())
796
                     let card = CVTemplateCard(template: template, palette: palette())
794
                     card.translatesAutoresizingMaskIntoConstraints = false
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
                     row.addArrangedSubview(card)
802
                     row.addArrangedSubview(card)
798
-                    templateCardsByID[template.id] = card
803
+                    templateCardsInGrid.append(card)
799
                 } else {
804
                 } else {
800
                     let filler = NSView()
805
                     let filler = NSView()
801
                     filler.translatesAutoresizingMaskIntoConstraints = false
806
                     filler.translatesAutoresizingMaskIntoConstraints = false
@@ -807,8 +812,15 @@ final class CVMakerPageView: NSView {
807
             index += columns
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
         applySelectionToCards()
825
         applySelectionToCards()
814
     }
826
     }
@@ -835,8 +847,9 @@ final class CVMakerPageView: NSView {
835
     }
847
     }
836
 
848
 
837
     private func applySelectionToCards() {
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
             previewAccentRed: Palette.previewAccentRed,
868
             previewAccentRed: Palette.previewAccentRed,
856
             previewAccentBlue: Palette.previewAccentBlue,
869
             previewAccentBlue: Palette.previewAccentBlue,
857
             primaryText: Palette.primaryText,
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
         updateSelectedChipStates()
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
         applySelectionToCards()
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
     @objc private func didTapUseTemplate() {
899
     @objc private func didTapUseTemplate() {
897
         guard let id = selectedTemplateID,
900
         guard let id = selectedTemplateID,
898
               let template = activeCatalog.first(where: { $0.id == id }) else {
901
               let template = activeCatalog.first(where: { $0.id == id }) else {
@@ -1173,22 +1176,20 @@ private final class CVChipButton: NSView {
1173
 
1176
 
1174
 // MARK: - Template card
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
 private final class CVTemplateCard: NSView {
1181
 private final class CVTemplateCard: NSView {
1179
     var onSelect: (() -> Void)?
1182
     var onSelect: (() -> Void)?
1180
-    var onPreview: (() -> Void)?
1181
     var isSelected: Bool = false { didSet { applyChrome() } }
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
     private let template: CVTemplate
1187
     private let template: CVTemplate
1183
     private let palette: CVTemplateCardPalette
1188
     private let palette: CVTemplateCardPalette
1184
     private let previewSurface = NSView()
1189
     private let previewSurface = NSView()
1185
     private let preview: CVTemplatePreviewView
1190
     private let preview: CVTemplatePreviewView
1186
     private let nameLabel = NSTextField(labelWithString: "")
1191
     private let nameLabel = NSTextField(labelWithString: "")
1187
     private let categoryLabel = NSTextField(labelWithString: "")
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
     private var trackingArea: NSTrackingArea?
1193
     private var trackingArea: NSTrackingArea?
1193
     private var isHovering: Bool = false
1194
     private var isHovering: Bool = false
1194
     private var didPushCursor: Bool = false
1195
     private var didPushCursor: Bool = false
@@ -1248,8 +1249,6 @@ private final class CVTemplateCard: NSView {
1248
 
1249
 
1249
         addSubview(previewSurface)
1250
         addSubview(previewSurface)
1250
         addSubview(footer)
1251
         addSubview(footer)
1251
-        addSubview(overlay)
1252
-        configureOverlay()
1253
 
1252
 
1254
         NSLayoutConstraint.activate([
1253
         NSLayoutConstraint.activate([
1255
             previewSurface.topAnchor.constraint(equalTo: topAnchor),
1254
             previewSurface.topAnchor.constraint(equalTo: topAnchor),
@@ -1265,12 +1264,7 @@ private final class CVTemplateCard: NSView {
1265
             footer.topAnchor.constraint(equalTo: previewSurface.bottomAnchor),
1264
             footer.topAnchor.constraint(equalTo: previewSurface.bottomAnchor),
1266
             footer.leadingAnchor.constraint(equalTo: leadingAnchor),
1265
             footer.leadingAnchor.constraint(equalTo: leadingAnchor),
1267
             footer.trailingAnchor.constraint(equalTo: trailingAnchor),
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
         applyChrome()
1269
         applyChrome()
1276
     }
1270
     }
@@ -1285,50 +1279,6 @@ private final class CVTemplateCard: NSView {
1285
         layer?.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 24, cornerHeight: 24, transform: nil)
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
     override func hitTest(_ point: NSPoint) -> NSView? {
1282
     override func hitTest(_ point: NSPoint) -> NSView? {
1333
         guard let superview else { return super.hitTest(point) }
1283
         guard let superview else { return super.hitTest(point) }
1334
         let local = convert(point, from: superview)
1284
         let local = convert(point, from: superview)
@@ -1336,13 +1286,8 @@ private final class CVTemplateCard: NSView {
1336
     }
1286
     }
1337
 
1287
 
1338
     override func mouseDown(with event: NSEvent) {
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
     private func playTapPulse() {
1293
     private func playTapPulse() {
@@ -1372,7 +1317,6 @@ private final class CVTemplateCard: NSView {
1372
     override func mouseEntered(with event: NSEvent) {
1317
     override func mouseEntered(with event: NSEvent) {
1373
         super.mouseEntered(with: event)
1318
         super.mouseEntered(with: event)
1374
         isHovering = true
1319
         isHovering = true
1375
-        animateOverlay(visible: true)
1376
         applyChrome()
1320
         applyChrome()
1377
         if !didPushCursor {
1321
         if !didPushCursor {
1378
             NSCursor.pointingHand.push()
1322
             NSCursor.pointingHand.push()
@@ -1383,7 +1327,6 @@ private final class CVTemplateCard: NSView {
1383
     override func mouseExited(with event: NSEvent) {
1327
     override func mouseExited(with event: NSEvent) {
1384
         super.mouseExited(with: event)
1328
         super.mouseExited(with: event)
1385
         isHovering = false
1329
         isHovering = false
1386
-        animateOverlay(visible: false)
1387
         applyChrome()
1330
         applyChrome()
1388
         if didPushCursor {
1331
         if didPushCursor {
1389
             NSCursor.pop()
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
     private func applyChrome() {
1346
     private func applyChrome() {
1414
         if isSelected {
1347
         if isSelected {
1415
             layer?.borderColor = palette.borderSelected.cgColor
1348
             layer?.borderColor = palette.borderSelected.cgColor

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

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